@boon4681/giri 0.0.1 → 0.0.2-alpha-2

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/dist/index.js CHANGED
@@ -44,1506 +44,1588 @@ var init_es5 = __esm({
44
44
  }
45
45
  });
46
46
 
47
- // src/index.ts
48
- var index_exports = {};
49
- __export(index_exports, {
50
- buildGiriApp: () => buildGiriApp,
51
- composeMiddleware: () => composeMiddleware,
52
- createContext: () => createContext,
53
- createTypedResponse: () => createTypedResponse,
54
- defineBodySchema: () => defineBodySchema,
55
- defineConfig: () => defineConfig,
56
- defineInputSchema: () => defineInputSchema,
57
- defineMiddleware: () => defineMiddleware,
58
- isGiriBodySchema: () => isGiriBodySchema,
59
- isGiriInputSchema: () => isGiriInputSchema,
60
- isTypedResponse: () => isTypedResponse,
61
- loadLifecycle: () => loadLifecycle,
62
- prepareRequestInput: () => prepareRequestInput,
63
- resolveGiriPaths: () => resolveGiriPaths,
64
- runInit: () => runInit,
65
- scanRoutes: () => scanRoutes,
66
- stack: () => stack,
67
- syncProject: () => syncProject,
68
- toResponse: () => toResponse,
69
- typedResponseToResponse: () => typedResponseToResponse
70
- });
71
- module.exports = __toCommonJS(index_exports);
72
-
73
- // src/types.ts
74
- var typedResponseBrand = /* @__PURE__ */ Symbol.for("giri.typed-response");
75
- var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
76
- var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
77
-
78
- // src/context.ts
79
- var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
80
- function createTypedResponse(data, status, format, headers) {
81
- return {
82
- [typedResponseBrand]: { data, status, format },
83
- data,
84
- status,
85
- format,
86
- headers
87
- };
88
- }
89
- function isTypedResponse(value) {
90
- return Boolean(value && typeof value === "object" && typedResponseBrand in value);
91
- }
92
- function createContext(options) {
93
- const url = new URL(options.request.url);
94
- const store = /* @__PURE__ */ new Map();
95
- const validated = options.validated ?? {};
96
- return {
97
- params: options.params ?? {},
98
- app: options.app ?? {},
99
- req: {
100
- raw: options.request,
101
- url,
102
- method: options.request.method,
103
- header: (name) => options.request.headers.get(name),
104
- json: () => options.request.json(),
105
- text: () => options.request.text(),
106
- arrayBuffer: () => options.request.arrayBuffer(),
107
- formData: () => options.request.formData(),
108
- valid: (key) => {
109
- if (!(key in validated)) {
110
- throw new Error(`No validated ${String(key)} data is available for this route.`);
111
- }
112
- return validated[key];
47
+ // src/generator/schema/program.ts
48
+ function createSchemaProgram(paths, routeFiles) {
49
+ let options = { ...DEFAULT_OPTIONS };
50
+ const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
51
+ if (configPath) {
52
+ const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
53
+ ...import_typescript.default.sys,
54
+ onUnRecoverableConfigFileDiagnostic: () => {
113
55
  }
114
- },
115
- set: (key, value) => {
116
- store.set(key, value);
117
- },
118
- get: (key) => store.get(key),
119
- json: (data, status = 200, headers) => createTypedResponse(data, status, "json", headers),
120
- text: (text, status = 200, headers) => createTypedResponse(text, status, "text", headers)
121
- };
122
- }
123
- function typedResponseToResponse(response) {
124
- const headers = new Headers(response.headers);
125
- if (response.format === "json" && !headers.has("content-type")) {
126
- headers.set("content-type", "application/json; charset=utf-8");
127
- }
128
- if (response.format === "text" && !headers.has("content-type")) {
129
- headers.set("content-type", "text/plain; charset=utf-8");
130
- }
131
- const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
132
- return new Response(body, {
133
- status: response.status,
134
- headers
135
- });
136
- }
137
- function toResponse(response) {
138
- return isTypedResponse(response) ? typedResponseToResponse(response) : response;
139
- }
140
- async function composeMiddleware(middleware, handle, context) {
141
- let index = -1;
142
- let result;
143
- const dispatch = async (i) => {
144
- if (i <= index) {
145
- throw new Error("next() called multiple times in giri middleware.");
146
- }
147
- index = i;
148
- if (i === middleware.length) {
149
- result = await handle(context);
150
- return result;
151
- }
152
- const returned = await middleware[i](context, () => dispatch(i + 1));
153
- if (returned !== void 0) {
154
- result = returned;
155
- return returned;
56
+ });
57
+ if (parsed) {
58
+ options = { ...parsed.options, noEmit: true };
156
59
  }
157
- return result;
158
- };
159
- await dispatch(0);
160
- if (result === void 0) {
161
- throw new Error("Route completed without returning a response.");
162
60
  }
163
- return result;
61
+ return import_typescript.default.createProgram(routeFiles, options);
164
62
  }
165
- function defineMiddleware(optionsOrMiddleware, maybeMiddleware) {
166
- if (typeof optionsOrMiddleware === "function") {
167
- return optionsOrMiddleware;
168
- }
169
- if (!maybeMiddleware) {
170
- throw new Error("defineMiddleware(options, middleware) requires a middleware function.");
63
+ var import_typescript, DEFAULT_OPTIONS;
64
+ var init_program = __esm({
65
+ "src/generator/schema/program.ts"() {
66
+ "use strict";
67
+ import_typescript = __toESM(require("typescript"));
68
+ DEFAULT_OPTIONS = {
69
+ target: import_typescript.default.ScriptTarget.ES2022,
70
+ module: import_typescript.default.ModuleKind.NodeNext,
71
+ moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
72
+ strict: true,
73
+ skipLibCheck: true,
74
+ noEmit: true
75
+ };
171
76
  }
172
- maybeMiddleware.openapi = optionsOrMiddleware.openapi;
173
- return maybeMiddleware;
174
- }
175
- function stack(...middleware) {
176
- return middleware;
177
- }
77
+ });
178
78
 
179
- // src/validation.ts
180
- function defineInputSchema(schema) {
181
- return { [inputSchemaBrand]: true, ...schema };
182
- }
183
- function isGiriInputSchema(value) {
184
- return Boolean(
185
- value && typeof value === "object" && value[inputSchemaBrand] === true
186
- );
79
+ // src/generator/schema/json-schema.ts
80
+ function createWalkContext(checker, location) {
81
+ return {
82
+ checker,
83
+ location,
84
+ defs: {},
85
+ inProgress: /* @__PURE__ */ new Map(),
86
+ usedDefs: /* @__PURE__ */ new Set(),
87
+ warnings: []
88
+ };
187
89
  }
188
- function defineBodySchema(contents) {
189
- return { [bodySchemaBrand]: true, contents };
90
+ function typeId(type) {
91
+ return type.id;
190
92
  }
191
- function isGiriBodySchema(value) {
192
- return Boolean(
193
- value && typeof value === "object" && value[bodySchemaBrand] === true
194
- );
93
+ function intrinsicName(type) {
94
+ return type.intrinsicName;
195
95
  }
196
- var MIME_TO_CONTENT_TYPE = {
197
- "application/json": "json",
198
- "multipart/form-data": "form",
199
- "application/x-www-form-urlencoded": "urlencoded",
200
- "text/plain": "text"
201
- };
202
- function contentTypeFromHeader(header) {
203
- if (!header) {
204
- return void 0;
205
- }
206
- const mime = header.split(";", 1)[0].trim().toLowerCase();
207
- return MIME_TO_CONTENT_TYPE[mime];
96
+ function isDateType(type) {
97
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
98
+ return symbol?.getName() === "Date";
208
99
  }
209
- function formDataObject(form) {
210
- const result = {};
211
- form.forEach((value, key) => {
212
- const current = result[key];
213
- if (current === void 0) {
214
- result[key] = value;
215
- } else if (Array.isArray(current)) {
216
- current.push(value);
100
+ function literalValuesOf(types) {
101
+ const values = [];
102
+ for (const member of types) {
103
+ if (member.isStringLiteral() || member.isNumberLiteral()) {
104
+ values.push(member.value);
105
+ } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
106
+ values.push(intrinsicName(member) === "true");
217
107
  } else {
218
- result[key] = [current, value];
108
+ return void 0;
219
109
  }
220
- });
221
- return result;
110
+ }
111
+ return values;
222
112
  }
223
- async function readRawBody(request, contentType) {
224
- const cloned = request.clone();
225
- if (contentType === "json") {
226
- return cloned.json();
113
+ function walkUnion(type, ctx) {
114
+ const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
115
+ const members = type.types.filter((member) => !(member.flags & flag));
116
+ if (members.length === 1) {
117
+ return walkType(members[0], ctx);
227
118
  }
228
- if (contentType === "text") {
229
- return cloned.text();
119
+ const enumValues = literalValuesOf(members);
120
+ if (enumValues) {
121
+ return { enum: enumValues };
230
122
  }
231
- return formDataObject(await cloned.formData());
123
+ return { anyOf: members.map((member) => walkType(member, ctx)) };
232
124
  }
233
- function queryObject(url) {
234
- const result = {};
235
- for (const [key, value] of url.searchParams) {
236
- const current = result[key];
237
- if (current === void 0) {
238
- result[key] = value;
239
- } else if (Array.isArray(current)) {
240
- current.push(value);
241
- } else {
242
- result[key] = [current, value];
125
+ function buildObjectSchema(type, ctx) {
126
+ const { checker } = ctx;
127
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
128
+ const properties = {};
129
+ const required = [];
130
+ for (const symbol of checker.getPropertiesOfType(type)) {
131
+ const name = symbol.getName();
132
+ const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
133
+ const optional = Boolean(symbol.getFlags() & import_typescript2.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript2.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript2.default.TypeFlags.Undefined));
134
+ properties[name] = walkType(propType, ctx);
135
+ if (!optional) {
136
+ required.push(name);
243
137
  }
244
138
  }
245
- return result;
246
- }
247
- async function runValidation(schema, value, label) {
248
- if (!isGiriInputSchema(schema)) {
249
- throw new Error(
250
- `giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
251
- );
139
+ const schema = { type: "object" };
140
+ if (Object.keys(properties).length > 0) {
141
+ schema.properties = properties;
252
142
  }
253
- return schema.validate(value);
254
- }
255
- async function prepareRequestInput(request, input) {
256
- const validated = {};
257
- if (input?.query) {
258
- const query = queryObject(new URL(request.url));
259
- const result = await runValidation(input.query, query, "query");
260
- if (!result.ok) {
261
- return {
262
- ok: false,
263
- response: createTypedResponse(
264
- { message: "Invalid query parameters.", issues: result.issues },
265
- 400,
266
- "json"
267
- )
268
- };
269
- }
270
- validated.query = result.value;
143
+ if (required.length > 0) {
144
+ schema.required = required;
271
145
  }
272
- if (input?.body) {
273
- const contents = input.body.contents;
274
- const declared = Object.keys(contents);
275
- const requested = contentTypeFromHeader(request.headers.get("content-type"));
276
- const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
277
- if (!chosen) {
278
- return {
279
- ok: false,
280
- response: createTypedResponse(
281
- { message: "Unsupported media type.", issues: { accepted: declared } },
282
- 415,
283
- "json"
284
- )
285
- };
286
- }
287
- let rawBody;
288
- try {
289
- rawBody = await readRawBody(request, chosen);
290
- } catch (error) {
291
- return {
292
- ok: false,
293
- response: createTypedResponse(
294
- { message: "Invalid request body.", issues: error },
295
- 400,
296
- "json"
297
- )
298
- };
299
- }
300
- const result = await runValidation(contents[chosen], rawBody, "body");
301
- if (!result.ok) {
302
- return {
303
- ok: false,
304
- response: createTypedResponse(
305
- { message: "Invalid request body.", issues: result.issues },
306
- 400,
307
- "json"
308
- )
309
- };
310
- }
311
- validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
146
+ if (indexInfo) {
147
+ schema.additionalProperties = walkType(indexInfo.type, ctx);
148
+ } else if (Object.keys(properties).length > 0) {
149
+ schema.additionalProperties = false;
312
150
  }
313
- return { ok: true, validated };
151
+ return schema;
314
152
  }
315
-
316
- // src/app.ts
317
- var import_node_module = __toESM(require("module"));
318
- var import_node_path3 = require("path");
319
-
320
- // src/loader/loader.ts
321
- var import_prompts = require("@clack/prompts");
322
- var import_node_fs = require("fs");
323
- var import_node_path = require("path");
324
- var import_node_process = require("process");
325
-
326
- // src/config/schema.ts
327
- var import_typebox = require("@sinclair/typebox");
328
- var configSchema = import_typebox.Type.Object({
329
- adapter: import_typebox.Type.Any(),
330
- alias: import_typebox.Type.Optional(import_typebox.Type.Record(
331
- import_typebox.Type.String(),
332
- import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
333
- )),
334
- outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
335
- server: import_typebox.Type.Optional(import_typebox.Type.Object({
336
- port: import_typebox.Type.Optional(import_typebox.Type.Number()),
337
- hostname: import_typebox.Type.Optional(import_typebox.Type.String())
338
- }, { additionalProperties: false })),
339
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
340
- }, { additionalProperties: false });
341
-
342
- // src/loader/loader.ts
343
- var import_value = require("@sinclair/typebox/value");
344
- var assertES5 = async (unregister) => {
345
- try {
346
- init_es5();
347
- } catch (e) {
348
- if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
349
- const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
350
- if (es5Error) {
351
- import_prompts.log.error(
352
- `Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
353
- );
354
- (0, import_node_process.exit)(1);
355
- }
356
- }
357
- import_prompts.log.error(e);
358
- (0, import_node_process.exit)(1);
359
- }
360
- };
361
- var safeRegister = async () => {
362
- const { register } = await import("esbuild-register/dist/node");
363
- let res;
364
- try {
365
- res = register({
366
- format: "cjs",
367
- loader: "ts"
368
- });
369
- } catch {
370
- res = {
371
- unregister: () => {
372
- }
373
- };
153
+ function defName(type) {
154
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
155
+ const name = symbol?.getName();
156
+ if (name && name !== "__type" && name !== "__object") {
157
+ return name;
374
158
  }
375
- await assertES5(res.unregister);
376
- return res;
377
- };
378
-
379
- // src/routes.ts
380
- var import_node_fs2 = require("fs");
381
- var import_promises = require("fs/promises");
382
- var import_node_path2 = require("path");
383
- var import_tinyglobby = require("tinyglobby");
384
- var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
385
- var METHOD_FROM_FILE = new Map(
386
- METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
387
- );
388
- function normalizeSlashes(path) {
389
- return path.split(import_node_path2.sep).join("/");
390
- }
391
- function isRouteSourceFile(fileName) {
392
- return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
159
+ return `Anonymous${typeId(type)}`;
393
160
  }
394
- function methodFromFile(fileName) {
395
- if (!isRouteSourceFile(fileName)) {
396
- return void 0;
161
+ function walkObject(type, ctx) {
162
+ const { checker } = ctx;
163
+ if (isDateType(type)) {
164
+ return { type: "string", format: "date-time" };
397
165
  }
398
- const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
399
- return METHOD_FROM_FILE.get(stem);
400
- }
401
- function sharedFileIn(dir) {
402
- for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
403
- const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
404
- if ((0, import_node_fs2.existsSync)(file)) {
405
- return file;
406
- }
166
+ if (checker.isArrayType(type)) {
167
+ const [element] = checker.getTypeArguments(type);
168
+ return { type: "array", items: element ? walkType(element, ctx) : {} };
407
169
  }
408
- return void 0;
409
- }
410
- function physicalRouteSegments(routesDir, routeDir) {
411
- const rel = (0, import_node_path2.relative)(routesDir, routeDir);
412
- if (!rel) {
413
- return [];
170
+ if (checker.isTupleType(type)) {
171
+ const elements = checker.getTypeArguments(type);
172
+ return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
414
173
  }
415
- return normalizeSlashes(rel).split("/").filter(Boolean);
174
+ const id = typeId(type);
175
+ const existing = ctx.inProgress.get(id);
176
+ if (existing) {
177
+ ctx.usedDefs.add(existing);
178
+ return { $ref: `#/$defs/${existing}` };
179
+ }
180
+ const name = defName(type);
181
+ ctx.inProgress.set(id, name);
182
+ const schema = buildObjectSchema(type, ctx);
183
+ ctx.inProgress.delete(id);
184
+ if (ctx.usedDefs.has(name)) {
185
+ ctx.defs[name] = schema;
186
+ return { $ref: `#/$defs/${name}` };
187
+ }
188
+ return schema;
416
189
  }
417
- function urlSegment(segment) {
418
- if (/^\(.+\)$/.test(segment)) {
190
+ function walkType(type, ctx) {
191
+ const flags = type.flags;
192
+ if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
419
193
  return {};
420
194
  }
421
- const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
422
- if (catchAll) {
423
- const name = catchAll[1];
424
- return {
425
- value: `:${name}{.*}`,
426
- param: { name, catchAll: true }
427
- };
195
+ if (flags & import_typescript2.default.TypeFlags.Null) {
196
+ return { type: "null" };
428
197
  }
429
- const param = /^\[(.+)\]$/.exec(segment);
430
- if (param) {
431
- const name = param[1];
432
- return {
433
- value: `:${name}`,
434
- param: { name, catchAll: false }
435
- };
198
+ if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
199
+ return {};
436
200
  }
437
- return { value: segment };
201
+ if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
202
+ ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
203
+ return { type: "string" };
204
+ }
205
+ if (type.isStringLiteral()) {
206
+ return { type: "string", const: type.value };
207
+ }
208
+ if (type.isNumberLiteral()) {
209
+ return { type: "number", const: type.value };
210
+ }
211
+ if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
212
+ return { type: "boolean", const: intrinsicName(type) === "true" };
213
+ }
214
+ if (flags & import_typescript2.default.TypeFlags.String) {
215
+ return { type: "string" };
216
+ }
217
+ if (flags & import_typescript2.default.TypeFlags.Number) {
218
+ return { type: "number" };
219
+ }
220
+ if (flags & import_typescript2.default.TypeFlags.Boolean) {
221
+ return { type: "boolean" };
222
+ }
223
+ if (type.isUnion()) {
224
+ return walkUnion(type, ctx);
225
+ }
226
+ if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
227
+ return walkObject(type, ctx);
228
+ }
229
+ return {};
438
230
  }
439
- function pathFromSegments(segments) {
440
- const pathSegments = [];
441
- const params = [];
442
- for (const segment of segments) {
443
- const converted = urlSegment(segment);
444
- if (converted.value) {
445
- pathSegments.push(converted.value);
231
+ var import_typescript2;
232
+ var init_json_schema = __esm({
233
+ "src/generator/schema/json-schema.ts"() {
234
+ "use strict";
235
+ import_typescript2 = __toESM(require("typescript"));
236
+ }
237
+ });
238
+
239
+ // src/generator/schema/responses.ts
240
+ function findHandleFunction(source) {
241
+ let found;
242
+ const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
243
+ for (const statement of source.statements) {
244
+ if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
245
+ found = statement;
446
246
  }
447
- if (converted.param) {
448
- params.push(converted.param);
247
+ if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
248
+ for (const declaration of statement.declarationList.declarations) {
249
+ if (import_typescript3.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript3.default.isArrowFunction(declaration.initializer) || import_typescript3.default.isFunctionExpression(declaration.initializer))) {
250
+ found = declaration.initializer;
251
+ }
252
+ }
449
253
  }
450
254
  }
451
- return {
452
- path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
453
- params
454
- };
255
+ return found;
455
256
  }
456
- async function scanRouteFolders(routesDir) {
457
- if (!(0, import_node_fs2.existsSync)(routesDir)) {
257
+ function collectReturnExpressions(fn) {
258
+ if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
259
+ return [fn.body];
260
+ }
261
+ if (!fn.body) {
458
262
  return [];
459
263
  }
460
- const folders = [routesDir];
461
- const walk = async (dir) => {
462
- for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
463
- if (entry.isDirectory() && entry.name !== "node_modules") {
464
- const full = (0, import_node_path2.join)(dir, entry.name);
465
- folders.push(full);
466
- await walk(full);
467
- }
264
+ const expressions = [];
265
+ const visit = (node) => {
266
+ if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
267
+ return;
268
+ }
269
+ if (import_typescript3.default.isReturnStatement(node) && node.expression) {
270
+ expressions.push(node.expression);
468
271
  }
272
+ import_typescript3.default.forEachChild(node, visit);
469
273
  };
470
- await walk(routesDir);
471
- return folders;
274
+ import_typescript3.default.forEachChild(fn.body, visit);
275
+ return expressions;
472
276
  }
473
- function routeParamsForDir(routesDir, dir) {
474
- return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
277
+ function propertyType(checker, type, name, location) {
278
+ const symbol = checker.getPropertyOfType(type, name);
279
+ return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
475
280
  }
476
- function sharedFilesForDir(routesDir, dir) {
477
- const segments = physicalRouteSegments(routesDir, dir);
478
- const dirs = [routesDir];
479
- let current = routesDir;
480
- for (const segment of segments) {
481
- current = (0, import_node_path2.join)(current, segment);
482
- dirs.push(current);
281
+ function isTypedResponse2(checker, type) {
282
+ return Boolean(
283
+ checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
284
+ );
285
+ }
286
+ function readFromCall(checker, expression) {
287
+ if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
288
+ return void 0;
483
289
  }
484
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
290
+ const method = expression.expression.name.text;
291
+ if (method !== "json" && method !== "text") {
292
+ return void 0;
293
+ }
294
+ if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
295
+ return void 0;
296
+ }
297
+ const [dataArg, statusArg] = expression.arguments;
298
+ if (!dataArg) {
299
+ return void 0;
300
+ }
301
+ let status = 200;
302
+ if (statusArg) {
303
+ const statusType = checker.getTypeAtLocation(statusArg);
304
+ status = statusType.isNumberLiteral() ? statusType.value : "default";
305
+ }
306
+ return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
485
307
  }
486
- async function scanRoutes(routesDir) {
487
- if (!(0, import_node_fs2.existsSync)(routesDir)) {
488
- return [];
308
+ function readFromType(checker, type, location) {
309
+ const dataType = propertyType(checker, type, "data", location);
310
+ const statusType = propertyType(checker, type, "status", location);
311
+ const formatType = propertyType(checker, type, "format", location);
312
+ if (!dataType || !statusType || !formatType) {
313
+ return void 0;
489
314
  }
490
- const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
491
- cwd: routesDir,
492
- absolute: true,
493
- onlyFiles: true
494
- });
495
- const routes = [];
496
- for (const file of files) {
497
- const method = methodFromFile((0, import_node_path2.basename)(file));
498
- if (!method) {
315
+ const status = statusType.isNumberLiteral() ? statusType.value : "default";
316
+ const format = formatType.isStringLiteral() && formatType.value === "text" ? "text" : "json";
317
+ return { status, format, data: dataType };
318
+ }
319
+ function constituents(type) {
320
+ return type.isUnion() ? type.types : [type];
321
+ }
322
+ function extractRouteResponses(program, file) {
323
+ const result = { responses: [], opaque: false, warnings: [], $defs: {} };
324
+ const source = program.getSourceFile(file);
325
+ if (!source) {
326
+ return result;
327
+ }
328
+ const checker = program.getTypeChecker();
329
+ const fn = findHandleFunction(source);
330
+ if (!fn) {
331
+ return result;
332
+ }
333
+ const ctx = createWalkContext(checker, fn);
334
+ const byStatus = /* @__PURE__ */ new Map();
335
+ const record = (hit) => {
336
+ const schema = walkType(hit.data, ctx);
337
+ const bucket = byStatus.get(hit.status) ?? { format: hit.format, schemas: [] };
338
+ bucket.schemas.push(schema);
339
+ byStatus.set(hit.status, bucket);
340
+ };
341
+ for (const expression of collectReturnExpressions(fn)) {
342
+ const fromCall = readFromCall(checker, expression);
343
+ if (fromCall) {
344
+ record(fromCall);
499
345
  continue;
500
346
  }
501
- const routeDir = (0, import_node_path2.dirname)(file);
502
- const routeSegments = physicalRouteSegments(routesDir, routeDir);
503
- const { path, params } = pathFromSegments(routeSegments);
504
- routes.push({
505
- method,
506
- path,
507
- file,
508
- routeDir,
509
- routeSegments,
510
- params,
511
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
512
- });
513
- }
514
- return routes.sort((left, right) => {
515
- const pathOrder = left.path.localeCompare(right.path);
516
- if (pathOrder !== 0) {
517
- return pathOrder;
347
+ let matched = false;
348
+ for (const member of constituents(checker.getTypeAtLocation(expression))) {
349
+ const hit = readFromType(checker, member, expression);
350
+ if (hit) {
351
+ record(hit);
352
+ matched = true;
353
+ }
354
+ }
355
+ if (!matched) {
356
+ result.opaque = true;
518
357
  }
519
- return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
520
- });
521
- }
522
-
523
- // src/app.ts
524
- function loadModule(file) {
525
- const resolved = require.resolve(file);
526
- delete require.cache[resolved];
527
- return require(resolved);
528
- }
529
- function interopDefault(value) {
530
- if (value && typeof value === "object" && "default" in value) {
531
- return value.default;
532
358
  }
533
- return value;
359
+ for (const [status, { format, schemas }] of byStatus) {
360
+ const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
361
+ result.responses.push({ status, format, schema });
362
+ }
363
+ result.responses.sort((a, b) => Number(a.status) - Number(b.status));
364
+ result.warnings = ctx.warnings;
365
+ result.$defs = ctx.defs;
366
+ return result;
534
367
  }
535
- function normalizeMiddleware(value, file) {
536
- const exported = interopDefault(value);
537
- if (exported === void 0) {
538
- return [];
368
+ var import_typescript3;
369
+ var init_responses = __esm({
370
+ "src/generator/schema/responses.ts"() {
371
+ "use strict";
372
+ import_typescript3 = __toESM(require("typescript"));
373
+ init_json_schema();
539
374
  }
540
- if (typeof exported === "function") {
541
- return [exported];
375
+ });
376
+
377
+ // src/generator/schema/index.ts
378
+ var schema_exports = {};
379
+ __export(schema_exports, {
380
+ createSchemaProgram: () => createSchemaProgram,
381
+ createWalkContext: () => createWalkContext,
382
+ extractRouteResponses: () => extractRouteResponses,
383
+ walkType: () => walkType
384
+ });
385
+ var init_schema = __esm({
386
+ "src/generator/schema/index.ts"() {
387
+ "use strict";
388
+ init_program();
389
+ init_responses();
390
+ init_json_schema();
542
391
  }
543
- if (Array.isArray(exported)) {
544
- for (const middleware of exported) {
545
- if (typeof middleware !== "function") {
546
- throw new Error(`Middleware export in ${file} must contain only functions.`);
392
+ });
393
+
394
+ // src/index.ts
395
+ var index_exports = {};
396
+ __export(index_exports, {
397
+ buildGiriApp: () => buildGiriApp,
398
+ composeMiddleware: () => composeMiddleware,
399
+ createContext: () => createContext,
400
+ createTypedResponse: () => createTypedResponse,
401
+ defineBodySchema: () => defineBodySchema,
402
+ defineConfig: () => defineConfig,
403
+ defineInputSchema: () => defineInputSchema,
404
+ defineMiddleware: () => defineMiddleware,
405
+ isGiriBodySchema: () => isGiriBodySchema,
406
+ isGiriInputSchema: () => isGiriInputSchema,
407
+ isTypedResponse: () => isTypedResponse,
408
+ loadLifecycle: () => loadLifecycle,
409
+ prepareRequestInput: () => prepareRequestInput,
410
+ resolveGiriPaths: () => resolveGiriPaths,
411
+ runInit: () => runInit,
412
+ scanRoutes: () => scanRoutes,
413
+ stack: () => stack,
414
+ syncProject: () => syncProject,
415
+ toResponse: () => toResponse,
416
+ typedResponseToResponse: () => typedResponseToResponse
417
+ });
418
+ module.exports = __toCommonJS(index_exports);
419
+
420
+ // src/types.ts
421
+ var typedResponseBrand = /* @__PURE__ */ Symbol.for("giri.typed-response");
422
+ var nativeContextBrand = /* @__PURE__ */ Symbol.for("giri.native-context");
423
+ var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
424
+ var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
425
+
426
+ // src/context.ts
427
+ var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
428
+ var pendingResponseBrand = /* @__PURE__ */ Symbol("giri.pending-response");
429
+ function getPending(context) {
430
+ return context[pendingResponseBrand];
431
+ }
432
+ function createTypedResponse(data, status, format, headers) {
433
+ return {
434
+ [typedResponseBrand]: { data, status, format },
435
+ data,
436
+ status,
437
+ format,
438
+ headers
439
+ };
440
+ }
441
+ function isTypedResponse(value) {
442
+ return Boolean(value && typeof value === "object" && typedResponseBrand in value);
443
+ }
444
+ function createContext(options) {
445
+ const url = new URL(options.request.url);
446
+ const store = /* @__PURE__ */ new Map();
447
+ const validated = options.validated ?? {};
448
+ const pending = { headers: new Headers() };
449
+ const defaultStatus = () => pending.status ?? 200;
450
+ const context = {
451
+ params: options.params ?? {},
452
+ app: options.app ?? {},
453
+ req: {
454
+ raw: options.request,
455
+ url,
456
+ method: options.request.method,
457
+ header: (name) => options.request.headers.get(name),
458
+ json: () => options.request.json(),
459
+ text: () => options.request.text(),
460
+ arrayBuffer: () => options.request.arrayBuffer(),
461
+ formData: () => options.request.formData(),
462
+ valid: (key) => {
463
+ if (!(key in validated)) {
464
+ throw new Error(`No validated ${String(key)} data is available for this route.`);
465
+ }
466
+ return validated[key];
467
+ }
468
+ },
469
+ set: (key, value) => {
470
+ store.set(key, value);
471
+ },
472
+ get: (key) => store.get(key),
473
+ json: (data, status, headers) => createTypedResponse(data, status ?? defaultStatus(), "json", headers),
474
+ text: (text, status, headers) => createTypedResponse(text, status ?? defaultStatus(), "text", headers),
475
+ html: (html, status, headers) => createTypedResponse(html, status ?? defaultStatus(), "html", headers),
476
+ body: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
477
+ newResponse: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
478
+ redirect: (location, status) => new Response(null, { status: status ?? 302, headers: { Location: location } }),
479
+ notFound: () => new Response("404 Not Found", { status: 404 }),
480
+ header: (name, value, options2) => {
481
+ if (value === void 0) {
482
+ pending.headers.delete(name);
483
+ } else if (options2?.append) {
484
+ pending.headers.append(name, value);
485
+ } else {
486
+ pending.headers.set(name, value);
547
487
  }
488
+ },
489
+ status: (code) => {
490
+ pending.status = code;
548
491
  }
549
- return exported;
550
- }
551
- throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
552
- }
553
- function assertBodySchema(value, file) {
554
- if (!isGiriBodySchema(value)) {
555
- throw new Error(
556
- `${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
557
- );
558
- }
492
+ };
493
+ context[nativeContextBrand] = options.native;
494
+ context[pendingResponseBrand] = pending;
495
+ return context;
559
496
  }
560
- function assertQuerySchema(value, file) {
561
- if (!isGiriInputSchema(value)) {
562
- throw new Error(
563
- `${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
564
- );
497
+ function typedResponseToResponse(response) {
498
+ const headers = new Headers(response.headers);
499
+ if (response.format === "json" && !headers.has("content-type")) {
500
+ headers.set("content-type", "application/json; charset=utf-8");
565
501
  }
566
- }
567
- function routeInput(routeModule, file) {
568
- const input = {};
569
- if (routeModule.body !== void 0) {
570
- assertBodySchema(routeModule.body, file);
571
- input.body = routeModule.body;
502
+ if (response.format === "text" && !headers.has("content-type")) {
503
+ headers.set("content-type", "text/plain; charset=utf-8");
572
504
  }
573
- if (routeModule.query !== void 0) {
574
- assertQuerySchema(routeModule.query, file);
575
- input.query = routeModule.query;
505
+ if (response.format === "html" && !headers.has("content-type")) {
506
+ headers.set("content-type", "text/html; charset=utf-8");
576
507
  }
577
- return input.body || input.query ? input : void 0;
578
- }
579
- function aliasValues(value) {
580
- return Array.isArray(value) ? value : [value];
581
- }
582
- function resolveAliasTarget(cwd, target, capture = "") {
583
- const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
584
- return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
508
+ const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
509
+ return new Response(body, {
510
+ status: response.status,
511
+ headers
512
+ });
585
513
  }
586
- function matchAlias(request, key) {
587
- if (key.includes("*")) {
588
- const [prefix2, suffix = ""] = key.split("*");
589
- if (request.startsWith(prefix2) && request.endsWith(suffix)) {
590
- return request.slice(prefix2.length, request.length - suffix.length);
591
- }
592
- return void 0;
593
- }
594
- if (request === key) {
595
- return "";
514
+ function toResponse(response, context) {
515
+ const base = isTypedResponse(response) ? typedResponseToResponse(response) : response;
516
+ const pending = context ? getPending(context) : void 0;
517
+ if (!pending) {
518
+ return base;
596
519
  }
597
- const prefix = `${key}/`;
598
- if (request.startsWith(prefix)) {
599
- return request.slice(prefix.length);
520
+ let hasPending = false;
521
+ pending.headers.forEach(() => {
522
+ hasPending = true;
523
+ });
524
+ if (!hasPending) {
525
+ return base;
600
526
  }
601
- return void 0;
527
+ const headers = new Headers(base.headers);
528
+ pending.headers.forEach((value, key) => {
529
+ if (!headers.has(key)) {
530
+ headers.set(key, value);
531
+ }
532
+ });
533
+ return new Response(base.body, { status: base.status, statusText: base.statusText, headers });
602
534
  }
603
- function resolveAliasRequest(request, alias, cwd) {
604
- for (const [key, value] of Object.entries(alias ?? {})) {
605
- const capture = matchAlias(request, key);
606
- if (capture === void 0) {
607
- continue;
535
+ async function composeMiddleware(middleware, handle, context) {
536
+ let index = -1;
537
+ let result;
538
+ const dispatch = async (i) => {
539
+ if (i <= index) {
540
+ throw new Error("next() called multiple times in giri middleware.");
608
541
  }
609
- const [target] = aliasValues(value);
610
- if (!target) {
611
- continue;
542
+ index = i;
543
+ if (i === middleware.length) {
544
+ result = await handle(context);
545
+ return result;
612
546
  }
613
- return resolveAliasTarget(cwd, target, capture);
547
+ const returned = await middleware[i](context, () => dispatch(i + 1));
548
+ if (returned !== void 0) {
549
+ result = returned;
550
+ return returned;
551
+ }
552
+ return result;
553
+ };
554
+ await dispatch(0);
555
+ if (result === void 0) {
556
+ throw new Error("Route completed without returning a response.");
614
557
  }
615
- return void 0;
558
+ return result;
616
559
  }
617
- function registerAliasResolver(alias, cwd) {
618
- if (!alias || Object.keys(alias).length === 0) {
619
- return () => {
620
- };
560
+ function defineMiddleware(optionsOrMiddleware, maybeMiddleware) {
561
+ if (typeof optionsOrMiddleware === "function") {
562
+ return optionsOrMiddleware;
621
563
  }
622
- const moduleWithResolver = import_node_module.default;
623
- const originalResolveFilename = moduleWithResolver._resolveFilename;
624
- moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
625
- return originalResolveFilename.call(
626
- this,
627
- resolveAliasRequest(request, alias, cwd) ?? request,
628
- parent,
629
- isMain,
630
- options
631
- );
632
- };
633
- return () => {
634
- moduleWithResolver._resolveFilename = originalResolveFilename;
635
- };
636
- }
637
- var GIRI_ALIAS_PREFIX = "$giri/";
638
- var giriOutDir;
639
- var giriResolverInstalled = false;
640
- function ensureGiriAliasResolver(outDir) {
641
- giriOutDir = outDir;
642
- if (giriResolverInstalled) {
643
- return;
564
+ if (!maybeMiddleware) {
565
+ throw new Error("defineMiddleware(options, middleware) requires a middleware function.");
644
566
  }
645
- giriResolverInstalled = true;
646
- const moduleWithResolver = import_node_module.default;
647
- const originalResolveFilename = moduleWithResolver._resolveFilename;
648
- moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
649
- const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
650
- return originalResolveFilename.call(this, mapped, parent, isMain, options);
651
- };
652
- }
653
- function resolveGiriPaths(config, cwd = process.cwd()) {
654
- return {
655
- cwd: (0, import_node_path3.resolve)(cwd),
656
- routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
657
- outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
658
- };
567
+ maybeMiddleware.openapi = optionsOrMiddleware.openapi;
568
+ return maybeMiddleware;
659
569
  }
660
- async function buildGiriApp(config, options = {}) {
661
- const paths = resolveGiriPaths(config, options.cwd);
662
- const routes = await scanRoutes(paths.routesDir);
663
- const app = config.adapter.createApp();
664
- ensureGiriAliasResolver(paths.outDir);
665
- const { unregister } = await safeRegister();
666
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
667
- try {
668
- for (const route of routes) {
669
- const routeModule = loadModule(route.file);
670
- if (typeof routeModule.handle !== "function") {
671
- throw new Error(`${route.file} must export a named handle function.`);
672
- }
673
- const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
674
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
675
- );
676
- const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
677
- config.adapter.register(app, {
678
- method: route.method,
679
- path: route.path,
680
- handle: routeModule.handle,
681
- middleware: [...folderMiddleware, ...verbMiddleware],
682
- input: routeInput(routeModule, route.file),
683
- services: options.services
684
- });
685
- }
686
- } finally {
687
- unregisterAliasResolver();
688
- unregister();
689
- }
690
- return { app, routes, paths };
570
+ function stack(...middleware) {
571
+ return middleware;
691
572
  }
692
573
 
693
- // src/generator/sync.ts
694
- var import_node_fs6 = require("fs");
695
- var import_promises3 = require("fs/promises");
696
- var import_node_path11 = require("path");
697
-
698
- // src/generator/app-types.ts
699
- var import_node_fs4 = require("fs");
700
- var import_node_path5 = require("path");
701
-
702
- // src/generator/util.ts
703
- var import_node_fs3 = require("fs");
704
- var import_promises2 = require("fs/promises");
705
- var import_node_path4 = require("path");
706
- var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
707
- function slash(path) {
708
- return path.split(import_node_path4.sep).join("/");
709
- }
710
- function importPath(fromFile, toFile) {
711
- let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
712
- if (!path.startsWith(".")) {
713
- path = `./${path}`;
714
- }
715
- return path;
716
- }
717
- function relativeConfigPath(fromDir, toPath) {
718
- let path = slash((0, import_node_path4.relative)(fromDir, toPath));
719
- if (!path.startsWith(".")) {
720
- path = `./${path}`;
721
- }
722
- return path;
574
+ // src/validation.ts
575
+ function defineInputSchema(schema) {
576
+ return { [inputSchemaBrand]: true, ...schema };
723
577
  }
724
- function moduleSpecifier(fromDir, target) {
725
- let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
726
- if (!path.startsWith(".")) {
727
- path = `./${path}`;
728
- }
729
- return path;
578
+ function isGiriInputSchema(value) {
579
+ return Boolean(
580
+ value && typeof value === "object" && value[inputSchemaBrand] === true
581
+ );
730
582
  }
731
- function typeFilePath(paths, routeDir) {
732
- const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
733
- return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
583
+ function defineBodySchema(contents) {
584
+ return { [bodySchemaBrand]: true, contents };
734
585
  }
735
- function assertSafeOutDir(paths) {
736
- const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
737
- if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
738
- throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
739
- }
586
+ function isGiriBodySchema(value) {
587
+ return Boolean(
588
+ value && typeof value === "object" && value[bodySchemaBrand] === true
589
+ );
740
590
  }
741
- var writeCache = /* @__PURE__ */ new Map();
742
- async function writeGenerated(path, content) {
743
- if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
744
- return;
591
+ var MIME_TO_CONTENT_TYPE = {
592
+ "application/json": "json",
593
+ "multipart/form-data": "form",
594
+ "application/x-www-form-urlencoded": "urlencoded",
595
+ "text/plain": "text"
596
+ };
597
+ function contentTypeFromHeader(header) {
598
+ if (!header) {
599
+ return void 0;
745
600
  }
746
- await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
747
- await (0, import_promises2.writeFile)(path, content);
748
- writeCache.set(path, content);
601
+ const mime = header.split(";", 1)[0].trim().toLowerCase();
602
+ return MIME_TO_CONTENT_TYPE[mime];
749
603
  }
750
- async function writeJson(path, value) {
751
- await writeGenerated(path, `${JSON.stringify(value, null, 2)}
752
- `);
604
+ function formDataObject(form) {
605
+ const result = {};
606
+ form.forEach((value, key) => {
607
+ const current = result[key];
608
+ if (current === void 0) {
609
+ result[key] = value;
610
+ } else if (Array.isArray(current)) {
611
+ current.push(value);
612
+ } else {
613
+ result[key] = [current, value];
614
+ }
615
+ });
616
+ return result;
753
617
  }
754
- async function pruneDir(dir, keep) {
755
- let entries;
756
- try {
757
- entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
758
- } catch {
759
- return;
618
+ async function readRawBody(request, contentType) {
619
+ const cloned = request.clone();
620
+ if (contentType === "json") {
621
+ return cloned.json();
760
622
  }
761
- for (const entry of entries) {
762
- const full = (0, import_node_path4.join)(dir, entry.name);
763
- if (entry.isDirectory()) {
764
- await pruneDir(full, keep);
765
- await (0, import_promises2.rmdir)(full).catch(() => {
766
- });
767
- } else if (!keep.has(full)) {
768
- await (0, import_promises2.rm)(full, { force: true });
769
- writeCache.delete(full);
770
- }
623
+ if (contentType === "text") {
624
+ return cloned.text();
771
625
  }
626
+ return formDataObject(await cloned.formData());
772
627
  }
773
-
774
- // src/generator/app-types.ts
775
- var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
776
- function findMainFile(cwd) {
777
- for (const ext of MAIN_EXTENSIONS) {
778
- const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
779
- if ((0, import_node_fs4.existsSync)(file)) {
780
- return file;
628
+ function queryObject(url) {
629
+ const result = {};
630
+ for (const [key, value] of url.searchParams) {
631
+ const current = result[key];
632
+ if (current === void 0) {
633
+ result[key] = value;
634
+ } else if (Array.isArray(current)) {
635
+ current.push(value);
636
+ } else {
637
+ result[key] = [current, value];
781
638
  }
782
639
  }
783
- return void 0;
640
+ return result;
784
641
  }
785
- function moduleSpecifier2(fromDir, target) {
786
- let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
787
- if (!path.startsWith(".")) {
788
- path = `./${path}`;
642
+ async function runValidation(schema, value, label) {
643
+ if (!isGiriInputSchema(schema)) {
644
+ throw new Error(
645
+ `giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
646
+ );
789
647
  }
790
- return path;
648
+ return schema.validate(value);
791
649
  }
792
- async function writeAppTypes(paths) {
793
- const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
794
- const mainFile = findMainFile(paths.cwd);
795
- if (!mainFile) {
796
- await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
797
- return;
650
+ async function prepareRequestInput(request, input) {
651
+ const validated = {};
652
+ if (input?.query) {
653
+ const query = queryObject(new URL(request.url));
654
+ const result = await runValidation(input.query, query, "query");
655
+ if (!result.ok) {
656
+ return {
657
+ ok: false,
658
+ response: createTypedResponse(
659
+ { message: "Invalid query parameters.", issues: result.issues },
660
+ 400,
661
+ "json"
662
+ )
663
+ };
664
+ }
665
+ validated.query = result.value;
798
666
  }
799
- const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
800
- await writeGenerated(
801
- file,
802
- [
803
- GENERATED_HEADER,
804
- "declare global {",
805
- " namespace Giri {",
806
- " interface Register {",
807
- ` app: typeof import(${JSON.stringify(spec)}) extends {`,
808
- " init: (...args: any[]) => infer R;",
809
- " }",
810
- " ? Awaited<R>",
811
- " : Record<string, unknown>;",
812
- " }",
813
- " }",
814
- "}",
815
- "export {};",
816
- ""
817
- ].join("\n")
818
- );
819
- }
820
-
821
- // src/generator/manifest.ts
822
- var import_node_path6 = require("path");
823
- async function writeManifest(paths, routes, data = {}) {
824
- const manifest = {
825
- version: 1,
826
- routes: routes.map((route) => {
827
- const responses = data.responsesByFile?.get(route.file);
828
- const input = data.inputsByFile?.get(route.file);
829
- const security = data.securityByFile?.get(route.file);
667
+ if (input?.body) {
668
+ const contents = input.body.contents;
669
+ const declared = Object.keys(contents);
670
+ const requested = contentTypeFromHeader(request.headers.get("content-type"));
671
+ const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
672
+ if (!chosen) {
830
673
  return {
831
- method: route.method,
832
- path: route.path,
833
- file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
834
- params: route.params,
835
- shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
836
- types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
837
- ...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
838
- ...input ? { input } : {},
839
- ...security && security.security.length > 0 ? { security: security.security } : {},
840
- responses: responses?.responses ?? [],
841
- ...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
674
+ ok: false,
675
+ response: createTypedResponse(
676
+ { message: "Unsupported media type.", issues: { accepted: declared } },
677
+ 415,
678
+ "json"
679
+ )
842
680
  };
843
- })
844
- };
845
- await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
681
+ }
682
+ let rawBody;
683
+ try {
684
+ rawBody = await readRawBody(request, chosen);
685
+ } catch (error) {
686
+ return {
687
+ ok: false,
688
+ response: createTypedResponse(
689
+ { message: "Invalid request body.", issues: error },
690
+ 400,
691
+ "json"
692
+ )
693
+ };
694
+ }
695
+ const result = await runValidation(contents[chosen], rawBody, "body");
696
+ if (!result.ok) {
697
+ return {
698
+ ok: false,
699
+ response: createTypedResponse(
700
+ { message: "Invalid request body.", issues: result.issues },
701
+ 400,
702
+ "json"
703
+ )
704
+ };
705
+ }
706
+ validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
707
+ }
708
+ return { ok: true, validated };
846
709
  }
847
710
 
848
- // src/generator/openapi.ts
849
- var import_node_fs5 = require("fs");
850
- var import_node_path7 = require("path");
851
- var REASON = {
852
- 200: "OK",
853
- 201: "Created",
854
- 202: "Accepted",
855
- 204: "No Content",
856
- 400: "Bad Request",
857
- 401: "Unauthorized",
858
- 403: "Forbidden",
859
- 404: "Not Found",
860
- 409: "Conflict",
861
- 422: "Unprocessable Entity",
862
- 500: "Internal Server Error"
863
- };
864
- function toOpenApiPath(path) {
865
- return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
866
- }
867
- function rewriteRefs(value) {
868
- if (Array.isArray(value)) {
869
- return value.map(rewriteRefs);
711
+ // src/app.ts
712
+ var import_node_module = __toESM(require("module"));
713
+ var import_node_path3 = require("path");
714
+
715
+ // src/loader/loader.ts
716
+ var import_prompts = require("@clack/prompts");
717
+ var import_node_fs = require("fs");
718
+ var import_node_path = require("path");
719
+ var import_node_process = require("process");
720
+
721
+ // src/config/schema.ts
722
+ var import_typebox = require("@sinclair/typebox");
723
+ var configSchema = import_typebox.Type.Object({
724
+ adapter: import_typebox.Type.Any(),
725
+ alias: import_typebox.Type.Optional(import_typebox.Type.Record(
726
+ import_typebox.Type.String(),
727
+ import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
728
+ )),
729
+ outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
730
+ server: import_typebox.Type.Optional(import_typebox.Type.Object({
731
+ port: import_typebox.Type.Optional(import_typebox.Type.Number()),
732
+ hostname: import_typebox.Type.Optional(import_typebox.Type.String())
733
+ }, { additionalProperties: false })),
734
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
735
+ }, { additionalProperties: false });
736
+
737
+ // src/loader/loader.ts
738
+ var import_value = require("@sinclair/typebox/value");
739
+ var assertES5 = async (unregister) => {
740
+ try {
741
+ init_es5();
742
+ } catch (e) {
743
+ if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
744
+ const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
745
+ if (es5Error) {
746
+ import_prompts.log.error(
747
+ `Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
748
+ );
749
+ (0, import_node_process.exit)(1);
750
+ }
751
+ }
752
+ import_prompts.log.error(e);
753
+ (0, import_node_process.exit)(1);
870
754
  }
871
- if (value && typeof value === "object") {
872
- const out = {};
873
- for (const [key, child] of Object.entries(value)) {
874
- if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
875
- out.$ref = child.replace("#/$defs/", "#/components/schemas/");
876
- } else {
877
- out[key] = rewriteRefs(child);
755
+ };
756
+ var safeRegister = async () => {
757
+ const { register } = await import("esbuild-register/dist/node");
758
+ let res;
759
+ try {
760
+ res = register({
761
+ format: "cjs",
762
+ loader: "ts"
763
+ });
764
+ } catch {
765
+ res = {
766
+ unregister: () => {
878
767
  }
879
- }
880
- return out;
768
+ };
881
769
  }
882
- return value;
770
+ await assertES5(res.unregister);
771
+ return res;
772
+ };
773
+
774
+ // src/routes.ts
775
+ var import_node_fs2 = require("fs");
776
+ var import_promises = require("fs/promises");
777
+ var import_node_path2 = require("path");
778
+ var import_tinyglobby = require("tinyglobby");
779
+ var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
780
+ var METHOD_FROM_FILE = new Map(
781
+ METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
782
+ );
783
+ function normalizeSlashes(path) {
784
+ return path.split(import_node_path2.sep).join("/");
883
785
  }
884
- function mediaTypeFor(format) {
885
- return format === "text" ? "text/plain" : "application/json";
786
+ function isRouteSourceFile(fileName) {
787
+ return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
886
788
  }
887
- var BODY_MEDIA_TYPE = {
888
- json: "application/json",
889
- form: "multipart/form-data",
890
- urlencoded: "application/x-www-form-urlencoded",
891
- text: "text/plain"
892
- };
893
- function buildResponses(responses) {
894
- if (responses.length === 0) {
895
- return { default: { description: "Response" } };
896
- }
897
- const out = {};
898
- for (const response of responses) {
899
- const key = response.status === "default" ? "default" : String(response.status);
900
- const description = typeof response.status === "number" && REASON[response.status] || "Response";
901
- out[key] = {
902
- description,
903
- content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
904
- };
789
+ function methodFromFile(fileName) {
790
+ if (!isRouteSourceFile(fileName)) {
791
+ return void 0;
905
792
  }
906
- return out;
793
+ const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
794
+ return METHOD_FROM_FILE.get(stem);
907
795
  }
908
- function pathParameters(route) {
909
- const seen = /* @__PURE__ */ new Set();
910
- const params = [];
911
- for (const param of route.params) {
912
- if (seen.has(param.name)) {
913
- continue;
796
+ function sharedFileIn(dir) {
797
+ for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
798
+ const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
799
+ if ((0, import_node_fs2.existsSync)(file)) {
800
+ return file;
914
801
  }
915
- seen.add(param.name);
916
- params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
917
802
  }
918
- return params;
803
+ return void 0;
919
804
  }
920
- function queryParameters(query) {
921
- if (!query || query.type !== "object" || typeof query.properties !== "object") {
805
+ function physicalRouteSegments(routesDir, routeDir) {
806
+ const rel = (0, import_node_path2.relative)(routesDir, routeDir);
807
+ if (!rel) {
922
808
  return [];
923
809
  }
924
- const properties = query.properties;
925
- const required = Array.isArray(query.required) ? query.required : [];
926
- return Object.entries(properties).map(([name, schema]) => ({
927
- name,
928
- in: "query",
929
- required: required.includes(name),
930
- schema: rewriteRefs(schema)
931
- }));
810
+ return normalizeSlashes(rel).split("/").filter(Boolean);
932
811
  }
933
- function readProjectInfo(cwd) {
934
- const file = (0, import_node_path7.join)(cwd, "package.json");
935
- if ((0, import_node_fs5.existsSync)(file)) {
936
- try {
937
- const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
938
- return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
939
- } catch {
940
- }
812
+ function urlSegment(segment) {
813
+ if (/^\(.+\)$/.test(segment)) {
814
+ return {};
941
815
  }
942
- return { title: "giri API", version: "0.0.0" };
816
+ const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
817
+ if (catchAll) {
818
+ const name = catchAll[1];
819
+ return {
820
+ value: `:${name}{.*}`,
821
+ param: { name, catchAll: true }
822
+ };
823
+ }
824
+ const param = /^\[(.+)\]$/.exec(segment);
825
+ if (param) {
826
+ const name = param[1];
827
+ return {
828
+ value: `:${name}`,
829
+ param: { name, catchAll: false }
830
+ };
831
+ }
832
+ return { value: segment };
943
833
  }
944
- function buildOpenApiDocument(paths, routes, data = {}) {
945
- const documentPaths = {};
946
- const schemas = {};
947
- const securitySchemes = {};
948
- for (const route of routes) {
949
- if (data.hiddenFiles?.has(route.file)) {
950
- continue;
951
- }
952
- const responses = data.responsesByFile?.get(route.file);
953
- const input = data.inputsByFile?.get(route.file);
954
- const security = data.securityByFile?.get(route.file);
955
- for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
956
- schemas[name] = rewriteRefs(schema);
957
- }
958
- const operation = { responses: buildResponses(responses?.responses ?? []) };
959
- const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
960
- if (parameters.length > 0) {
961
- operation.parameters = parameters;
962
- }
963
- if (input?.body) {
964
- const content = {};
965
- for (const [contentType, schema] of Object.entries(input.body)) {
966
- content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
967
- schema: rewriteRefs(schema)
968
- };
969
- }
970
- if (Object.keys(content).length > 0) {
971
- operation.requestBody = { required: true, content };
972
- }
973
- }
974
- if (security && security.security.length > 0) {
975
- operation.security = security.security;
834
+ function pathFromSegments(segments) {
835
+ const pathSegments = [];
836
+ const params = [];
837
+ for (const segment of segments) {
838
+ const converted = urlSegment(segment);
839
+ if (converted.value) {
840
+ pathSegments.push(converted.value);
976
841
  }
977
- if (security) {
978
- Object.assign(securitySchemes, security.securitySchemes);
842
+ if (converted.param) {
843
+ params.push(converted.param);
979
844
  }
980
- const openApiPath = toOpenApiPath(route.path);
981
- const pathItem = documentPaths[openApiPath] ?? {};
982
- pathItem[route.method.toLowerCase()] = operation;
983
- documentPaths[openApiPath] = pathItem;
984
845
  }
985
- const document = {
986
- openapi: "3.1.0",
987
- info: readProjectInfo(paths.cwd),
988
- paths: documentPaths
846
+ return {
847
+ path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
848
+ params
989
849
  };
990
- const components = {};
991
- if (Object.keys(schemas).length > 0) {
992
- components.schemas = schemas;
993
- }
994
- if (Object.keys(securitySchemes).length > 0) {
995
- components.securitySchemes = securitySchemes;
996
- }
997
- if (Object.keys(components).length > 0) {
998
- document.components = components;
999
- }
1000
- return document;
1001
- }
1002
- async function writeOpenApi(paths, routes, data = {}) {
1003
- await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
1004
- }
1005
-
1006
- // src/generator/param-types.ts
1007
- var import_node_path8 = require("path");
1008
- function paramsType(params) {
1009
- if (params.length === 0) {
1010
- return "{}";
1011
- }
1012
- const unique = /* @__PURE__ */ new Map();
1013
- for (const param of params) {
1014
- unique.set(param.name, param);
1015
- }
1016
- const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
1017
- return `{
1018
- ${fields}
1019
- }`;
1020
850
  }
1021
- function varsType(typesDir, sharedFiles) {
1022
- if (sharedFiles.length === 0) {
1023
- return "{}";
851
+ async function scanRouteFolders(routesDir) {
852
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
853
+ return [];
1024
854
  }
1025
- return sharedFiles.map((file) => {
1026
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1027
- return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1028
- }).join("\n & ");
1029
- }
1030
- function methodExports(typesDir, verbs) {
1031
- return verbs.map(({ method, file }) => {
1032
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1033
- const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
1034
- const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
1035
- return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
1036
- });
1037
- }
1038
- async function writeParamTypes(paths, folders) {
1039
- for (const { dir, params, sharedFiles, verbs } of folders) {
1040
- const file = typeFilePath(paths, dir);
1041
- const typesDir = (0, import_node_path8.dirname)(file);
1042
- const lines = [
1043
- GENERATED_HEADER,
1044
- `export type Params = ${paramsType(params)};`,
1045
- "export type RouteParams = Params;",
1046
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1047
- "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1048
- ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1049
- 'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
1050
- ' import("@boon4681/giri").Handle<Params, Input, Vars>;'
1051
- ];
1052
- if (verbs.length > 0) {
1053
- lines.push(...methodExports(typesDir, verbs));
855
+ const folders = [routesDir];
856
+ const walk = async (dir) => {
857
+ for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
858
+ if (entry.isDirectory() && entry.name !== "node_modules") {
859
+ const full = (0, import_node_path2.join)(dir, entry.name);
860
+ folders.push(full);
861
+ await walk(full);
862
+ }
1054
863
  }
1055
- lines.push("");
1056
- await writeGenerated(file, lines.join("\n"));
1057
- }
1058
- }
1059
-
1060
- // src/generator/inputs.ts
1061
- function sanitize(schema) {
1062
- const { $schema, ...rest } = schema;
1063
- void $schema;
1064
- return rest;
864
+ };
865
+ await walk(routesDir);
866
+ return folders;
1065
867
  }
1066
- function inputToJsonSchema(schema) {
1067
- if (!isGiriInputSchema(schema)) {
1068
- return void 0;
868
+ function routeParamsForDir(routesDir, dir) {
869
+ return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
870
+ }
871
+ function sharedFilesForDir(routesDir, dir) {
872
+ const segments = physicalRouteSegments(routesDir, dir);
873
+ const dirs = [routesDir];
874
+ let current = routesDir;
875
+ for (const segment of segments) {
876
+ current = (0, import_node_path2.join)(current, segment);
877
+ dirs.push(current);
1069
878
  }
1070
- return sanitize(schema.toJsonSchema());
879
+ return dirs.map(sharedFileIn).filter((file) => Boolean(file));
1071
880
  }
1072
- function bodyToJsonSchemas(value) {
1073
- if (!isGiriBodySchema(value)) {
1074
- return void 0;
881
+ async function scanRoutes(routesDir) {
882
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
883
+ return [];
1075
884
  }
1076
- const out = {};
1077
- for (const [contentType, schema] of Object.entries(value.contents)) {
1078
- const json = inputToJsonSchema(schema);
1079
- if (json) {
1080
- out[contentType] = json;
885
+ const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
886
+ cwd: routesDir,
887
+ absolute: true,
888
+ onlyFiles: true
889
+ });
890
+ const routes = [];
891
+ for (const file of files) {
892
+ const method = methodFromFile((0, import_node_path2.basename)(file));
893
+ if (!method) {
894
+ continue;
1081
895
  }
896
+ const routeDir = (0, import_node_path2.dirname)(file);
897
+ const routeSegments = physicalRouteSegments(routesDir, routeDir);
898
+ const { path, params } = pathFromSegments(routeSegments);
899
+ routes.push({
900
+ method,
901
+ path,
902
+ file,
903
+ routeDir,
904
+ routeSegments,
905
+ params,
906
+ sharedFiles: sharedFilesForDir(routesDir, routeDir)
907
+ });
1082
908
  }
1083
- return Object.keys(out).length > 0 ? out : void 0;
909
+ return routes.sort((left, right) => {
910
+ const pathOrder = left.path.localeCompare(right.path);
911
+ if (pathOrder !== 0) {
912
+ return pathOrder;
913
+ }
914
+ return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
915
+ });
1084
916
  }
1085
917
 
1086
- // src/generator/route-meta.ts
1087
- function loadModule2(file) {
918
+ // src/app.ts
919
+ function loadModule(file) {
1088
920
  const resolved = require.resolve(file);
1089
921
  delete require.cache[resolved];
1090
922
  return require(resolved);
1091
923
  }
1092
- function interopDefault2(value) {
924
+ function interopDefault(value) {
1093
925
  if (value && typeof value === "object" && "default" in value) {
1094
926
  return value.default;
1095
927
  }
1096
928
  return value;
1097
929
  }
1098
- function middlewareFunctions(value) {
1099
- const exported = interopDefault2(value);
930
+ function normalizeMiddleware(value, file) {
931
+ const exported = interopDefault(value);
932
+ if (exported === void 0) {
933
+ return [];
934
+ }
1100
935
  if (typeof exported === "function") {
1101
936
  return [exported];
1102
937
  }
1103
938
  if (Array.isArray(exported)) {
1104
- return exported.filter((fn) => typeof fn === "function");
939
+ for (const middleware of exported) {
940
+ if (typeof middleware !== "function") {
941
+ throw new Error(`Middleware export in ${file} must contain only functions.`);
942
+ }
943
+ }
944
+ return exported;
1105
945
  }
1106
- return [];
946
+ throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
1107
947
  }
1108
- function readInput(routeModule) {
948
+ function assertBodySchema(value, file) {
949
+ if (!isGiriBodySchema(value)) {
950
+ throw new Error(
951
+ `${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
952
+ );
953
+ }
954
+ }
955
+ function assertQuerySchema(value, file) {
956
+ if (!isGiriInputSchema(value)) {
957
+ throw new Error(
958
+ `${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
959
+ );
960
+ }
961
+ }
962
+ function routeInput(routeModule, file) {
1109
963
  const input = {};
1110
- const body = bodyToJsonSchemas(routeModule.body);
1111
- const query = inputToJsonSchema(routeModule.query);
1112
- if (body) {
1113
- input.body = body;
964
+ if (routeModule.body !== void 0) {
965
+ assertBodySchema(routeModule.body, file);
966
+ input.body = routeModule.body;
1114
967
  }
1115
- if (query) {
1116
- input.query = query;
968
+ if (routeModule.query !== void 0) {
969
+ assertQuerySchema(routeModule.query, file);
970
+ input.query = routeModule.query;
1117
971
  }
1118
972
  return input.body || input.query ? input : void 0;
1119
973
  }
1120
- function hiddenFrom(value) {
1121
- if (value === false) {
1122
- return true;
974
+ function aliasValues(value) {
975
+ return Array.isArray(value) ? value : [value];
976
+ }
977
+ function resolveAliasTarget(cwd, target, capture = "") {
978
+ const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
979
+ return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
980
+ }
981
+ function matchAlias(request, key) {
982
+ if (key.includes("*")) {
983
+ const [prefix2, suffix = ""] = key.split("*");
984
+ if (request.startsWith(prefix2) && request.endsWith(suffix)) {
985
+ return request.slice(prefix2.length, request.length - suffix.length);
986
+ }
987
+ return void 0;
1123
988
  }
1124
- if (value === true) {
1125
- return false;
989
+ if (request === key) {
990
+ return "";
1126
991
  }
1127
- if (value && typeof value === "object" && "hidden" in value) {
1128
- return Boolean(value.hidden);
992
+ const prefix = `${key}/`;
993
+ if (request.startsWith(prefix)) {
994
+ return request.slice(prefix.length);
1129
995
  }
1130
996
  return void 0;
1131
997
  }
1132
- function collectHidden(route, routeModule, loadShared) {
1133
- let hidden = false;
1134
- for (const file of route.sharedFiles) {
1135
- const opinion = hiddenFrom(loadShared(file).openapi);
1136
- if (opinion !== void 0) {
1137
- hidden = opinion;
998
+ function resolveAliasRequest(request, alias, cwd) {
999
+ for (const [key, value] of Object.entries(alias ?? {})) {
1000
+ const capture = matchAlias(request, key);
1001
+ if (capture === void 0) {
1002
+ continue;
1003
+ }
1004
+ const [target] = aliasValues(value);
1005
+ if (!target) {
1006
+ continue;
1138
1007
  }
1008
+ return resolveAliasTarget(cwd, target, capture);
1139
1009
  }
1140
- const verb = hiddenFrom(routeModule.openapi);
1141
- return verb ?? hidden;
1010
+ return void 0;
1142
1011
  }
1143
- function collectSecurity(route, routeModule, loadShared) {
1144
- const skipInherited = Boolean(
1145
- routeModule.config?.skipInherited
1146
- );
1147
- const middleware = [];
1148
- if (!skipInherited) {
1149
- for (const file of route.sharedFiles) {
1150
- middleware.push(...middlewareFunctions(loadShared(file).middleware));
1151
- }
1012
+ function registerAliasResolver(alias, cwd) {
1013
+ if (!alias || Object.keys(alias).length === 0) {
1014
+ return () => {
1015
+ };
1152
1016
  }
1153
- middleware.push(...middlewareFunctions(routeModule.middleware));
1154
- const security = [];
1155
- const securitySchemes = {};
1156
- for (const fn of middleware) {
1157
- const openapi = fn.openapi;
1158
- if (openapi?.security) {
1159
- for (const requirement of openapi.security) {
1160
- if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
1161
- security.push(requirement);
1162
- }
1163
- }
1164
- }
1165
- if (openapi?.securitySchemes) {
1166
- Object.assign(securitySchemes, openapi.securitySchemes);
1167
- }
1017
+ const moduleWithResolver = import_node_module.default;
1018
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
1019
+ moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
1020
+ return originalResolveFilename.call(
1021
+ this,
1022
+ resolveAliasRequest(request, alias, cwd) ?? request,
1023
+ parent,
1024
+ isMain,
1025
+ options
1026
+ );
1027
+ };
1028
+ return () => {
1029
+ moduleWithResolver._resolveFilename = originalResolveFilename;
1030
+ };
1031
+ }
1032
+ var GIRI_ALIAS_PREFIX = "$giri/";
1033
+ var giriOutDir;
1034
+ var giriResolverInstalled = false;
1035
+ function ensureGiriAliasResolver(outDir) {
1036
+ giriOutDir = outDir;
1037
+ if (giriResolverInstalled) {
1038
+ return;
1168
1039
  }
1169
- return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
1040
+ giriResolverInstalled = true;
1041
+ const moduleWithResolver = import_node_module.default;
1042
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
1043
+ moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
1044
+ const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
1045
+ return originalResolveFilename.call(this, mapped, parent, isMain, options);
1046
+ };
1170
1047
  }
1171
- async function extractRouteMeta(config, paths, routes) {
1172
- const byFile = /* @__PURE__ */ new Map();
1173
- const { unregister } = await safeRegister();
1174
- const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1175
- const sharedCache = /* @__PURE__ */ new Map();
1176
- const loadShared = (file) => {
1177
- if (!sharedCache.has(file)) {
1178
- try {
1179
- sharedCache.set(file, loadModule2(file));
1180
- } catch {
1181
- sharedCache.set(file, {});
1182
- }
1183
- }
1184
- return sharedCache.get(file);
1048
+ function resolveGiriPaths(config, cwd = process.cwd()) {
1049
+ return {
1050
+ cwd: (0, import_node_path3.resolve)(cwd),
1051
+ routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
1052
+ outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
1185
1053
  };
1054
+ }
1055
+ async function buildGiriApp(config, options = {}) {
1056
+ const paths = resolveGiriPaths(config, options.cwd);
1057
+ const routes = await scanRoutes(paths.routesDir);
1058
+ const app = config.adapter.createApp();
1059
+ ensureGiriAliasResolver(paths.outDir);
1060
+ const { unregister } = await safeRegister();
1061
+ const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
1186
1062
  try {
1187
1063
  for (const route of routes) {
1188
- try {
1189
- const routeModule = loadModule2(route.file);
1190
- const meta = {};
1191
- const input = readInput(routeModule);
1192
- const security = collectSecurity(route, routeModule, loadShared);
1193
- const hidden = collectHidden(route, routeModule, loadShared);
1194
- if (input) {
1195
- meta.input = input;
1196
- }
1197
- if (security) {
1198
- meta.security = security;
1199
- }
1200
- if (hidden) {
1201
- meta.hidden = true;
1202
- }
1203
- if (meta.input || meta.security || meta.hidden) {
1204
- byFile.set(route.file, meta);
1205
- }
1206
- } catch {
1064
+ const routeModule = loadModule(route.file);
1065
+ if (typeof routeModule.handle !== "function") {
1066
+ throw new Error(`${route.file} must export a named handle function.`);
1207
1067
  }
1068
+ const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
1069
+ (file) => normalizeMiddleware(loadModule(file).middleware, file)
1070
+ );
1071
+ const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
1072
+ config.adapter.register(app, {
1073
+ method: route.method,
1074
+ path: route.path,
1075
+ handle: routeModule.handle,
1076
+ middleware: [...folderMiddleware, ...verbMiddleware],
1077
+ input: routeInput(routeModule, route.file),
1078
+ services: options.services
1079
+ });
1208
1080
  }
1209
1081
  } finally {
1210
- unregisterAlias();
1082
+ unregisterAliasResolver();
1211
1083
  unregister();
1212
1084
  }
1213
- return byFile;
1085
+ return { app, routes, paths };
1214
1086
  }
1215
1087
 
1216
- // src/generator/route-types.ts
1217
- var import_node_path9 = require("path");
1218
- async function writeRouteTypes(paths, routes) {
1219
- const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
1220
- const lines = [
1221
- GENERATED_HEADER,
1222
- "export interface RouteParams {"
1223
- ];
1224
- for (const route of routes) {
1225
- const typeFile = typeFilePath(paths, route.routeDir);
1226
- lines.push(
1227
- ` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
1228
- importPath(file, typeFile)
1229
- )}).Params;`
1230
- );
1088
+ // src/generator/sync.ts
1089
+ var import_node_fs6 = require("fs");
1090
+ var import_promises3 = require("fs/promises");
1091
+ var import_node_path11 = require("path");
1092
+
1093
+ // src/generator/app-types.ts
1094
+ var import_node_fs4 = require("fs");
1095
+ var import_node_path5 = require("path");
1096
+
1097
+ // src/generator/util.ts
1098
+ var import_node_fs3 = require("fs");
1099
+ var import_promises2 = require("fs/promises");
1100
+ var import_node_path4 = require("path");
1101
+ var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
1102
+ function slash(path) {
1103
+ return path.split(import_node_path4.sep).join("/");
1104
+ }
1105
+ function importPath(fromFile, toFile) {
1106
+ let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
1107
+ if (!path.startsWith(".")) {
1108
+ path = `./${path}`;
1231
1109
  }
1232
- lines.push("}", "");
1233
- await writeGenerated(file, lines.join("\n"));
1110
+ return path;
1234
1111
  }
1235
-
1236
- // src/generator/schema/program.ts
1237
- var import_typescript = __toESM(require("typescript"));
1238
- var DEFAULT_OPTIONS = {
1239
- target: import_typescript.default.ScriptTarget.ES2022,
1240
- module: import_typescript.default.ModuleKind.NodeNext,
1241
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
1242
- strict: true,
1243
- skipLibCheck: true,
1244
- noEmit: true
1245
- };
1246
- function createSchemaProgram(paths, routeFiles) {
1247
- let options = { ...DEFAULT_OPTIONS };
1248
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
1249
- if (configPath) {
1250
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
1251
- ...import_typescript.default.sys,
1252
- onUnRecoverableConfigFileDiagnostic: () => {
1253
- }
1254
- });
1255
- if (parsed) {
1256
- options = { ...parsed.options, noEmit: true };
1112
+ function relativeConfigPath(fromDir, toPath) {
1113
+ let path = slash((0, import_node_path4.relative)(fromDir, toPath));
1114
+ if (!path.startsWith(".")) {
1115
+ path = `./${path}`;
1116
+ }
1117
+ return path;
1118
+ }
1119
+ function moduleSpecifier(fromDir, target) {
1120
+ let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
1121
+ if (!path.startsWith(".")) {
1122
+ path = `./${path}`;
1123
+ }
1124
+ return path;
1125
+ }
1126
+ function typeFilePath(paths, routeDir) {
1127
+ const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
1128
+ return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
1129
+ }
1130
+ function assertSafeOutDir(paths) {
1131
+ const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
1132
+ if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
1133
+ throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
1134
+ }
1135
+ }
1136
+ var writeCache = /* @__PURE__ */ new Map();
1137
+ async function writeGenerated(path, content) {
1138
+ if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
1139
+ return;
1140
+ }
1141
+ await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
1142
+ await (0, import_promises2.writeFile)(path, content);
1143
+ writeCache.set(path, content);
1144
+ }
1145
+ async function writeJson(path, value) {
1146
+ await writeGenerated(path, `${JSON.stringify(value, null, 2)}
1147
+ `);
1148
+ }
1149
+ async function pruneDir(dir, keep) {
1150
+ let entries;
1151
+ try {
1152
+ entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
1153
+ } catch {
1154
+ return;
1155
+ }
1156
+ for (const entry of entries) {
1157
+ const full = (0, import_node_path4.join)(dir, entry.name);
1158
+ if (entry.isDirectory()) {
1159
+ await pruneDir(full, keep);
1160
+ await (0, import_promises2.rmdir)(full).catch(() => {
1161
+ });
1162
+ } else if (!keep.has(full)) {
1163
+ await (0, import_promises2.rm)(full, { force: true });
1164
+ writeCache.delete(full);
1257
1165
  }
1258
1166
  }
1259
- return import_typescript.default.createProgram(routeFiles, options);
1260
1167
  }
1261
1168
 
1262
- // src/generator/schema/responses.ts
1263
- var import_typescript3 = __toESM(require("typescript"));
1264
-
1265
- // src/generator/schema/json-schema.ts
1266
- var import_typescript2 = __toESM(require("typescript"));
1267
- function createWalkContext(checker, location) {
1268
- return {
1269
- checker,
1270
- location,
1271
- defs: {},
1272
- inProgress: /* @__PURE__ */ new Map(),
1273
- usedDefs: /* @__PURE__ */ new Set(),
1274
- warnings: []
1275
- };
1169
+ // src/generator/app-types.ts
1170
+ var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1171
+ function findMainFile(cwd) {
1172
+ for (const ext of MAIN_EXTENSIONS) {
1173
+ const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
1174
+ if ((0, import_node_fs4.existsSync)(file)) {
1175
+ return file;
1176
+ }
1177
+ }
1178
+ return void 0;
1276
1179
  }
1277
- function typeId(type) {
1278
- return type.id;
1180
+ function moduleSpecifier2(fromDir, target) {
1181
+ let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
1182
+ if (!path.startsWith(".")) {
1183
+ path = `./${path}`;
1184
+ }
1185
+ return path;
1279
1186
  }
1280
- function intrinsicName(type) {
1281
- return type.intrinsicName;
1187
+ async function writeAppTypes(paths) {
1188
+ const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
1189
+ const mainFile = findMainFile(paths.cwd);
1190
+ if (!mainFile) {
1191
+ await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
1192
+ return;
1193
+ }
1194
+ const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
1195
+ await writeGenerated(
1196
+ file,
1197
+ [
1198
+ GENERATED_HEADER,
1199
+ "declare global {",
1200
+ " namespace Giri {",
1201
+ " interface Register {",
1202
+ ` app: typeof import(${JSON.stringify(spec)}) extends {`,
1203
+ " init: (...args: any[]) => infer R;",
1204
+ " }",
1205
+ " ? Awaited<R>",
1206
+ " : Record<string, unknown>;",
1207
+ " }",
1208
+ " }",
1209
+ "}",
1210
+ "export {};",
1211
+ ""
1212
+ ].join("\n")
1213
+ );
1282
1214
  }
1283
- function isDateType(type) {
1284
- const symbol = type.getSymbol() ?? type.aliasSymbol;
1285
- return symbol?.getName() === "Date";
1215
+
1216
+ // src/generator/manifest.ts
1217
+ var import_node_path6 = require("path");
1218
+ async function writeManifest(paths, routes, data = {}) {
1219
+ const manifest = {
1220
+ version: 1,
1221
+ routes: routes.map((route) => {
1222
+ const responses = data.responsesByFile?.get(route.file);
1223
+ const input = data.inputsByFile?.get(route.file);
1224
+ const security = data.securityByFile?.get(route.file);
1225
+ return {
1226
+ method: route.method,
1227
+ path: route.path,
1228
+ file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
1229
+ params: route.params,
1230
+ shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
1231
+ types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
1232
+ ...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
1233
+ ...input ? { input } : {},
1234
+ ...security && security.security.length > 0 ? { security: security.security } : {},
1235
+ responses: responses?.responses ?? [],
1236
+ ...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
1237
+ };
1238
+ })
1239
+ };
1240
+ await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
1286
1241
  }
1287
- function literalValuesOf(types) {
1288
- const values = [];
1289
- for (const member of types) {
1290
- if (member.isStringLiteral() || member.isNumberLiteral()) {
1291
- values.push(member.value);
1292
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
1293
- values.push(intrinsicName(member) === "true");
1294
- } else {
1295
- return void 0;
1242
+
1243
+ // src/generator/openapi.ts
1244
+ var import_node_fs5 = require("fs");
1245
+ var import_node_path7 = require("path");
1246
+ var REASON = {
1247
+ 200: "OK",
1248
+ 201: "Created",
1249
+ 202: "Accepted",
1250
+ 204: "No Content",
1251
+ 400: "Bad Request",
1252
+ 401: "Unauthorized",
1253
+ 403: "Forbidden",
1254
+ 404: "Not Found",
1255
+ 409: "Conflict",
1256
+ 422: "Unprocessable Entity",
1257
+ 500: "Internal Server Error"
1258
+ };
1259
+ function toOpenApiPath(path) {
1260
+ return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
1261
+ }
1262
+ function rewriteRefs(value) {
1263
+ if (Array.isArray(value)) {
1264
+ return value.map(rewriteRefs);
1265
+ }
1266
+ if (value && typeof value === "object") {
1267
+ const out = {};
1268
+ for (const [key, child] of Object.entries(value)) {
1269
+ if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
1270
+ out.$ref = child.replace("#/$defs/", "#/components/schemas/");
1271
+ } else {
1272
+ out[key] = rewriteRefs(child);
1273
+ }
1296
1274
  }
1275
+ return out;
1297
1276
  }
1298
- return values;
1277
+ return value;
1299
1278
  }
1300
- function walkUnion(type, ctx) {
1301
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
1302
- const members = type.types.filter((member) => !(member.flags & flag));
1303
- if (members.length === 1) {
1304
- return walkType(members[0], ctx);
1279
+ function mediaTypeFor(format) {
1280
+ return format === "text" ? "text/plain" : "application/json";
1281
+ }
1282
+ var BODY_MEDIA_TYPE = {
1283
+ json: "application/json",
1284
+ form: "multipart/form-data",
1285
+ urlencoded: "application/x-www-form-urlencoded",
1286
+ text: "text/plain"
1287
+ };
1288
+ function buildResponses(responses) {
1289
+ if (responses.length === 0) {
1290
+ return { default: { description: "Response" } };
1305
1291
  }
1306
- const enumValues = literalValuesOf(members);
1307
- if (enumValues) {
1308
- return { enum: enumValues };
1292
+ const out = {};
1293
+ for (const response of responses) {
1294
+ const key = response.status === "default" ? "default" : String(response.status);
1295
+ const description = typeof response.status === "number" && REASON[response.status] || "Response";
1296
+ out[key] = {
1297
+ description,
1298
+ content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
1299
+ };
1309
1300
  }
1310
- return { anyOf: members.map((member) => walkType(member, ctx)) };
1301
+ return out;
1311
1302
  }
1312
- function buildObjectSchema(type, ctx) {
1313
- const { checker } = ctx;
1314
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
1315
- const properties = {};
1316
- const required = [];
1317
- for (const symbol of checker.getPropertiesOfType(type)) {
1318
- const name = symbol.getName();
1319
- const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
1320
- const optional = Boolean(symbol.getFlags() & import_typescript2.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript2.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript2.default.TypeFlags.Undefined));
1321
- properties[name] = walkType(propType, ctx);
1322
- if (!optional) {
1323
- required.push(name);
1303
+ function pathParameters(route) {
1304
+ const seen = /* @__PURE__ */ new Set();
1305
+ const params = [];
1306
+ for (const param of route.params) {
1307
+ if (seen.has(param.name)) {
1308
+ continue;
1324
1309
  }
1310
+ seen.add(param.name);
1311
+ params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
1325
1312
  }
1326
- const schema = { type: "object" };
1327
- if (Object.keys(properties).length > 0) {
1328
- schema.properties = properties;
1329
- }
1330
- if (required.length > 0) {
1331
- schema.required = required;
1332
- }
1333
- if (indexInfo) {
1334
- schema.additionalProperties = walkType(indexInfo.type, ctx);
1335
- } else if (Object.keys(properties).length > 0) {
1336
- schema.additionalProperties = false;
1337
- }
1338
- return schema;
1313
+ return params;
1339
1314
  }
1340
- function defName(type) {
1341
- const symbol = type.getSymbol() ?? type.aliasSymbol;
1342
- const name = symbol?.getName();
1343
- if (name && name !== "__type" && name !== "__object") {
1344
- return name;
1315
+ function queryParameters(query) {
1316
+ if (!query || query.type !== "object" || typeof query.properties !== "object") {
1317
+ return [];
1345
1318
  }
1346
- return `Anonymous${typeId(type)}`;
1319
+ const properties = query.properties;
1320
+ const required = Array.isArray(query.required) ? query.required : [];
1321
+ return Object.entries(properties).map(([name, schema]) => ({
1322
+ name,
1323
+ in: "query",
1324
+ required: required.includes(name),
1325
+ schema: rewriteRefs(schema)
1326
+ }));
1347
1327
  }
1348
- function walkObject(type, ctx) {
1349
- const { checker } = ctx;
1350
- if (isDateType(type)) {
1351
- return { type: "string", format: "date-time" };
1352
- }
1353
- if (checker.isArrayType(type)) {
1354
- const [element] = checker.getTypeArguments(type);
1355
- return { type: "array", items: element ? walkType(element, ctx) : {} };
1356
- }
1357
- if (checker.isTupleType(type)) {
1358
- const elements = checker.getTypeArguments(type);
1359
- return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
1360
- }
1361
- const id = typeId(type);
1362
- const existing = ctx.inProgress.get(id);
1363
- if (existing) {
1364
- ctx.usedDefs.add(existing);
1365
- return { $ref: `#/$defs/${existing}` };
1366
- }
1367
- const name = defName(type);
1368
- ctx.inProgress.set(id, name);
1369
- const schema = buildObjectSchema(type, ctx);
1370
- ctx.inProgress.delete(id);
1371
- if (ctx.usedDefs.has(name)) {
1372
- ctx.defs[name] = schema;
1373
- return { $ref: `#/$defs/${name}` };
1328
+ function readProjectInfo(cwd) {
1329
+ const file = (0, import_node_path7.join)(cwd, "package.json");
1330
+ if ((0, import_node_fs5.existsSync)(file)) {
1331
+ try {
1332
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
1333
+ return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
1334
+ } catch {
1335
+ }
1374
1336
  }
1375
- return schema;
1337
+ return { title: "giri API", version: "0.0.0" };
1376
1338
  }
1377
- function walkType(type, ctx) {
1378
- const flags = type.flags;
1379
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
1380
- return {};
1381
- }
1382
- if (flags & import_typescript2.default.TypeFlags.Null) {
1383
- return { type: "null" };
1339
+ function buildOpenApiDocument(paths, routes, data = {}) {
1340
+ const documentPaths = {};
1341
+ const schemas = {};
1342
+ const securitySchemes = {};
1343
+ for (const route of routes) {
1344
+ if (data.hiddenFiles?.has(route.file)) {
1345
+ continue;
1346
+ }
1347
+ const responses = data.responsesByFile?.get(route.file);
1348
+ const input = data.inputsByFile?.get(route.file);
1349
+ const security = data.securityByFile?.get(route.file);
1350
+ for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1351
+ schemas[name] = rewriteRefs(schema);
1352
+ }
1353
+ const operation = { responses: buildResponses(responses?.responses ?? []) };
1354
+ const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1355
+ if (parameters.length > 0) {
1356
+ operation.parameters = parameters;
1357
+ }
1358
+ if (input?.body) {
1359
+ const content = {};
1360
+ for (const [contentType, schema] of Object.entries(input.body)) {
1361
+ content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
1362
+ schema: rewriteRefs(schema)
1363
+ };
1364
+ }
1365
+ if (Object.keys(content).length > 0) {
1366
+ operation.requestBody = { required: true, content };
1367
+ }
1368
+ }
1369
+ if (security && security.security.length > 0) {
1370
+ operation.security = security.security;
1371
+ }
1372
+ if (security) {
1373
+ Object.assign(securitySchemes, security.securitySchemes);
1374
+ }
1375
+ const openApiPath = toOpenApiPath(route.path);
1376
+ const pathItem = documentPaths[openApiPath] ?? {};
1377
+ pathItem[route.method.toLowerCase()] = operation;
1378
+ documentPaths[openApiPath] = pathItem;
1384
1379
  }
1385
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
1386
- return {};
1380
+ const document = {
1381
+ openapi: "3.1.0",
1382
+ info: readProjectInfo(paths.cwd),
1383
+ paths: documentPaths
1384
+ };
1385
+ const components = {};
1386
+ if (Object.keys(schemas).length > 0) {
1387
+ components.schemas = schemas;
1387
1388
  }
1388
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
1389
- ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
1390
- return { type: "string" };
1389
+ if (Object.keys(securitySchemes).length > 0) {
1390
+ components.securitySchemes = securitySchemes;
1391
1391
  }
1392
- if (type.isStringLiteral()) {
1393
- return { type: "string", const: type.value };
1392
+ if (Object.keys(components).length > 0) {
1393
+ document.components = components;
1394
1394
  }
1395
- if (type.isNumberLiteral()) {
1396
- return { type: "number", const: type.value };
1395
+ return document;
1396
+ }
1397
+ async function writeOpenApi(paths, routes, data = {}) {
1398
+ await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
1399
+ }
1400
+
1401
+ // src/generator/param-types.ts
1402
+ var import_node_path8 = require("path");
1403
+ function paramsType(params) {
1404
+ if (params.length === 0) {
1405
+ return "{}";
1397
1406
  }
1398
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
1399
- return { type: "boolean", const: intrinsicName(type) === "true" };
1407
+ const unique = /* @__PURE__ */ new Map();
1408
+ for (const param of params) {
1409
+ unique.set(param.name, param);
1400
1410
  }
1401
- if (flags & import_typescript2.default.TypeFlags.String) {
1402
- return { type: "string" };
1411
+ const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
1412
+ return `{
1413
+ ${fields}
1414
+ }`;
1415
+ }
1416
+ function varsType(typesDir, sharedFiles) {
1417
+ if (sharedFiles.length === 0) {
1418
+ return "{}";
1403
1419
  }
1404
- if (flags & import_typescript2.default.TypeFlags.Number) {
1405
- return { type: "number" };
1420
+ return sharedFiles.map((file) => {
1421
+ const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1422
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1423
+ }).join("\n & ");
1424
+ }
1425
+ function methodExports(typesDir, verbs) {
1426
+ return verbs.map(({ method, file }) => {
1427
+ const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1428
+ const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
1429
+ const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
1430
+ return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
1431
+ });
1432
+ }
1433
+ async function writeParamTypes(paths, folders) {
1434
+ for (const { dir, params, sharedFiles, verbs } of folders) {
1435
+ const file = typeFilePath(paths, dir);
1436
+ const typesDir = (0, import_node_path8.dirname)(file);
1437
+ const lines = [
1438
+ GENERATED_HEADER,
1439
+ `export type Params = ${paramsType(params)};`,
1440
+ "export type RouteParams = Params;",
1441
+ `type Vars = ${varsType(typesDir, sharedFiles)};`,
1442
+ "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1443
+ ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1444
+ 'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
1445
+ ' import("@boon4681/giri").Handle<Params, Input, Vars>;'
1446
+ ];
1447
+ if (verbs.length > 0) {
1448
+ lines.push(...methodExports(typesDir, verbs));
1449
+ }
1450
+ lines.push("");
1451
+ await writeGenerated(file, lines.join("\n"));
1406
1452
  }
1407
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
1408
- return { type: "boolean" };
1453
+ }
1454
+
1455
+ // src/generator/inputs.ts
1456
+ function sanitize(schema) {
1457
+ const { $schema, ...rest } = schema;
1458
+ void $schema;
1459
+ return rest;
1460
+ }
1461
+ function inputToJsonSchema(schema) {
1462
+ if (!isGiriInputSchema(schema)) {
1463
+ return void 0;
1409
1464
  }
1410
- if (type.isUnion()) {
1411
- return walkUnion(type, ctx);
1465
+ return sanitize(schema.toJsonSchema());
1466
+ }
1467
+ function bodyToJsonSchemas(value) {
1468
+ if (!isGiriBodySchema(value)) {
1469
+ return void 0;
1412
1470
  }
1413
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
1414
- return walkObject(type, ctx);
1471
+ const out = {};
1472
+ for (const [contentType, schema] of Object.entries(value.contents)) {
1473
+ const json = inputToJsonSchema(schema);
1474
+ if (json) {
1475
+ out[contentType] = json;
1476
+ }
1415
1477
  }
1416
- return {};
1478
+ return Object.keys(out).length > 0 ? out : void 0;
1417
1479
  }
1418
1480
 
1419
- // src/generator/schema/responses.ts
1420
- function findHandleFunction(source) {
1421
- let found;
1422
- const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
1423
- for (const statement of source.statements) {
1424
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
1425
- found = statement;
1426
- }
1427
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
1428
- for (const declaration of statement.declarationList.declarations) {
1429
- if (import_typescript3.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript3.default.isArrowFunction(declaration.initializer) || import_typescript3.default.isFunctionExpression(declaration.initializer))) {
1430
- found = declaration.initializer;
1431
- }
1432
- }
1433
- }
1481
+ // src/generator/route-meta.ts
1482
+ function loadModule2(file) {
1483
+ const resolved = require.resolve(file);
1484
+ delete require.cache[resolved];
1485
+ return require(resolved);
1486
+ }
1487
+ function interopDefault2(value) {
1488
+ if (value && typeof value === "object" && "default" in value) {
1489
+ return value.default;
1434
1490
  }
1435
- return found;
1491
+ return value;
1436
1492
  }
1437
- function collectReturnExpressions(fn) {
1438
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
1439
- return [fn.body];
1493
+ function middlewareFunctions(value) {
1494
+ const exported = interopDefault2(value);
1495
+ if (typeof exported === "function") {
1496
+ return [exported];
1440
1497
  }
1441
- if (!fn.body) {
1442
- return [];
1498
+ if (Array.isArray(exported)) {
1499
+ return exported.filter((fn) => typeof fn === "function");
1443
1500
  }
1444
- const expressions = [];
1445
- const visit = (node) => {
1446
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
1447
- return;
1448
- }
1449
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
1450
- expressions.push(node.expression);
1451
- }
1452
- import_typescript3.default.forEachChild(node, visit);
1453
- };
1454
- import_typescript3.default.forEachChild(fn.body, visit);
1455
- return expressions;
1456
- }
1457
- function propertyType(checker, type, name, location) {
1458
- const symbol = checker.getPropertyOfType(type, name);
1459
- return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
1460
- }
1461
- function isTypedResponse2(checker, type) {
1462
- return Boolean(
1463
- checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
1464
- );
1501
+ return [];
1465
1502
  }
1466
- function readFromCall(checker, expression) {
1467
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
1468
- return void 0;
1503
+ function readInput(routeModule) {
1504
+ const input = {};
1505
+ const body = bodyToJsonSchemas(routeModule.body);
1506
+ const query = inputToJsonSchema(routeModule.query);
1507
+ if (body) {
1508
+ input.body = body;
1469
1509
  }
1470
- const method = expression.expression.name.text;
1471
- if (method !== "json" && method !== "text") {
1472
- return void 0;
1510
+ if (query) {
1511
+ input.query = query;
1473
1512
  }
1474
- if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
1475
- return void 0;
1513
+ return input.body || input.query ? input : void 0;
1514
+ }
1515
+ function hiddenFrom(value) {
1516
+ if (value === false) {
1517
+ return true;
1476
1518
  }
1477
- const [dataArg, statusArg] = expression.arguments;
1478
- if (!dataArg) {
1479
- return void 0;
1519
+ if (value === true) {
1520
+ return false;
1480
1521
  }
1481
- let status = 200;
1482
- if (statusArg) {
1483
- const statusType = checker.getTypeAtLocation(statusArg);
1484
- status = statusType.isNumberLiteral() ? statusType.value : "default";
1522
+ if (value && typeof value === "object" && "hidden" in value) {
1523
+ return Boolean(value.hidden);
1485
1524
  }
1486
- return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
1525
+ return void 0;
1487
1526
  }
1488
- function readFromType(checker, type, location) {
1489
- const dataType = propertyType(checker, type, "data", location);
1490
- const statusType = propertyType(checker, type, "status", location);
1491
- const formatType = propertyType(checker, type, "format", location);
1492
- if (!dataType || !statusType || !formatType) {
1493
- return void 0;
1527
+ function collectHidden(route, routeModule, loadShared) {
1528
+ let hidden = false;
1529
+ for (const file of route.sharedFiles) {
1530
+ const opinion = hiddenFrom(loadShared(file).openapi);
1531
+ if (opinion !== void 0) {
1532
+ hidden = opinion;
1533
+ }
1494
1534
  }
1495
- const status = statusType.isNumberLiteral() ? statusType.value : "default";
1496
- const format = formatType.isStringLiteral() && formatType.value === "text" ? "text" : "json";
1497
- return { status, format, data: dataType };
1498
- }
1499
- function constituents(type) {
1500
- return type.isUnion() ? type.types : [type];
1535
+ const verb = hiddenFrom(routeModule.openapi);
1536
+ return verb ?? hidden;
1501
1537
  }
1502
- function extractRouteResponses(program, file) {
1503
- const result = { responses: [], opaque: false, warnings: [], $defs: {} };
1504
- const source = program.getSourceFile(file);
1505
- if (!source) {
1506
- return result;
1507
- }
1508
- const checker = program.getTypeChecker();
1509
- const fn = findHandleFunction(source);
1510
- if (!fn) {
1511
- return result;
1538
+ function collectSecurity(route, routeModule, loadShared) {
1539
+ const skipInherited = Boolean(
1540
+ routeModule.config?.skipInherited
1541
+ );
1542
+ const middleware = [];
1543
+ if (!skipInherited) {
1544
+ for (const file of route.sharedFiles) {
1545
+ middleware.push(...middlewareFunctions(loadShared(file).middleware));
1546
+ }
1512
1547
  }
1513
- const ctx = createWalkContext(checker, fn);
1514
- const byStatus = /* @__PURE__ */ new Map();
1515
- const record = (hit) => {
1516
- const schema = walkType(hit.data, ctx);
1517
- const bucket = byStatus.get(hit.status) ?? { format: hit.format, schemas: [] };
1518
- bucket.schemas.push(schema);
1519
- byStatus.set(hit.status, bucket);
1520
- };
1521
- for (const expression of collectReturnExpressions(fn)) {
1522
- const fromCall = readFromCall(checker, expression);
1523
- if (fromCall) {
1524
- record(fromCall);
1525
- continue;
1548
+ middleware.push(...middlewareFunctions(routeModule.middleware));
1549
+ const security = [];
1550
+ const securitySchemes = {};
1551
+ for (const fn of middleware) {
1552
+ const openapi = fn.openapi;
1553
+ if (openapi?.security) {
1554
+ for (const requirement of openapi.security) {
1555
+ if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
1556
+ security.push(requirement);
1557
+ }
1558
+ }
1526
1559
  }
1527
- let matched = false;
1528
- for (const member of constituents(checker.getTypeAtLocation(expression))) {
1529
- const hit = readFromType(checker, member, expression);
1530
- if (hit) {
1531
- record(hit);
1532
- matched = true;
1560
+ if (openapi?.securitySchemes) {
1561
+ Object.assign(securitySchemes, openapi.securitySchemes);
1562
+ }
1563
+ }
1564
+ return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
1565
+ }
1566
+ async function extractRouteMeta(config, paths, routes) {
1567
+ const byFile = /* @__PURE__ */ new Map();
1568
+ const { unregister } = await safeRegister();
1569
+ const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1570
+ const sharedCache = /* @__PURE__ */ new Map();
1571
+ const loadShared = (file) => {
1572
+ if (!sharedCache.has(file)) {
1573
+ try {
1574
+ sharedCache.set(file, loadModule2(file));
1575
+ } catch {
1576
+ sharedCache.set(file, {});
1533
1577
  }
1534
1578
  }
1535
- if (!matched) {
1536
- result.opaque = true;
1579
+ return sharedCache.get(file);
1580
+ };
1581
+ try {
1582
+ for (const route of routes) {
1583
+ try {
1584
+ const routeModule = loadModule2(route.file);
1585
+ const meta = {};
1586
+ const input = readInput(routeModule);
1587
+ const security = collectSecurity(route, routeModule, loadShared);
1588
+ const hidden = collectHidden(route, routeModule, loadShared);
1589
+ if (input) {
1590
+ meta.input = input;
1591
+ }
1592
+ if (security) {
1593
+ meta.security = security;
1594
+ }
1595
+ if (hidden) {
1596
+ meta.hidden = true;
1597
+ }
1598
+ if (meta.input || meta.security || meta.hidden) {
1599
+ byFile.set(route.file, meta);
1600
+ }
1601
+ } catch {
1602
+ }
1537
1603
  }
1604
+ } finally {
1605
+ unregisterAlias();
1606
+ unregister();
1538
1607
  }
1539
- for (const [status, { format, schemas }] of byStatus) {
1540
- const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
1541
- result.responses.push({ status, format, schema });
1608
+ return byFile;
1609
+ }
1610
+
1611
+ // src/generator/route-types.ts
1612
+ var import_node_path9 = require("path");
1613
+ async function writeRouteTypes(paths, routes) {
1614
+ const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
1615
+ const lines = [
1616
+ GENERATED_HEADER,
1617
+ "export interface RouteParams {"
1618
+ ];
1619
+ for (const route of routes) {
1620
+ const typeFile = typeFilePath(paths, route.routeDir);
1621
+ lines.push(
1622
+ ` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
1623
+ importPath(file, typeFile)
1624
+ )}).Params;`
1625
+ );
1542
1626
  }
1543
- result.responses.sort((a, b) => Number(a.status) - Number(b.status));
1544
- result.warnings = ctx.warnings;
1545
- result.$defs = ctx.defs;
1546
- return result;
1627
+ lines.push("}", "");
1628
+ await writeGenerated(file, lines.join("\n"));
1547
1629
  }
1548
1630
 
1549
1631
  // src/generator/tsconfig.ts
@@ -1602,20 +1684,21 @@ async function typeFolders(paths, routes) {
1602
1684
  verbs: verbsByDir.get(slash(dir)) ?? []
1603
1685
  }));
1604
1686
  }
1605
- function extractResponses(paths, routes) {
1687
+ async function extractResponses(paths, routes) {
1606
1688
  const byFile = /* @__PURE__ */ new Map();
1607
1689
  if (routes.length === 0) {
1608
1690
  return byFile;
1609
1691
  }
1610
1692
  try {
1693
+ const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1611
1694
  const files = [...new Set(routes.map((route) => route.file))];
1612
1695
  const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1613
- const program = createSchemaProgram(
1696
+ const program = createSchemaProgram2(
1614
1697
  paths,
1615
1698
  (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1616
1699
  );
1617
1700
  for (const file of files) {
1618
- byFile.set(file, extractRouteResponses(program, file));
1701
+ byFile.set(file, extractRouteResponses2(program, file));
1619
1702
  }
1620
1703
  } catch (error) {
1621
1704
  console.warn(`giri: skipped response schema generation (${error.message}).`);
@@ -1657,7 +1740,7 @@ async function syncProject(config, options = {}) {
1657
1740
  await writeRouteTypes(paths, routes);
1658
1741
  await writeAppTypes(paths);
1659
1742
  await writeTsConfig(paths, config);
1660
- const responsesByFile = extractResponses(paths, routes);
1743
+ const responsesByFile = await extractResponses(paths, routes);
1661
1744
  const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1662
1745
  const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
1663
1746
  await writeManifest(paths, routes, data);