@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.
- package/README.md +166 -0
- package/dist/HttpApiVirtualModulePlugin.d.ts +26 -0
- package/dist/HttpApiVirtualModulePlugin.d.ts.map +1 -0
- package/dist/HttpApiVirtualModulePlugin.js +301 -0
- package/dist/RouterVirtualModulePlugin.d.ts +23 -0
- package/dist/RouterVirtualModulePlugin.d.ts.map +1 -0
- package/dist/RouterVirtualModulePlugin.js +176 -0
- package/dist/createTypeInfoApiSessionForApp.d.ts +29 -0
- package/dist/createTypeInfoApiSessionForApp.d.ts.map +1 -0
- package/dist/createTypeInfoApiSessionForApp.js +46 -0
- package/dist/httpapi/defineApiHandler.d.ts +70 -0
- package/dist/httpapi/defineApiHandler.d.ts.map +1 -0
- package/dist/httpapi/defineApiHandler.js +23 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/internal/appConfigTypes.d.ts +11 -0
- package/dist/internal/appConfigTypes.d.ts.map +1 -0
- package/dist/internal/appConfigTypes.js +1 -0
- package/dist/internal/appLayerTypes.d.ts +24 -0
- package/dist/internal/appLayerTypes.d.ts.map +1 -0
- package/dist/internal/appLayerTypes.js +28 -0
- package/dist/internal/buildRouteDescriptors.d.ts +48 -0
- package/dist/internal/buildRouteDescriptors.d.ts.map +1 -0
- package/dist/internal/buildRouteDescriptors.js +371 -0
- package/dist/internal/emitHttpApiSource.d.ts +18 -0
- package/dist/internal/emitHttpApiSource.d.ts.map +1 -0
- package/dist/internal/emitHttpApiSource.js +404 -0
- package/dist/internal/emitRouterHelpers.d.ts +17 -0
- package/dist/internal/emitRouterHelpers.d.ts.map +1 -0
- package/dist/internal/emitRouterHelpers.js +74 -0
- package/dist/internal/emitRouterSource.d.ts +8 -0
- package/dist/internal/emitRouterSource.d.ts.map +1 -0
- package/dist/internal/emitRouterSource.js +139 -0
- package/dist/internal/extractHttpApiLiterals.d.ts +17 -0
- package/dist/internal/extractHttpApiLiterals.d.ts.map +1 -0
- package/dist/internal/extractHttpApiLiterals.js +45 -0
- package/dist/internal/httpapiDescriptorTree.d.ts +75 -0
- package/dist/internal/httpapiDescriptorTree.d.ts.map +1 -0
- package/dist/internal/httpapiDescriptorTree.js +182 -0
- package/dist/internal/httpapiEndpointContract.d.ts +32 -0
- package/dist/internal/httpapiEndpointContract.d.ts.map +1 -0
- package/dist/internal/httpapiEndpointContract.js +79 -0
- package/dist/internal/httpapiFileRoles.d.ts +67 -0
- package/dist/internal/httpapiFileRoles.d.ts.map +1 -0
- package/dist/internal/httpapiFileRoles.js +145 -0
- package/dist/internal/httpapiId.d.ts +30 -0
- package/dist/internal/httpapiId.d.ts.map +1 -0
- package/dist/internal/httpapiId.js +57 -0
- package/dist/internal/httpapiOpenApiConfig.d.ts +87 -0
- package/dist/internal/httpapiOpenApiConfig.d.ts.map +1 -0
- package/dist/internal/httpapiOpenApiConfig.js +144 -0
- package/dist/internal/httpapiSort.d.ts +16 -0
- package/dist/internal/httpapiSort.d.ts.map +1 -0
- package/dist/internal/httpapiSort.js +29 -0
- package/dist/internal/path.d.ts +16 -0
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/path.js +38 -0
- package/dist/internal/resolveConfig.d.ts +8 -0
- package/dist/internal/resolveConfig.d.ts.map +1 -0
- package/dist/internal/resolveConfig.js +13 -0
- package/dist/internal/routeIdentifiers.d.ts +18 -0
- package/dist/internal/routeIdentifiers.d.ts.map +1 -0
- package/dist/internal/routeIdentifiers.js +90 -0
- package/dist/internal/routeTypeNode.d.ts +45 -0
- package/dist/internal/routeTypeNode.d.ts.map +1 -0
- package/dist/internal/routeTypeNode.js +93 -0
- package/dist/internal/routerDescriptorTree.d.ts +110 -0
- package/dist/internal/routerDescriptorTree.d.ts.map +1 -0
- package/dist/internal/routerDescriptorTree.js +230 -0
- package/dist/internal/typeTargetBootstrap.d.ts +2 -0
- package/dist/internal/typeTargetBootstrap.d.ts.map +1 -0
- package/dist/internal/typeTargetBootstrap.js +23 -0
- package/dist/internal/typeTargetBootstrapHttpApi.d.ts +2 -0
- package/dist/internal/typeTargetBootstrapHttpApi.d.ts.map +1 -0
- package/dist/internal/typeTargetBootstrapHttpApi.js +21 -0
- package/dist/internal/typeTargetSpecs.d.ts +15 -0
- package/dist/internal/typeTargetSpecs.d.ts.map +1 -0
- package/dist/internal/typeTargetSpecs.js +32 -0
- package/dist/internal/validation.d.ts +12 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +32 -0
- package/package.json +45 -0
- package/src/HttpApiVirtualModulePlugin.test.ts +1062 -0
- package/src/HttpApiVirtualModulePlugin.ts +376 -0
- package/src/RouterVirtualModulePlugin.test.ts +1254 -0
- package/src/RouterVirtualModulePlugin.ts +242 -0
- package/src/createTypeInfoApiSessionForApp.ts +57 -0
- package/src/defineApiHandler.test.ts +100 -0
- package/src/httpapi/defineApiHandler.ts +141 -0
- package/src/httpapiDescriptorTree.test.ts +124 -0
- package/src/httpapiEndpointContract.test.ts +160 -0
- package/src/httpapiFileRoles.test.ts +105 -0
- package/src/index.ts +40 -0
- package/src/internal/appConfigTypes.ts +12 -0
- package/src/internal/appLayerTypes.ts +79 -0
- package/src/internal/buildRouteDescriptors.ts +489 -0
- package/src/internal/emitHttpApiSource.ts +563 -0
- package/src/internal/emitRouterHelpers.ts +89 -0
- package/src/internal/emitRouterSource.ts +191 -0
- package/src/internal/extractHttpApiLiterals.ts +67 -0
- package/src/internal/httpapiDescriptorTree.ts +283 -0
- package/src/internal/httpapiEndpointContract.ts +110 -0
- package/src/internal/httpapiFileRoles.ts +204 -0
- package/src/internal/httpapiId.ts +78 -0
- package/src/internal/httpapiOpenApiConfig.ts +228 -0
- package/src/internal/httpapiSort.ts +39 -0
- package/src/internal/path.ts +46 -0
- package/src/internal/resolveConfig.ts +15 -0
- package/src/internal/routeIdentifiers.ts +93 -0
- package/src/internal/routeTypeNode.ts +120 -0
- package/src/internal/routerDescriptorTree.ts +366 -0
- package/src/internal/typeTargetBootstrap.ts +24 -0
- package/src/internal/typeTargetBootstrapHttpApi.ts +22 -0
- package/src/internal/typeTargetSpecs.ts +35 -0
- package/src/internal/validation.ts +46 -0
|
@@ -0,0 +1,563 @@
|
|
|
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 type {
|
|
13
|
+
DirectoryConventionRef,
|
|
14
|
+
HttpApiDescriptorTree,
|
|
15
|
+
HttpApiEndpointNode,
|
|
16
|
+
HttpApiGroupNode,
|
|
17
|
+
HttpApiTreeNode,
|
|
18
|
+
RootOrGroupConventionRef,
|
|
19
|
+
} from "./httpapiDescriptorTree.js";
|
|
20
|
+
import type {
|
|
21
|
+
HttpApiDirectoryCompanionKind,
|
|
22
|
+
HttpApiEndpointCompanionKind,
|
|
23
|
+
} from "./httpapiFileRoles.js";
|
|
24
|
+
import { compareHttpApiPathOrder } from "./httpapiFileRoles.js";
|
|
25
|
+
import { stripScriptExtension, toPosixPath } from "./path.js";
|
|
26
|
+
import { makeUniqueVarNames, pathToIdentifier } from "./routeIdentifiers.js";
|
|
27
|
+
|
|
28
|
+
const ROOT_GROUP_KEY = "__root__";
|
|
29
|
+
|
|
30
|
+
const DIRECTORY_CONVENTION_KINDS = [
|
|
31
|
+
"_dependencies.ts",
|
|
32
|
+
"_middlewares.ts",
|
|
33
|
+
"_prefix.ts",
|
|
34
|
+
"_openapi.ts",
|
|
35
|
+
] as const satisfies readonly HttpApiDirectoryCompanionKind[];
|
|
36
|
+
|
|
37
|
+
type DirectoryConventionKind = (typeof DIRECTORY_CONVENTION_KINDS)[number];
|
|
38
|
+
|
|
39
|
+
type DirectoryCompanionPaths = Readonly<Record<DirectoryConventionKind, readonly string[]>>;
|
|
40
|
+
type MutableDirectoryCompanionPaths = Record<DirectoryConventionKind, string[]>;
|
|
41
|
+
|
|
42
|
+
type EndpointCompanionPaths = {
|
|
43
|
+
readonly ".name"?: string;
|
|
44
|
+
readonly ".dependencies"?: string;
|
|
45
|
+
readonly ".middlewares"?: string;
|
|
46
|
+
readonly ".prefix"?: string;
|
|
47
|
+
readonly ".openapi"?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type EndpointRenderSpec = {
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly stem: string;
|
|
53
|
+
readonly groupKey: string;
|
|
54
|
+
readonly modulePath: string;
|
|
55
|
+
readonly companions: EndpointCompanionPaths;
|
|
56
|
+
readonly directoryCompanions: DirectoryCompanionPaths;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type GroupRenderSpec = {
|
|
60
|
+
readonly key: string;
|
|
61
|
+
readonly dirPath: string;
|
|
62
|
+
readonly defaultName: string;
|
|
63
|
+
readonly overridePath?: string;
|
|
64
|
+
readonly directoryCompanions: DirectoryCompanionPaths;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type ApiRenderSpec = {
|
|
68
|
+
readonly defaultIdentifier: string;
|
|
69
|
+
readonly apiRootPath?: string;
|
|
70
|
+
readonly directoryCompanions: DirectoryCompanionPaths;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type DirectoryConventionIndexEntry = {
|
|
74
|
+
readonly apiRootPaths: string[];
|
|
75
|
+
readonly groupOverridePaths: string[];
|
|
76
|
+
readonly companionPaths: MutableDirectoryCompanionPaths;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type EndpointWithGroupKey = {
|
|
80
|
+
readonly node: HttpApiEndpointNode;
|
|
81
|
+
readonly groupKey: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function createMutableDirectoryCompanionPaths(): MutableDirectoryCompanionPaths {
|
|
85
|
+
return {
|
|
86
|
+
"_dependencies.ts": [],
|
|
87
|
+
"_middlewares.ts": [],
|
|
88
|
+
"_prefix.ts": [],
|
|
89
|
+
"_openapi.ts": [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function freezeDirectoryCompanionPaths(
|
|
94
|
+
paths: MutableDirectoryCompanionPaths,
|
|
95
|
+
): DirectoryCompanionPaths {
|
|
96
|
+
return {
|
|
97
|
+
"_dependencies.ts": [...paths["_dependencies.ts"]],
|
|
98
|
+
"_middlewares.ts": [...paths["_middlewares.ts"]],
|
|
99
|
+
"_prefix.ts": [...paths["_prefix.ts"]],
|
|
100
|
+
"_openapi.ts": [...paths["_openapi.ts"]],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createDirectoryConventionIndexEntry(): DirectoryConventionIndexEntry {
|
|
105
|
+
return {
|
|
106
|
+
apiRootPaths: [],
|
|
107
|
+
groupOverridePaths: [],
|
|
108
|
+
companionPaths: createMutableDirectoryCompanionPaths(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeDirPath(dirPath: string): string {
|
|
113
|
+
return dirPath === "." ? "" : dirPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dirnamePosix(path: string): string {
|
|
117
|
+
const index = path.lastIndexOf("/");
|
|
118
|
+
return index < 0 ? "" : path.slice(0, index);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ancestorDirs(dirPath: string): string[] {
|
|
122
|
+
const normalized = normalizeDirPath(dirPath);
|
|
123
|
+
if (normalized === "") return [""];
|
|
124
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
125
|
+
const out: string[] = [""];
|
|
126
|
+
let current = "";
|
|
127
|
+
for (const segment of segments) {
|
|
128
|
+
current = current ? `${current}/${segment}` : segment;
|
|
129
|
+
out.push(current);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pushUnique(values: string[], value: string): void {
|
|
135
|
+
if (!values.includes(value)) values.push(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pushUniqueMany(values: string[], incoming: readonly string[]): void {
|
|
139
|
+
for (const value of incoming) {
|
|
140
|
+
pushUnique(values, value);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sortDirectoryCompanionPaths(paths: MutableDirectoryCompanionPaths): void {
|
|
145
|
+
for (const kind of DIRECTORY_CONVENTION_KINDS) {
|
|
146
|
+
paths[kind].sort(compareHttpApiPathOrder);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function upsertIndexEntry(
|
|
151
|
+
index: Map<string, DirectoryConventionIndexEntry>,
|
|
152
|
+
dirPath: string,
|
|
153
|
+
): DirectoryConventionIndexEntry {
|
|
154
|
+
const normalized = normalizeDirPath(dirPath);
|
|
155
|
+
const existing = index.get(normalized);
|
|
156
|
+
if (existing) return existing;
|
|
157
|
+
const created = createDirectoryConventionIndexEntry();
|
|
158
|
+
index.set(normalized, created);
|
|
159
|
+
return created;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function addConventionToIndex(
|
|
163
|
+
entry: DirectoryConventionIndexEntry,
|
|
164
|
+
convention: DirectoryConventionRef | RootOrGroupConventionRef,
|
|
165
|
+
): void {
|
|
166
|
+
if (convention.kind === "api_root") {
|
|
167
|
+
pushUnique(entry.apiRootPaths, convention.path);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (convention.kind === "group_override") {
|
|
171
|
+
pushUnique(entry.groupOverridePaths, convention.path);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
pushUnique(entry.companionPaths[convention.kind], convention.path);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function indexDirectoryConventions(
|
|
178
|
+
tree: HttpApiDescriptorTree,
|
|
179
|
+
): Map<string, DirectoryConventionIndexEntry> {
|
|
180
|
+
const index = new Map<string, DirectoryConventionIndexEntry>();
|
|
181
|
+
const rootEntry = upsertIndexEntry(index, "");
|
|
182
|
+
for (const convention of tree.conventions) {
|
|
183
|
+
addConventionToIndex(rootEntry, convention);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const visit = (nodes: readonly HttpApiTreeNode[]): void => {
|
|
187
|
+
for (const node of nodes) {
|
|
188
|
+
if (node.type === "endpoint") continue;
|
|
189
|
+
const entry = upsertIndexEntry(index, node.dirPath);
|
|
190
|
+
for (const convention of node.conventions) {
|
|
191
|
+
addConventionToIndex(entry, convention);
|
|
192
|
+
}
|
|
193
|
+
visit(node.children);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
visit(tree.children);
|
|
198
|
+
|
|
199
|
+
for (const entry of index.values()) {
|
|
200
|
+
entry.apiRootPaths.sort(compareHttpApiPathOrder);
|
|
201
|
+
entry.groupOverridePaths.sort(compareHttpApiPathOrder);
|
|
202
|
+
sortDirectoryCompanionPaths(entry.companionPaths);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return index;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectEndpointNodesWithGroupKey(
|
|
209
|
+
nodes: readonly HttpApiTreeNode[],
|
|
210
|
+
currentGroupKey: string,
|
|
211
|
+
): EndpointWithGroupKey[] {
|
|
212
|
+
const out: EndpointWithGroupKey[] = [];
|
|
213
|
+
for (const node of nodes) {
|
|
214
|
+
if (node.type === "endpoint") {
|
|
215
|
+
out.push({ node, groupKey: currentGroupKey });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (node.type === "group") {
|
|
219
|
+
out.push(...collectEndpointNodesWithGroupKey(node.children, node.dirPath));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
out.push(...collectEndpointNodesWithGroupKey(node.children, currentGroupKey));
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function collectGroupNodes(nodes: readonly HttpApiTreeNode[]): HttpApiGroupNode[] {
|
|
228
|
+
const out: HttpApiGroupNode[] = [];
|
|
229
|
+
for (const node of nodes) {
|
|
230
|
+
if (node.type === "group") {
|
|
231
|
+
out.push(node);
|
|
232
|
+
out.push(...collectGroupNodes(node.children));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (node.type === "pathless_directory") {
|
|
236
|
+
out.push(...collectGroupNodes(node.children));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mapEndpointCompanionPaths(node: HttpApiEndpointNode): EndpointCompanionPaths {
|
|
243
|
+
const mapped: Partial<Record<HttpApiEndpointCompanionKind, string>> = {};
|
|
244
|
+
for (const companion of node.companions) {
|
|
245
|
+
if (!mapped[companion.kind]) {
|
|
246
|
+
mapped[companion.kind] = companion.path;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
".name": mapped[".name"],
|
|
251
|
+
".dependencies": mapped[".dependencies"],
|
|
252
|
+
".middlewares": mapped[".middlewares"],
|
|
253
|
+
".prefix": mapped[".prefix"],
|
|
254
|
+
".openapi": mapped[".openapi"],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createDirectoryCompanionPathsForDir(
|
|
259
|
+
dirPath: string,
|
|
260
|
+
index: Map<string, DirectoryConventionIndexEntry>,
|
|
261
|
+
): DirectoryCompanionPaths {
|
|
262
|
+
const merged = createMutableDirectoryCompanionPaths();
|
|
263
|
+
for (const ancestor of ancestorDirs(dirPath)) {
|
|
264
|
+
const entry = index.get(ancestor);
|
|
265
|
+
if (!entry) continue;
|
|
266
|
+
for (const kind of DIRECTORY_CONVENTION_KINDS) {
|
|
267
|
+
pushUniqueMany(merged[kind], entry.companionPaths[kind]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
sortDirectoryCompanionPaths(merged);
|
|
271
|
+
return freezeDirectoryCompanionPaths(merged);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildEndpointRenderSpecs(
|
|
275
|
+
tree: HttpApiDescriptorTree,
|
|
276
|
+
index: Map<string, DirectoryConventionIndexEntry>,
|
|
277
|
+
): EndpointRenderSpec[] {
|
|
278
|
+
const endpointEntries = collectEndpointNodesWithGroupKey(tree.children, ROOT_GROUP_KEY);
|
|
279
|
+
const specs: EndpointRenderSpec[] = [];
|
|
280
|
+
|
|
281
|
+
for (const entry of endpointEntries) {
|
|
282
|
+
const endpointDir = dirnamePosix(entry.node.path);
|
|
283
|
+
specs.push({
|
|
284
|
+
path: entry.node.path,
|
|
285
|
+
stem: entry.node.stem,
|
|
286
|
+
groupKey: entry.groupKey,
|
|
287
|
+
modulePath: entry.node.path,
|
|
288
|
+
companions: mapEndpointCompanionPaths(entry.node),
|
|
289
|
+
directoryCompanions: createDirectoryCompanionPathsForDir(endpointDir, index),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return specs.sort((a, b) => compareHttpApiPathOrder(a.path, b.path));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function compareGroupKeys(a: string, b: string): number {
|
|
297
|
+
const left = a === ROOT_GROUP_KEY ? "" : a;
|
|
298
|
+
const right = b === ROOT_GROUP_KEY ? "" : b;
|
|
299
|
+
return compareHttpApiPathOrder(left, right);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildGroupRenderSpecs(
|
|
303
|
+
tree: HttpApiDescriptorTree,
|
|
304
|
+
index: Map<string, DirectoryConventionIndexEntry>,
|
|
305
|
+
endpoints: readonly EndpointRenderSpec[],
|
|
306
|
+
): GroupRenderSpec[] {
|
|
307
|
+
const byDir = new Map<string, HttpApiGroupNode>();
|
|
308
|
+
for (const groupNode of collectGroupNodes(tree.children)) {
|
|
309
|
+
byDir.set(groupNode.dirPath, groupNode);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const groupKeys = new Set<string>(byDir.keys());
|
|
313
|
+
if (endpoints.some((endpoint) => endpoint.groupKey === ROOT_GROUP_KEY)) {
|
|
314
|
+
groupKeys.add(ROOT_GROUP_KEY);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const specs: GroupRenderSpec[] = [];
|
|
318
|
+
const sortedKeys = [...groupKeys].sort(compareGroupKeys);
|
|
319
|
+
for (const groupKey of sortedKeys) {
|
|
320
|
+
const dirPath = groupKey === ROOT_GROUP_KEY ? "" : groupKey;
|
|
321
|
+
const node = byDir.get(groupKey);
|
|
322
|
+
const defaultName = node?.groupName ?? "root";
|
|
323
|
+
const entry = index.get(dirPath);
|
|
324
|
+
specs.push({
|
|
325
|
+
key: groupKey,
|
|
326
|
+
dirPath,
|
|
327
|
+
defaultName,
|
|
328
|
+
overridePath: entry?.groupOverridePaths[0],
|
|
329
|
+
directoryCompanions: createDirectoryCompanionPathsForDir(dirPath, index),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return specs;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildApiRenderSpec(
|
|
337
|
+
targetDirectory: string,
|
|
338
|
+
index: Map<string, DirectoryConventionIndexEntry>,
|
|
339
|
+
): ApiRenderSpec {
|
|
340
|
+
const rootEntry = index.get("");
|
|
341
|
+
return {
|
|
342
|
+
defaultIdentifier: basename(targetDirectory) || "api",
|
|
343
|
+
apiRootPath: rootEntry?.apiRootPaths[0],
|
|
344
|
+
directoryCompanions: freezeDirectoryCompanionPaths(
|
|
345
|
+
rootEntry ? rootEntry.companionPaths : createMutableDirectoryCompanionPaths(),
|
|
346
|
+
),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function toImportSpecifier(
|
|
351
|
+
importerDir: string,
|
|
352
|
+
targetDir: string,
|
|
353
|
+
relativeFilePath: string,
|
|
354
|
+
): string {
|
|
355
|
+
const absPath = join(targetDir, relativeFilePath);
|
|
356
|
+
const rel = toPosixPath(relative(importerDir, absPath));
|
|
357
|
+
const specifier = rel.startsWith(".") ? rel : `./${rel}`;
|
|
358
|
+
return stripScriptExtension(specifier) + ".js";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const METHOD_FACTORIES: Record<string, string> = {
|
|
362
|
+
GET: "get",
|
|
363
|
+
POST: "post",
|
|
364
|
+
PUT: "put",
|
|
365
|
+
PATCH: "patch",
|
|
366
|
+
DELETE: "delete",
|
|
367
|
+
HEAD: "head",
|
|
368
|
+
OPTIONS: "options",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const OPTIONAL_ENDPOINT_EXPORTS = ["headers", "body", "success", "error"] as const;
|
|
372
|
+
type OptionalExport = (typeof OPTIONAL_ENDPOINT_EXPORTS)[number];
|
|
373
|
+
|
|
374
|
+
/** body maps to payload in HttpApiEndpoint options */
|
|
375
|
+
const EXPORT_TO_OPTION: Record<OptionalExport, string> = {
|
|
376
|
+
headers: "headers",
|
|
377
|
+
body: "payload",
|
|
378
|
+
success: "success",
|
|
379
|
+
error: "error",
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export function emitHttpApiSource(input: {
|
|
383
|
+
readonly tree: HttpApiDescriptorTree;
|
|
384
|
+
readonly targetDirectory: string;
|
|
385
|
+
readonly importer: string;
|
|
386
|
+
readonly extractedLiteralsByPath: ReadonlyMap<
|
|
387
|
+
string,
|
|
388
|
+
{ readonly path: string; readonly method: string; readonly name: string }
|
|
389
|
+
>;
|
|
390
|
+
readonly optionalExportsByPath: ReadonlyMap<string, ReadonlySet<OptionalExport>>;
|
|
391
|
+
/** When true for an endpoint path, handler returns HttpServerResponse; use handleRaw instead of handle. */
|
|
392
|
+
readonly handlerIsRawByPath?: ReadonlyMap<string, boolean>;
|
|
393
|
+
}): string {
|
|
394
|
+
const directoryConventions = indexDirectoryConventions(input.tree);
|
|
395
|
+
const endpointSpecs = buildEndpointRenderSpecs(input.tree, directoryConventions);
|
|
396
|
+
const groupSpecs = buildGroupRenderSpecs(input.tree, directoryConventions, endpointSpecs);
|
|
397
|
+
const apiSpec = buildApiRenderSpec(input.targetDirectory, directoryConventions);
|
|
398
|
+
|
|
399
|
+
const endpointPaths = endpointSpecs.map((e) => e.modulePath);
|
|
400
|
+
const importerDir = dirname(toPosixPath(input.importer));
|
|
401
|
+
const proposedNames = endpointPaths.map((path) => ({
|
|
402
|
+
path,
|
|
403
|
+
proposedName: pathToIdentifier(path),
|
|
404
|
+
}));
|
|
405
|
+
const varNameByPath = makeUniqueVarNames(proposedNames);
|
|
406
|
+
|
|
407
|
+
const importLines: string[] = [
|
|
408
|
+
`import { emptyRecordString, emptyRecordStringArray, composeWithLayers, resolveConfig, type AppConfig, type ComputeLayers, type LayerOrGroup, type RunConfig } from "@typed/app";`,
|
|
409
|
+
`import * as Effect from "effect/Effect";`,
|
|
410
|
+
`import * as Layer from "effect/Layer";`,
|
|
411
|
+
`import * as HttpApi from "effect/unstable/httpapi/HttpApi";`,
|
|
412
|
+
`import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";`,
|
|
413
|
+
`import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";`,
|
|
414
|
+
`import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";`,
|
|
415
|
+
`import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";`,
|
|
416
|
+
`import * as HttpApiScalar from "effect/unstable/httpapi/HttpApiScalar";`,
|
|
417
|
+
`import * as HttpApiSwagger from "effect/unstable/httpapi/HttpApiSwagger";`,
|
|
418
|
+
`import * as HttpServer from "effect/unstable/http/HttpServer";`,
|
|
419
|
+
`import * as HttpRouter from "effect/unstable/http/HttpRouter";`,
|
|
420
|
+
`import * as OpenApiModule from "effect/unstable/httpapi/OpenApi";`,
|
|
421
|
+
`import http from "node:http";`,
|
|
422
|
+
`import { NodeHttpServer } from "@effect/platform-node";`,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
for (const path of endpointPaths) {
|
|
426
|
+
const importSpecifier = toImportSpecifier(importerDir, input.targetDirectory, path);
|
|
427
|
+
importLines.push(
|
|
428
|
+
`import * as ${varNameByPath.get(path)} from ${JSON.stringify(importSpecifier)};`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const apiId = apiSpec.defaultIdentifier;
|
|
433
|
+
|
|
434
|
+
const groupExprs: string[] = [];
|
|
435
|
+
for (const groupSpec of groupSpecs) {
|
|
436
|
+
const endpointsInGroup = endpointSpecs.filter((e) => e.groupKey === groupSpec.key);
|
|
437
|
+
if (endpointsInGroup.length === 0) continue;
|
|
438
|
+
|
|
439
|
+
const endpointExprs: string[] = [];
|
|
440
|
+
for (const ep of endpointsInGroup) {
|
|
441
|
+
const varName = varNameByPath.get(ep.modulePath)!;
|
|
442
|
+
const literals = input.extractedLiteralsByPath.get(ep.path);
|
|
443
|
+
const method = (literals?.method ?? "GET").toUpperCase();
|
|
444
|
+
const name = literals?.name ?? ep.stem;
|
|
445
|
+
const factory = METHOD_FACTORIES[method] ?? "get";
|
|
446
|
+
const m = varName;
|
|
447
|
+
const optionalPresent = input.optionalExportsByPath.get(ep.path) ?? new Set<OptionalExport>();
|
|
448
|
+
const optsParts: string[] = [
|
|
449
|
+
`params: ${m}.route.pathSchema`,
|
|
450
|
+
`query: ${m}.route.querySchema`,
|
|
451
|
+
];
|
|
452
|
+
for (const exp of OPTIONAL_ENDPOINT_EXPORTS) {
|
|
453
|
+
if (optionalPresent.has(exp)) {
|
|
454
|
+
const optName = EXPORT_TO_OPTION[exp];
|
|
455
|
+
optsParts.push(`${optName}: ${m}.${exp}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const opts = optsParts.join(", ");
|
|
459
|
+
endpointExprs.push(
|
|
460
|
+
`HttpApiEndpoint.${factory}(${JSON.stringify(name)}, ${m}.route.path, { ${opts} })`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const groupName = groupSpec.defaultName;
|
|
465
|
+
const groupChain = endpointExprs.map((expr) => `.add(${expr})`).join("");
|
|
466
|
+
groupExprs.push(`HttpApiGroup.make(${JSON.stringify(groupName)})${groupChain}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const apiChain = groupExprs.map((g) => `.add(${g})`).join("");
|
|
470
|
+
const apiExpr = `HttpApi.make(${JSON.stringify(apiId)})${apiChain}`;
|
|
471
|
+
|
|
472
|
+
const groupLayerBlocks: string[] = [];
|
|
473
|
+
for (const groupSpec of groupSpecs) {
|
|
474
|
+
const endpointsInGroup = endpointSpecs.filter((e) => e.groupKey === groupSpec.key);
|
|
475
|
+
if (endpointsInGroup.length === 0) continue;
|
|
476
|
+
const groupName = groupSpec.defaultName;
|
|
477
|
+
const handlerIsRaw = input.handlerIsRawByPath;
|
|
478
|
+
const handleCalls = endpointsInGroup
|
|
479
|
+
.map((e) => {
|
|
480
|
+
const varName = varNameByPath.get(e.modulePath)!;
|
|
481
|
+
const literals = input.extractedLiteralsByPath.get(e.path);
|
|
482
|
+
const name = literals?.name ?? e.stem;
|
|
483
|
+
const isRaw = handlerIsRaw?.get(e.path) === true;
|
|
484
|
+
if (isRaw) {
|
|
485
|
+
return `.handleRaw(${JSON.stringify(name)}, (ctx) => ${varName}.handler(ctx))`;
|
|
486
|
+
}
|
|
487
|
+
const optPresent = input.optionalExportsByPath.get(e.path) ?? new Set<OptionalExport>();
|
|
488
|
+
const headersArg = optPresent.has("headers") ? "ctx.headers" : "emptyRecordString";
|
|
489
|
+
const bodyArg = optPresent.has("body") ? "ctx.payload" : "undefined";
|
|
490
|
+
return `.handle(${JSON.stringify(name)}, (ctx) => ${varName}.handler({ path: ctx.params ?? emptyRecordString, query: ctx.query ?? emptyRecordStringArray, headers: ${headersArg}, body: ${bodyArg} }))`;
|
|
491
|
+
})
|
|
492
|
+
.join("\n ");
|
|
493
|
+
groupLayerBlocks.push(
|
|
494
|
+
`HttpApiBuilder.group(Api, ${JSON.stringify(groupName)}, (handlers) => handlers${handleCalls})`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const baseApiLayer = `HttpApiBuilder.layer(Api)`;
|
|
499
|
+
const mergedApiLayer =
|
|
500
|
+
groupLayerBlocks.length === 0
|
|
501
|
+
? baseApiLayer
|
|
502
|
+
: groupLayerBlocks.reduce(
|
|
503
|
+
(acc, groupBlock) => `${acc}.pipe(Layer.provideMerge(${groupBlock}))`,
|
|
504
|
+
baseApiLayer,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const middlewaresPath = apiSpec.directoryCompanions["_middlewares.ts"][0];
|
|
508
|
+
const hasMiddlewares = Boolean(middlewaresPath);
|
|
509
|
+
if (hasMiddlewares) {
|
|
510
|
+
const middlewareSpecifier = toImportSpecifier(
|
|
511
|
+
importerDir,
|
|
512
|
+
input.targetDirectory,
|
|
513
|
+
middlewaresPath,
|
|
514
|
+
);
|
|
515
|
+
importLines.push(`import * as ApiMiddlewares from ${JSON.stringify(middlewareSpecifier)};`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const serveOptions = hasMiddlewares
|
|
519
|
+
? `{ disableListenLog, middleware: ApiMiddlewares.middleware ?? ApiMiddlewares.default }`
|
|
520
|
+
: `{ disableListenLog }`;
|
|
521
|
+
|
|
522
|
+
return `${importLines.join("\n")}
|
|
523
|
+
|
|
524
|
+
export const Api = ${apiExpr};
|
|
525
|
+
export const ApiLayer = ${mergedApiLayer};
|
|
526
|
+
export const OpenApi = OpenApiModule.fromApi(Api);
|
|
527
|
+
export const Swagger = HttpApiSwagger.layer(Api);
|
|
528
|
+
export const Scalar = HttpApiScalar.layer(Api);
|
|
529
|
+
export const Client = HttpApiClient.make(Api);
|
|
530
|
+
|
|
531
|
+
export const App = <const Layers extends readonly LayerOrGroup[] = []>(
|
|
532
|
+
config?: AppConfig,
|
|
533
|
+
...layersToMergeIntoRouter: Layers
|
|
534
|
+
): Layer.Layer<
|
|
535
|
+
Layer.Success<ComputeLayers<Layers, typeof ApiLayer>>,
|
|
536
|
+
Layer.Error<ComputeLayers<Layers, typeof ApiLayer>>,
|
|
537
|
+
Exclude<Layer.Services<ComputeLayers<Layers, typeof ApiLayer>>, HttpRouter.HttpRouter> | HttpServer.HttpServer
|
|
538
|
+
> => {
|
|
539
|
+
const disableListenLog = config?.disableListenLog ?? false;
|
|
540
|
+
const appLayer = composeWithLayers(ApiLayer, layersToMergeIntoRouter) as ComputeLayers<
|
|
541
|
+
Layers,
|
|
542
|
+
typeof ApiLayer
|
|
543
|
+
>;
|
|
544
|
+
return HttpRouter.serve(appLayer, ${serveOptions})
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
export const serve = <const Layers extends readonly LayerOrGroup[] = []>(
|
|
548
|
+
config?: RunConfig,
|
|
549
|
+
...layersToMergeIntoRouter: Layers
|
|
550
|
+
) =>
|
|
551
|
+
Layer.unwrap(
|
|
552
|
+
Effect.gen(function* () {
|
|
553
|
+
const host = yield* resolveConfig(config?.host, "0.0.0.0");
|
|
554
|
+
const port = yield* resolveConfig(config?.port, 3000);
|
|
555
|
+
const disableListenLog = yield* resolveConfig(config?.disableListenLog, false);
|
|
556
|
+
const appConfig: AppConfig = { disableListenLog };
|
|
557
|
+
const appLayer = App(appConfig, ...layersToMergeIntoRouter);
|
|
558
|
+
const serverLayer = NodeHttpServer.layer(http.createServer, { host, port });
|
|
559
|
+
return appLayer.pipe(Layer.provide(serverLayer));
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { CatchForm, DepsExportKind, RuntimeKind } from "./routeTypeNode.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emit the handler expression that converts to a function returning Fx.
|
|
5
|
+
* Router passes RefSubject<Params> (an Fx) to function handlers.
|
|
6
|
+
* Plain sync handlers (value in, value out) always use Fx.map(params, handler).
|
|
7
|
+
*/
|
|
8
|
+
export function handlerExprFor(
|
|
9
|
+
runtimeKind: RuntimeKind,
|
|
10
|
+
isFn: boolean,
|
|
11
|
+
expectsRefSubject: boolean,
|
|
12
|
+
varName: string,
|
|
13
|
+
exportName: string,
|
|
14
|
+
): string {
|
|
15
|
+
const ref = `${varName}.${exportName}`;
|
|
16
|
+
if (runtimeKind === "plain") {
|
|
17
|
+
return isFn ? `(params) => Fx.map(params, ${ref})` : `constant(Fx.succeed(${ref}))`;
|
|
18
|
+
}
|
|
19
|
+
if (isFn && expectsRefSubject) {
|
|
20
|
+
return `(params) => ${ref}(params)`;
|
|
21
|
+
}
|
|
22
|
+
switch (runtimeKind) {
|
|
23
|
+
case "effect":
|
|
24
|
+
return isFn ? `(params) => Fx.mapEffect(params, ${ref})` : `constant(Fx.fromEffect(${ref}))`;
|
|
25
|
+
case "stream":
|
|
26
|
+
return isFn
|
|
27
|
+
? `(params) => Fx.switchMap(params, (p) => Fx.fromStream(${ref}(p)))`
|
|
28
|
+
: `constant(Fx.fromStream(${ref}))`;
|
|
29
|
+
case "fx":
|
|
30
|
+
return isFn ? ref : `constant(${ref})`;
|
|
31
|
+
case "unknown":
|
|
32
|
+
throw new Error("RVM-KIND-001: runtime kind unknown (should have been caught in buildRouteDescriptors)");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Lift a value or function result to Fx based on return kind (plain, effect, stream, fx). */
|
|
37
|
+
export function liftToFx(expr: string, kind: RuntimeKind): string {
|
|
38
|
+
switch (kind) {
|
|
39
|
+
case "plain":
|
|
40
|
+
return `Fx.succeed(${expr})`;
|
|
41
|
+
case "effect":
|
|
42
|
+
return `Fx.fromEffect(${expr})`;
|
|
43
|
+
case "stream":
|
|
44
|
+
return `Fx.fromStream(${expr})`;
|
|
45
|
+
case "fx":
|
|
46
|
+
return expr;
|
|
47
|
+
case "unknown":
|
|
48
|
+
throw new Error("RVM-KIND-001: runtime kind unknown (should have been caught in buildRouteDescriptors)");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Emit the catch expression that converts to (causeRef) => Fx form.
|
|
54
|
+
* Supports: value fallbacks, (Cause) => ..., (E) => ..., and native (causeRef) => Fx.
|
|
55
|
+
*/
|
|
56
|
+
export function catchExprFor(catchForm: CatchForm, varName: string, exportName: string): string {
|
|
57
|
+
const ref = `${varName}.${exportName}`;
|
|
58
|
+
const { form, returnKind } = catchForm;
|
|
59
|
+
|
|
60
|
+
if (form === "native") {
|
|
61
|
+
return ref;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (form === "value") {
|
|
65
|
+
const lifted = liftToFx(ref, returnKind);
|
|
66
|
+
return `(_causeRef) => ${lifted}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (form === "fn-cause") {
|
|
70
|
+
const lifted = liftToFx(`${ref}(cause)`, returnKind);
|
|
71
|
+
return `(causeRef) => Fx.flatMap(causeRef, (cause) => ${lifted})`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// form === "fn-error": (e) => A | Effect | Stream | Fx — use Cause.findFail + Result.match
|
|
75
|
+
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)} }))`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Targeted lift for .provide() based on dependency export kind (layer, servicemap, array). */
|
|
79
|
+
export function depsExprFor(kind: DepsExportKind, varName: string): string {
|
|
80
|
+
const ref = `${varName}.default`;
|
|
81
|
+
switch (kind) {
|
|
82
|
+
case "layer":
|
|
83
|
+
return ref;
|
|
84
|
+
case "servicemap":
|
|
85
|
+
return `Layer.succeedServices(${ref})`;
|
|
86
|
+
case "array":
|
|
87
|
+
return `Router.normalizeDependencyInput(${ref})`;
|
|
88
|
+
}
|
|
89
|
+
}
|