@boon4681/giri 0.0.1 → 0.0.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,1543 @@ 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
+ function createTypedResponse(data, status, format, headers) {
429
+ return {
430
+ [typedResponseBrand]: { data, status, format },
431
+ data,
432
+ status,
433
+ format,
434
+ headers
435
+ };
436
+ }
437
+ function isTypedResponse(value) {
438
+ return Boolean(value && typeof value === "object" && typedResponseBrand in value);
439
+ }
440
+ function createContext(options) {
441
+ const url = new URL(options.request.url);
442
+ const store = /* @__PURE__ */ new Map();
443
+ const validated = options.validated ?? {};
444
+ const context = {
445
+ params: options.params ?? {},
446
+ app: options.app ?? {},
447
+ req: {
448
+ raw: options.request,
449
+ url,
450
+ method: options.request.method,
451
+ header: (name) => options.request.headers.get(name),
452
+ json: () => options.request.json(),
453
+ text: () => options.request.text(),
454
+ arrayBuffer: () => options.request.arrayBuffer(),
455
+ formData: () => options.request.formData(),
456
+ valid: (key) => {
457
+ if (!(key in validated)) {
458
+ throw new Error(`No validated ${String(key)} data is available for this route.`);
459
+ }
460
+ return validated[key];
547
461
  }
548
- }
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
- }
559
- }
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
- );
565
- }
462
+ },
463
+ set: (key, value) => {
464
+ store.set(key, value);
465
+ },
466
+ get: (key) => store.get(key),
467
+ json: (data, status = 200, headers) => createTypedResponse(data, status, "json", headers),
468
+ text: (text, status = 200, headers) => createTypedResponse(text, status, "text", headers)
469
+ };
470
+ context[nativeContextBrand] = options.native;
471
+ return context;
566
472
  }
567
- function routeInput(routeModule, file) {
568
- const input = {};
569
- if (routeModule.body !== void 0) {
570
- assertBodySchema(routeModule.body, file);
571
- input.body = routeModule.body;
473
+ function typedResponseToResponse(response) {
474
+ const headers = new Headers(response.headers);
475
+ if (response.format === "json" && !headers.has("content-type")) {
476
+ headers.set("content-type", "application/json; charset=utf-8");
572
477
  }
573
- if (routeModule.query !== void 0) {
574
- assertQuerySchema(routeModule.query, file);
575
- input.query = routeModule.query;
478
+ if (response.format === "text" && !headers.has("content-type")) {
479
+ headers.set("content-type", "text/plain; charset=utf-8");
576
480
  }
577
- return input.body || input.query ? input : void 0;
578
- }
579
- function aliasValues(value) {
580
- return Array.isArray(value) ? value : [value];
481
+ const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
482
+ return new Response(body, {
483
+ status: response.status,
484
+ headers
485
+ });
581
486
  }
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);
487
+ function toResponse(response) {
488
+ return isTypedResponse(response) ? typedResponseToResponse(response) : response;
585
489
  }
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);
490
+ async function composeMiddleware(middleware, handle, context) {
491
+ let index = -1;
492
+ let result;
493
+ const dispatch = async (i) => {
494
+ if (i <= index) {
495
+ throw new Error("next() called multiple times in giri middleware.");
591
496
  }
592
- return void 0;
593
- }
594
- if (request === key) {
595
- return "";
596
- }
597
- const prefix = `${key}/`;
598
- if (request.startsWith(prefix)) {
599
- return request.slice(prefix.length);
600
- }
601
- return void 0;
602
- }
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;
497
+ index = i;
498
+ if (i === middleware.length) {
499
+ result = await handle(context);
500
+ return result;
608
501
  }
609
- const [target] = aliasValues(value);
610
- if (!target) {
611
- continue;
502
+ const returned = await middleware[i](context, () => dispatch(i + 1));
503
+ if (returned !== void 0) {
504
+ result = returned;
505
+ return returned;
612
506
  }
613
- return resolveAliasTarget(cwd, target, capture);
507
+ return result;
508
+ };
509
+ await dispatch(0);
510
+ if (result === void 0) {
511
+ throw new Error("Route completed without returning a response.");
614
512
  }
615
- return void 0;
513
+ return result;
616
514
  }
617
- function registerAliasResolver(alias, cwd) {
618
- if (!alias || Object.keys(alias).length === 0) {
619
- return () => {
620
- };
515
+ function defineMiddleware(optionsOrMiddleware, maybeMiddleware) {
516
+ if (typeof optionsOrMiddleware === "function") {
517
+ return optionsOrMiddleware;
621
518
  }
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;
519
+ if (!maybeMiddleware) {
520
+ throw new Error("defineMiddleware(options, middleware) requires a middleware function.");
644
521
  }
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
- };
522
+ maybeMiddleware.openapi = optionsOrMiddleware.openapi;
523
+ return maybeMiddleware;
659
524
  }
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 };
525
+ function stack(...middleware) {
526
+ return middleware;
691
527
  }
692
528
 
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;
529
+ // src/validation.ts
530
+ function defineInputSchema(schema) {
531
+ return { [inputSchemaBrand]: true, ...schema };
723
532
  }
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;
533
+ function isGiriInputSchema(value) {
534
+ return Boolean(
535
+ value && typeof value === "object" && value[inputSchemaBrand] === true
536
+ );
730
537
  }
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");
538
+ function defineBodySchema(contents) {
539
+ return { [bodySchemaBrand]: true, contents };
734
540
  }
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
- }
541
+ function isGiriBodySchema(value) {
542
+ return Boolean(
543
+ value && typeof value === "object" && value[bodySchemaBrand] === true
544
+ );
740
545
  }
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;
546
+ var MIME_TO_CONTENT_TYPE = {
547
+ "application/json": "json",
548
+ "multipart/form-data": "form",
549
+ "application/x-www-form-urlencoded": "urlencoded",
550
+ "text/plain": "text"
551
+ };
552
+ function contentTypeFromHeader(header) {
553
+ if (!header) {
554
+ return void 0;
745
555
  }
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);
556
+ const mime = header.split(";", 1)[0].trim().toLowerCase();
557
+ return MIME_TO_CONTENT_TYPE[mime];
749
558
  }
750
- async function writeJson(path, value) {
751
- await writeGenerated(path, `${JSON.stringify(value, null, 2)}
752
- `);
559
+ function formDataObject(form) {
560
+ const result = {};
561
+ form.forEach((value, key) => {
562
+ const current = result[key];
563
+ if (current === void 0) {
564
+ result[key] = value;
565
+ } else if (Array.isArray(current)) {
566
+ current.push(value);
567
+ } else {
568
+ result[key] = [current, value];
569
+ }
570
+ });
571
+ return result;
753
572
  }
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;
573
+ async function readRawBody(request, contentType) {
574
+ const cloned = request.clone();
575
+ if (contentType === "json") {
576
+ return cloned.json();
760
577
  }
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
- }
578
+ if (contentType === "text") {
579
+ return cloned.text();
771
580
  }
581
+ return formDataObject(await cloned.formData());
772
582
  }
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;
583
+ function queryObject(url) {
584
+ const result = {};
585
+ for (const [key, value] of url.searchParams) {
586
+ const current = result[key];
587
+ if (current === void 0) {
588
+ result[key] = value;
589
+ } else if (Array.isArray(current)) {
590
+ current.push(value);
591
+ } else {
592
+ result[key] = [current, value];
781
593
  }
782
594
  }
783
- return void 0;
595
+ return result;
784
596
  }
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}`;
597
+ async function runValidation(schema, value, label) {
598
+ if (!isGiriInputSchema(schema)) {
599
+ throw new Error(
600
+ `giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
601
+ );
789
602
  }
790
- return path;
603
+ return schema.validate(value);
791
604
  }
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;
605
+ async function prepareRequestInput(request, input) {
606
+ const validated = {};
607
+ if (input?.query) {
608
+ const query = queryObject(new URL(request.url));
609
+ const result = await runValidation(input.query, query, "query");
610
+ if (!result.ok) {
611
+ return {
612
+ ok: false,
613
+ response: createTypedResponse(
614
+ { message: "Invalid query parameters.", issues: result.issues },
615
+ 400,
616
+ "json"
617
+ )
618
+ };
619
+ }
620
+ validated.query = result.value;
798
621
  }
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);
622
+ if (input?.body) {
623
+ const contents = input.body.contents;
624
+ const declared = Object.keys(contents);
625
+ const requested = contentTypeFromHeader(request.headers.get("content-type"));
626
+ const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
627
+ if (!chosen) {
830
628
  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 } : {}
629
+ ok: false,
630
+ response: createTypedResponse(
631
+ { message: "Unsupported media type.", issues: { accepted: declared } },
632
+ 415,
633
+ "json"
634
+ )
842
635
  };
843
- })
844
- };
845
- await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
636
+ }
637
+ let rawBody;
638
+ try {
639
+ rawBody = await readRawBody(request, chosen);
640
+ } catch (error) {
641
+ return {
642
+ ok: false,
643
+ response: createTypedResponse(
644
+ { message: "Invalid request body.", issues: error },
645
+ 400,
646
+ "json"
647
+ )
648
+ };
649
+ }
650
+ const result = await runValidation(contents[chosen], rawBody, "body");
651
+ if (!result.ok) {
652
+ return {
653
+ ok: false,
654
+ response: createTypedResponse(
655
+ { message: "Invalid request body.", issues: result.issues },
656
+ 400,
657
+ "json"
658
+ )
659
+ };
660
+ }
661
+ validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
662
+ }
663
+ return { ok: true, validated };
846
664
  }
847
665
 
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);
666
+ // src/app.ts
667
+ var import_node_module = __toESM(require("module"));
668
+ var import_node_path3 = require("path");
669
+
670
+ // src/loader/loader.ts
671
+ var import_prompts = require("@clack/prompts");
672
+ var import_node_fs = require("fs");
673
+ var import_node_path = require("path");
674
+ var import_node_process = require("process");
675
+
676
+ // src/config/schema.ts
677
+ var import_typebox = require("@sinclair/typebox");
678
+ var configSchema = import_typebox.Type.Object({
679
+ adapter: import_typebox.Type.Any(),
680
+ alias: import_typebox.Type.Optional(import_typebox.Type.Record(
681
+ import_typebox.Type.String(),
682
+ import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
683
+ )),
684
+ outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
685
+ server: import_typebox.Type.Optional(import_typebox.Type.Object({
686
+ port: import_typebox.Type.Optional(import_typebox.Type.Number()),
687
+ hostname: import_typebox.Type.Optional(import_typebox.Type.String())
688
+ }, { additionalProperties: false })),
689
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
690
+ }, { additionalProperties: false });
691
+
692
+ // src/loader/loader.ts
693
+ var import_value = require("@sinclair/typebox/value");
694
+ var assertES5 = async (unregister) => {
695
+ try {
696
+ init_es5();
697
+ } catch (e) {
698
+ if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
699
+ const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
700
+ if (es5Error) {
701
+ import_prompts.log.error(
702
+ `Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
703
+ );
704
+ (0, import_node_process.exit)(1);
705
+ }
706
+ }
707
+ import_prompts.log.error(e);
708
+ (0, import_node_process.exit)(1);
870
709
  }
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);
710
+ };
711
+ var safeRegister = async () => {
712
+ const { register } = await import("esbuild-register/dist/node");
713
+ let res;
714
+ try {
715
+ res = register({
716
+ format: "cjs",
717
+ loader: "ts"
718
+ });
719
+ } catch {
720
+ res = {
721
+ unregister: () => {
878
722
  }
879
- }
880
- return out;
723
+ };
881
724
  }
882
- return value;
725
+ await assertES5(res.unregister);
726
+ return res;
727
+ };
728
+
729
+ // src/routes.ts
730
+ var import_node_fs2 = require("fs");
731
+ var import_promises = require("fs/promises");
732
+ var import_node_path2 = require("path");
733
+ var import_tinyglobby = require("tinyglobby");
734
+ var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
735
+ var METHOD_FROM_FILE = new Map(
736
+ METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
737
+ );
738
+ function normalizeSlashes(path) {
739
+ return path.split(import_node_path2.sep).join("/");
883
740
  }
884
- function mediaTypeFor(format) {
885
- return format === "text" ? "text/plain" : "application/json";
741
+ function isRouteSourceFile(fileName) {
742
+ return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
886
743
  }
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
- };
744
+ function methodFromFile(fileName) {
745
+ if (!isRouteSourceFile(fileName)) {
746
+ return void 0;
905
747
  }
906
- return out;
748
+ const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
749
+ return METHOD_FROM_FILE.get(stem);
907
750
  }
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;
751
+ function sharedFileIn(dir) {
752
+ for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
753
+ const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
754
+ if ((0, import_node_fs2.existsSync)(file)) {
755
+ return file;
914
756
  }
915
- seen.add(param.name);
916
- params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
917
757
  }
918
- return params;
758
+ return void 0;
919
759
  }
920
- function queryParameters(query) {
921
- if (!query || query.type !== "object" || typeof query.properties !== "object") {
760
+ function physicalRouteSegments(routesDir, routeDir) {
761
+ const rel = (0, import_node_path2.relative)(routesDir, routeDir);
762
+ if (!rel) {
922
763
  return [];
923
764
  }
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
- }));
765
+ return normalizeSlashes(rel).split("/").filter(Boolean);
932
766
  }
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
- }
767
+ function urlSegment(segment) {
768
+ if (/^\(.+\)$/.test(segment)) {
769
+ return {};
941
770
  }
942
- return { title: "giri API", version: "0.0.0" };
771
+ const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
772
+ if (catchAll) {
773
+ const name = catchAll[1];
774
+ return {
775
+ value: `:${name}{.*}`,
776
+ param: { name, catchAll: true }
777
+ };
778
+ }
779
+ const param = /^\[(.+)\]$/.exec(segment);
780
+ if (param) {
781
+ const name = param[1];
782
+ return {
783
+ value: `:${name}`,
784
+ param: { name, catchAll: false }
785
+ };
786
+ }
787
+ return { value: segment };
943
788
  }
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;
789
+ function pathFromSegments(segments) {
790
+ const pathSegments = [];
791
+ const params = [];
792
+ for (const segment of segments) {
793
+ const converted = urlSegment(segment);
794
+ if (converted.value) {
795
+ pathSegments.push(converted.value);
976
796
  }
977
- if (security) {
978
- Object.assign(securitySchemes, security.securitySchemes);
797
+ if (converted.param) {
798
+ params.push(converted.param);
979
799
  }
980
- const openApiPath = toOpenApiPath(route.path);
981
- const pathItem = documentPaths[openApiPath] ?? {};
982
- pathItem[route.method.toLowerCase()] = operation;
983
- documentPaths[openApiPath] = pathItem;
984
800
  }
985
- const document = {
986
- openapi: "3.1.0",
987
- info: readProjectInfo(paths.cwd),
988
- paths: documentPaths
801
+ return {
802
+ path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
803
+ params
989
804
  };
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
805
  }
1021
- function varsType(typesDir, sharedFiles) {
1022
- if (sharedFiles.length === 0) {
1023
- return "{}";
806
+ async function scanRouteFolders(routesDir) {
807
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
808
+ return [];
1024
809
  }
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));
810
+ const folders = [routesDir];
811
+ const walk = async (dir) => {
812
+ for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
813
+ if (entry.isDirectory() && entry.name !== "node_modules") {
814
+ const full = (0, import_node_path2.join)(dir, entry.name);
815
+ folders.push(full);
816
+ await walk(full);
817
+ }
1054
818
  }
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;
819
+ };
820
+ await walk(routesDir);
821
+ return folders;
1065
822
  }
1066
- function inputToJsonSchema(schema) {
1067
- if (!isGiriInputSchema(schema)) {
1068
- return void 0;
823
+ function routeParamsForDir(routesDir, dir) {
824
+ return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
825
+ }
826
+ function sharedFilesForDir(routesDir, dir) {
827
+ const segments = physicalRouteSegments(routesDir, dir);
828
+ const dirs = [routesDir];
829
+ let current = routesDir;
830
+ for (const segment of segments) {
831
+ current = (0, import_node_path2.join)(current, segment);
832
+ dirs.push(current);
1069
833
  }
1070
- return sanitize(schema.toJsonSchema());
834
+ return dirs.map(sharedFileIn).filter((file) => Boolean(file));
1071
835
  }
1072
- function bodyToJsonSchemas(value) {
1073
- if (!isGiriBodySchema(value)) {
1074
- return void 0;
836
+ async function scanRoutes(routesDir) {
837
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
838
+ return [];
1075
839
  }
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;
840
+ const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
841
+ cwd: routesDir,
842
+ absolute: true,
843
+ onlyFiles: true
844
+ });
845
+ const routes = [];
846
+ for (const file of files) {
847
+ const method = methodFromFile((0, import_node_path2.basename)(file));
848
+ if (!method) {
849
+ continue;
1081
850
  }
851
+ const routeDir = (0, import_node_path2.dirname)(file);
852
+ const routeSegments = physicalRouteSegments(routesDir, routeDir);
853
+ const { path, params } = pathFromSegments(routeSegments);
854
+ routes.push({
855
+ method,
856
+ path,
857
+ file,
858
+ routeDir,
859
+ routeSegments,
860
+ params,
861
+ sharedFiles: sharedFilesForDir(routesDir, routeDir)
862
+ });
1082
863
  }
1083
- return Object.keys(out).length > 0 ? out : void 0;
864
+ return routes.sort((left, right) => {
865
+ const pathOrder = left.path.localeCompare(right.path);
866
+ if (pathOrder !== 0) {
867
+ return pathOrder;
868
+ }
869
+ return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
870
+ });
1084
871
  }
1085
872
 
1086
- // src/generator/route-meta.ts
1087
- function loadModule2(file) {
873
+ // src/app.ts
874
+ function loadModule(file) {
1088
875
  const resolved = require.resolve(file);
1089
876
  delete require.cache[resolved];
1090
877
  return require(resolved);
1091
878
  }
1092
- function interopDefault2(value) {
879
+ function interopDefault(value) {
1093
880
  if (value && typeof value === "object" && "default" in value) {
1094
881
  return value.default;
1095
882
  }
1096
883
  return value;
1097
884
  }
1098
- function middlewareFunctions(value) {
1099
- const exported = interopDefault2(value);
885
+ function normalizeMiddleware(value, file) {
886
+ const exported = interopDefault(value);
887
+ if (exported === void 0) {
888
+ return [];
889
+ }
1100
890
  if (typeof exported === "function") {
1101
891
  return [exported];
1102
892
  }
1103
893
  if (Array.isArray(exported)) {
1104
- return exported.filter((fn) => typeof fn === "function");
894
+ for (const middleware of exported) {
895
+ if (typeof middleware !== "function") {
896
+ throw new Error(`Middleware export in ${file} must contain only functions.`);
897
+ }
898
+ }
899
+ return exported;
1105
900
  }
1106
- return [];
901
+ throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
1107
902
  }
1108
- function readInput(routeModule) {
903
+ function assertBodySchema(value, file) {
904
+ if (!isGiriBodySchema(value)) {
905
+ throw new Error(
906
+ `${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
907
+ );
908
+ }
909
+ }
910
+ function assertQuerySchema(value, file) {
911
+ if (!isGiriInputSchema(value)) {
912
+ throw new Error(
913
+ `${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
914
+ );
915
+ }
916
+ }
917
+ function routeInput(routeModule, file) {
1109
918
  const input = {};
1110
- const body = bodyToJsonSchemas(routeModule.body);
1111
- const query = inputToJsonSchema(routeModule.query);
1112
- if (body) {
1113
- input.body = body;
919
+ if (routeModule.body !== void 0) {
920
+ assertBodySchema(routeModule.body, file);
921
+ input.body = routeModule.body;
1114
922
  }
1115
- if (query) {
1116
- input.query = query;
923
+ if (routeModule.query !== void 0) {
924
+ assertQuerySchema(routeModule.query, file);
925
+ input.query = routeModule.query;
1117
926
  }
1118
927
  return input.body || input.query ? input : void 0;
1119
928
  }
1120
- function hiddenFrom(value) {
1121
- if (value === false) {
1122
- return true;
929
+ function aliasValues(value) {
930
+ return Array.isArray(value) ? value : [value];
931
+ }
932
+ function resolveAliasTarget(cwd, target, capture = "") {
933
+ const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
934
+ return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
935
+ }
936
+ function matchAlias(request, key) {
937
+ if (key.includes("*")) {
938
+ const [prefix2, suffix = ""] = key.split("*");
939
+ if (request.startsWith(prefix2) && request.endsWith(suffix)) {
940
+ return request.slice(prefix2.length, request.length - suffix.length);
941
+ }
942
+ return void 0;
1123
943
  }
1124
- if (value === true) {
1125
- return false;
944
+ if (request === key) {
945
+ return "";
1126
946
  }
1127
- if (value && typeof value === "object" && "hidden" in value) {
1128
- return Boolean(value.hidden);
947
+ const prefix = `${key}/`;
948
+ if (request.startsWith(prefix)) {
949
+ return request.slice(prefix.length);
1129
950
  }
1130
951
  return void 0;
1131
952
  }
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;
953
+ function resolveAliasRequest(request, alias, cwd) {
954
+ for (const [key, value] of Object.entries(alias ?? {})) {
955
+ const capture = matchAlias(request, key);
956
+ if (capture === void 0) {
957
+ continue;
958
+ }
959
+ const [target] = aliasValues(value);
960
+ if (!target) {
961
+ continue;
1138
962
  }
963
+ return resolveAliasTarget(cwd, target, capture);
1139
964
  }
1140
- const verb = hiddenFrom(routeModule.openapi);
1141
- return verb ?? hidden;
965
+ return void 0;
1142
966
  }
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
- }
967
+ function registerAliasResolver(alias, cwd) {
968
+ if (!alias || Object.keys(alias).length === 0) {
969
+ return () => {
970
+ };
1152
971
  }
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
- }
972
+ const moduleWithResolver = import_node_module.default;
973
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
974
+ moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
975
+ return originalResolveFilename.call(
976
+ this,
977
+ resolveAliasRequest(request, alias, cwd) ?? request,
978
+ parent,
979
+ isMain,
980
+ options
981
+ );
982
+ };
983
+ return () => {
984
+ moduleWithResolver._resolveFilename = originalResolveFilename;
985
+ };
986
+ }
987
+ var GIRI_ALIAS_PREFIX = "$giri/";
988
+ var giriOutDir;
989
+ var giriResolverInstalled = false;
990
+ function ensureGiriAliasResolver(outDir) {
991
+ giriOutDir = outDir;
992
+ if (giriResolverInstalled) {
993
+ return;
1168
994
  }
1169
- return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
995
+ giriResolverInstalled = true;
996
+ const moduleWithResolver = import_node_module.default;
997
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
998
+ moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
999
+ const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
1000
+ return originalResolveFilename.call(this, mapped, parent, isMain, options);
1001
+ };
1170
1002
  }
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);
1003
+ function resolveGiriPaths(config, cwd = process.cwd()) {
1004
+ return {
1005
+ cwd: (0, import_node_path3.resolve)(cwd),
1006
+ routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
1007
+ outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
1185
1008
  };
1009
+ }
1010
+ async function buildGiriApp(config, options = {}) {
1011
+ const paths = resolveGiriPaths(config, options.cwd);
1012
+ const routes = await scanRoutes(paths.routesDir);
1013
+ const app = config.adapter.createApp();
1014
+ ensureGiriAliasResolver(paths.outDir);
1015
+ const { unregister } = await safeRegister();
1016
+ const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
1186
1017
  try {
1187
1018
  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 {
1019
+ const routeModule = loadModule(route.file);
1020
+ if (typeof routeModule.handle !== "function") {
1021
+ throw new Error(`${route.file} must export a named handle function.`);
1207
1022
  }
1023
+ const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
1024
+ (file) => normalizeMiddleware(loadModule(file).middleware, file)
1025
+ );
1026
+ const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
1027
+ config.adapter.register(app, {
1028
+ method: route.method,
1029
+ path: route.path,
1030
+ handle: routeModule.handle,
1031
+ middleware: [...folderMiddleware, ...verbMiddleware],
1032
+ input: routeInput(routeModule, route.file),
1033
+ services: options.services
1034
+ });
1208
1035
  }
1209
1036
  } finally {
1210
- unregisterAlias();
1037
+ unregisterAliasResolver();
1211
1038
  unregister();
1212
1039
  }
1213
- return byFile;
1040
+ return { app, routes, paths };
1214
1041
  }
1215
1042
 
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
- );
1043
+ // src/generator/sync.ts
1044
+ var import_node_fs6 = require("fs");
1045
+ var import_promises3 = require("fs/promises");
1046
+ var import_node_path11 = require("path");
1047
+
1048
+ // src/generator/app-types.ts
1049
+ var import_node_fs4 = require("fs");
1050
+ var import_node_path5 = require("path");
1051
+
1052
+ // src/generator/util.ts
1053
+ var import_node_fs3 = require("fs");
1054
+ var import_promises2 = require("fs/promises");
1055
+ var import_node_path4 = require("path");
1056
+ var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
1057
+ function slash(path) {
1058
+ return path.split(import_node_path4.sep).join("/");
1059
+ }
1060
+ function importPath(fromFile, toFile) {
1061
+ let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
1062
+ if (!path.startsWith(".")) {
1063
+ path = `./${path}`;
1231
1064
  }
1232
- lines.push("}", "");
1233
- await writeGenerated(file, lines.join("\n"));
1065
+ return path;
1234
1066
  }
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 };
1067
+ function relativeConfigPath(fromDir, toPath) {
1068
+ let path = slash((0, import_node_path4.relative)(fromDir, toPath));
1069
+ if (!path.startsWith(".")) {
1070
+ path = `./${path}`;
1071
+ }
1072
+ return path;
1073
+ }
1074
+ function moduleSpecifier(fromDir, target) {
1075
+ let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
1076
+ if (!path.startsWith(".")) {
1077
+ path = `./${path}`;
1078
+ }
1079
+ return path;
1080
+ }
1081
+ function typeFilePath(paths, routeDir) {
1082
+ const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
1083
+ return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
1084
+ }
1085
+ function assertSafeOutDir(paths) {
1086
+ const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
1087
+ if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
1088
+ throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
1089
+ }
1090
+ }
1091
+ var writeCache = /* @__PURE__ */ new Map();
1092
+ async function writeGenerated(path, content) {
1093
+ if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
1094
+ return;
1095
+ }
1096
+ await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
1097
+ await (0, import_promises2.writeFile)(path, content);
1098
+ writeCache.set(path, content);
1099
+ }
1100
+ async function writeJson(path, value) {
1101
+ await writeGenerated(path, `${JSON.stringify(value, null, 2)}
1102
+ `);
1103
+ }
1104
+ async function pruneDir(dir, keep) {
1105
+ let entries;
1106
+ try {
1107
+ entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
1108
+ } catch {
1109
+ return;
1110
+ }
1111
+ for (const entry of entries) {
1112
+ const full = (0, import_node_path4.join)(dir, entry.name);
1113
+ if (entry.isDirectory()) {
1114
+ await pruneDir(full, keep);
1115
+ await (0, import_promises2.rmdir)(full).catch(() => {
1116
+ });
1117
+ } else if (!keep.has(full)) {
1118
+ await (0, import_promises2.rm)(full, { force: true });
1119
+ writeCache.delete(full);
1257
1120
  }
1258
1121
  }
1259
- return import_typescript.default.createProgram(routeFiles, options);
1260
1122
  }
1261
1123
 
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
- };
1124
+ // src/generator/app-types.ts
1125
+ var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1126
+ function findMainFile(cwd) {
1127
+ for (const ext of MAIN_EXTENSIONS) {
1128
+ const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
1129
+ if ((0, import_node_fs4.existsSync)(file)) {
1130
+ return file;
1131
+ }
1132
+ }
1133
+ return void 0;
1276
1134
  }
1277
- function typeId(type) {
1278
- return type.id;
1135
+ function moduleSpecifier2(fromDir, target) {
1136
+ let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
1137
+ if (!path.startsWith(".")) {
1138
+ path = `./${path}`;
1139
+ }
1140
+ return path;
1279
1141
  }
1280
- function intrinsicName(type) {
1281
- return type.intrinsicName;
1142
+ async function writeAppTypes(paths) {
1143
+ const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
1144
+ const mainFile = findMainFile(paths.cwd);
1145
+ if (!mainFile) {
1146
+ await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
1147
+ return;
1148
+ }
1149
+ const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
1150
+ await writeGenerated(
1151
+ file,
1152
+ [
1153
+ GENERATED_HEADER,
1154
+ "declare global {",
1155
+ " namespace Giri {",
1156
+ " interface Register {",
1157
+ ` app: typeof import(${JSON.stringify(spec)}) extends {`,
1158
+ " init: (...args: any[]) => infer R;",
1159
+ " }",
1160
+ " ? Awaited<R>",
1161
+ " : Record<string, unknown>;",
1162
+ " }",
1163
+ " }",
1164
+ "}",
1165
+ "export {};",
1166
+ ""
1167
+ ].join("\n")
1168
+ );
1282
1169
  }
1283
- function isDateType(type) {
1284
- const symbol = type.getSymbol() ?? type.aliasSymbol;
1285
- return symbol?.getName() === "Date";
1170
+
1171
+ // src/generator/manifest.ts
1172
+ var import_node_path6 = require("path");
1173
+ async function writeManifest(paths, routes, data = {}) {
1174
+ const manifest = {
1175
+ version: 1,
1176
+ routes: routes.map((route) => {
1177
+ const responses = data.responsesByFile?.get(route.file);
1178
+ const input = data.inputsByFile?.get(route.file);
1179
+ const security = data.securityByFile?.get(route.file);
1180
+ return {
1181
+ method: route.method,
1182
+ path: route.path,
1183
+ file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
1184
+ params: route.params,
1185
+ shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
1186
+ types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
1187
+ ...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
1188
+ ...input ? { input } : {},
1189
+ ...security && security.security.length > 0 ? { security: security.security } : {},
1190
+ responses: responses?.responses ?? [],
1191
+ ...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
1192
+ };
1193
+ })
1194
+ };
1195
+ await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
1286
1196
  }
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;
1197
+
1198
+ // src/generator/openapi.ts
1199
+ var import_node_fs5 = require("fs");
1200
+ var import_node_path7 = require("path");
1201
+ var REASON = {
1202
+ 200: "OK",
1203
+ 201: "Created",
1204
+ 202: "Accepted",
1205
+ 204: "No Content",
1206
+ 400: "Bad Request",
1207
+ 401: "Unauthorized",
1208
+ 403: "Forbidden",
1209
+ 404: "Not Found",
1210
+ 409: "Conflict",
1211
+ 422: "Unprocessable Entity",
1212
+ 500: "Internal Server Error"
1213
+ };
1214
+ function toOpenApiPath(path) {
1215
+ return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
1216
+ }
1217
+ function rewriteRefs(value) {
1218
+ if (Array.isArray(value)) {
1219
+ return value.map(rewriteRefs);
1220
+ }
1221
+ if (value && typeof value === "object") {
1222
+ const out = {};
1223
+ for (const [key, child] of Object.entries(value)) {
1224
+ if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
1225
+ out.$ref = child.replace("#/$defs/", "#/components/schemas/");
1226
+ } else {
1227
+ out[key] = rewriteRefs(child);
1228
+ }
1296
1229
  }
1230
+ return out;
1297
1231
  }
1298
- return values;
1232
+ return value;
1299
1233
  }
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);
1234
+ function mediaTypeFor(format) {
1235
+ return format === "text" ? "text/plain" : "application/json";
1236
+ }
1237
+ var BODY_MEDIA_TYPE = {
1238
+ json: "application/json",
1239
+ form: "multipart/form-data",
1240
+ urlencoded: "application/x-www-form-urlencoded",
1241
+ text: "text/plain"
1242
+ };
1243
+ function buildResponses(responses) {
1244
+ if (responses.length === 0) {
1245
+ return { default: { description: "Response" } };
1305
1246
  }
1306
- const enumValues = literalValuesOf(members);
1307
- if (enumValues) {
1308
- return { enum: enumValues };
1247
+ const out = {};
1248
+ for (const response of responses) {
1249
+ const key = response.status === "default" ? "default" : String(response.status);
1250
+ const description = typeof response.status === "number" && REASON[response.status] || "Response";
1251
+ out[key] = {
1252
+ description,
1253
+ content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
1254
+ };
1309
1255
  }
1310
- return { anyOf: members.map((member) => walkType(member, ctx)) };
1256
+ return out;
1311
1257
  }
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);
1258
+ function pathParameters(route) {
1259
+ const seen = /* @__PURE__ */ new Set();
1260
+ const params = [];
1261
+ for (const param of route.params) {
1262
+ if (seen.has(param.name)) {
1263
+ continue;
1324
1264
  }
1265
+ seen.add(param.name);
1266
+ params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
1325
1267
  }
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;
1268
+ return params;
1339
1269
  }
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;
1270
+ function queryParameters(query) {
1271
+ if (!query || query.type !== "object" || typeof query.properties !== "object") {
1272
+ return [];
1345
1273
  }
1346
- return `Anonymous${typeId(type)}`;
1274
+ const properties = query.properties;
1275
+ const required = Array.isArray(query.required) ? query.required : [];
1276
+ return Object.entries(properties).map(([name, schema]) => ({
1277
+ name,
1278
+ in: "query",
1279
+ required: required.includes(name),
1280
+ schema: rewriteRefs(schema)
1281
+ }));
1347
1282
  }
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}` };
1283
+ function readProjectInfo(cwd) {
1284
+ const file = (0, import_node_path7.join)(cwd, "package.json");
1285
+ if ((0, import_node_fs5.existsSync)(file)) {
1286
+ try {
1287
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
1288
+ return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
1289
+ } catch {
1290
+ }
1374
1291
  }
1375
- return schema;
1292
+ return { title: "giri API", version: "0.0.0" };
1376
1293
  }
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" };
1294
+ function buildOpenApiDocument(paths, routes, data = {}) {
1295
+ const documentPaths = {};
1296
+ const schemas = {};
1297
+ const securitySchemes = {};
1298
+ for (const route of routes) {
1299
+ if (data.hiddenFiles?.has(route.file)) {
1300
+ continue;
1301
+ }
1302
+ const responses = data.responsesByFile?.get(route.file);
1303
+ const input = data.inputsByFile?.get(route.file);
1304
+ const security = data.securityByFile?.get(route.file);
1305
+ for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1306
+ schemas[name] = rewriteRefs(schema);
1307
+ }
1308
+ const operation = { responses: buildResponses(responses?.responses ?? []) };
1309
+ const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1310
+ if (parameters.length > 0) {
1311
+ operation.parameters = parameters;
1312
+ }
1313
+ if (input?.body) {
1314
+ const content = {};
1315
+ for (const [contentType, schema] of Object.entries(input.body)) {
1316
+ content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
1317
+ schema: rewriteRefs(schema)
1318
+ };
1319
+ }
1320
+ if (Object.keys(content).length > 0) {
1321
+ operation.requestBody = { required: true, content };
1322
+ }
1323
+ }
1324
+ if (security && security.security.length > 0) {
1325
+ operation.security = security.security;
1326
+ }
1327
+ if (security) {
1328
+ Object.assign(securitySchemes, security.securitySchemes);
1329
+ }
1330
+ const openApiPath = toOpenApiPath(route.path);
1331
+ const pathItem = documentPaths[openApiPath] ?? {};
1332
+ pathItem[route.method.toLowerCase()] = operation;
1333
+ documentPaths[openApiPath] = pathItem;
1384
1334
  }
1385
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
1386
- return {};
1335
+ const document = {
1336
+ openapi: "3.1.0",
1337
+ info: readProjectInfo(paths.cwd),
1338
+ paths: documentPaths
1339
+ };
1340
+ const components = {};
1341
+ if (Object.keys(schemas).length > 0) {
1342
+ components.schemas = schemas;
1387
1343
  }
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" };
1344
+ if (Object.keys(securitySchemes).length > 0) {
1345
+ components.securitySchemes = securitySchemes;
1391
1346
  }
1392
- if (type.isStringLiteral()) {
1393
- return { type: "string", const: type.value };
1347
+ if (Object.keys(components).length > 0) {
1348
+ document.components = components;
1394
1349
  }
1395
- if (type.isNumberLiteral()) {
1396
- return { type: "number", const: type.value };
1350
+ return document;
1351
+ }
1352
+ async function writeOpenApi(paths, routes, data = {}) {
1353
+ await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
1354
+ }
1355
+
1356
+ // src/generator/param-types.ts
1357
+ var import_node_path8 = require("path");
1358
+ function paramsType(params) {
1359
+ if (params.length === 0) {
1360
+ return "{}";
1397
1361
  }
1398
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
1399
- return { type: "boolean", const: intrinsicName(type) === "true" };
1362
+ const unique = /* @__PURE__ */ new Map();
1363
+ for (const param of params) {
1364
+ unique.set(param.name, param);
1400
1365
  }
1401
- if (flags & import_typescript2.default.TypeFlags.String) {
1402
- return { type: "string" };
1366
+ const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
1367
+ return `{
1368
+ ${fields}
1369
+ }`;
1370
+ }
1371
+ function varsType(typesDir, sharedFiles) {
1372
+ if (sharedFiles.length === 0) {
1373
+ return "{}";
1403
1374
  }
1404
- if (flags & import_typescript2.default.TypeFlags.Number) {
1405
- return { type: "number" };
1375
+ return sharedFiles.map((file) => {
1376
+ const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1377
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1378
+ }).join("\n & ");
1379
+ }
1380
+ function methodExports(typesDir, verbs) {
1381
+ return verbs.map(({ method, file }) => {
1382
+ const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1383
+ const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
1384
+ const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
1385
+ return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
1386
+ });
1387
+ }
1388
+ async function writeParamTypes(paths, folders) {
1389
+ for (const { dir, params, sharedFiles, verbs } of folders) {
1390
+ const file = typeFilePath(paths, dir);
1391
+ const typesDir = (0, import_node_path8.dirname)(file);
1392
+ const lines = [
1393
+ GENERATED_HEADER,
1394
+ `export type Params = ${paramsType(params)};`,
1395
+ "export type RouteParams = Params;",
1396
+ `type Vars = ${varsType(typesDir, sharedFiles)};`,
1397
+ "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1398
+ ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1399
+ 'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
1400
+ ' import("@boon4681/giri").Handle<Params, Input, Vars>;'
1401
+ ];
1402
+ if (verbs.length > 0) {
1403
+ lines.push(...methodExports(typesDir, verbs));
1404
+ }
1405
+ lines.push("");
1406
+ await writeGenerated(file, lines.join("\n"));
1406
1407
  }
1407
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
1408
- return { type: "boolean" };
1408
+ }
1409
+
1410
+ // src/generator/inputs.ts
1411
+ function sanitize(schema) {
1412
+ const { $schema, ...rest } = schema;
1413
+ void $schema;
1414
+ return rest;
1415
+ }
1416
+ function inputToJsonSchema(schema) {
1417
+ if (!isGiriInputSchema(schema)) {
1418
+ return void 0;
1409
1419
  }
1410
- if (type.isUnion()) {
1411
- return walkUnion(type, ctx);
1420
+ return sanitize(schema.toJsonSchema());
1421
+ }
1422
+ function bodyToJsonSchemas(value) {
1423
+ if (!isGiriBodySchema(value)) {
1424
+ return void 0;
1412
1425
  }
1413
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
1414
- return walkObject(type, ctx);
1426
+ const out = {};
1427
+ for (const [contentType, schema] of Object.entries(value.contents)) {
1428
+ const json = inputToJsonSchema(schema);
1429
+ if (json) {
1430
+ out[contentType] = json;
1431
+ }
1415
1432
  }
1416
- return {};
1433
+ return Object.keys(out).length > 0 ? out : void 0;
1417
1434
  }
1418
1435
 
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
- }
1436
+ // src/generator/route-meta.ts
1437
+ function loadModule2(file) {
1438
+ const resolved = require.resolve(file);
1439
+ delete require.cache[resolved];
1440
+ return require(resolved);
1441
+ }
1442
+ function interopDefault2(value) {
1443
+ if (value && typeof value === "object" && "default" in value) {
1444
+ return value.default;
1434
1445
  }
1435
- return found;
1446
+ return value;
1436
1447
  }
1437
- function collectReturnExpressions(fn) {
1438
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
1439
- return [fn.body];
1448
+ function middlewareFunctions(value) {
1449
+ const exported = interopDefault2(value);
1450
+ if (typeof exported === "function") {
1451
+ return [exported];
1440
1452
  }
1441
- if (!fn.body) {
1442
- return [];
1453
+ if (Array.isArray(exported)) {
1454
+ return exported.filter((fn) => typeof fn === "function");
1443
1455
  }
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
- );
1456
+ return [];
1465
1457
  }
1466
- function readFromCall(checker, expression) {
1467
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
1468
- return void 0;
1458
+ function readInput(routeModule) {
1459
+ const input = {};
1460
+ const body = bodyToJsonSchemas(routeModule.body);
1461
+ const query = inputToJsonSchema(routeModule.query);
1462
+ if (body) {
1463
+ input.body = body;
1469
1464
  }
1470
- const method = expression.expression.name.text;
1471
- if (method !== "json" && method !== "text") {
1472
- return void 0;
1465
+ if (query) {
1466
+ input.query = query;
1473
1467
  }
1474
- if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
1475
- return void 0;
1468
+ return input.body || input.query ? input : void 0;
1469
+ }
1470
+ function hiddenFrom(value) {
1471
+ if (value === false) {
1472
+ return true;
1476
1473
  }
1477
- const [dataArg, statusArg] = expression.arguments;
1478
- if (!dataArg) {
1479
- return void 0;
1474
+ if (value === true) {
1475
+ return false;
1480
1476
  }
1481
- let status = 200;
1482
- if (statusArg) {
1483
- const statusType = checker.getTypeAtLocation(statusArg);
1484
- status = statusType.isNumberLiteral() ? statusType.value : "default";
1477
+ if (value && typeof value === "object" && "hidden" in value) {
1478
+ return Boolean(value.hidden);
1485
1479
  }
1486
- return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
1480
+ return void 0;
1487
1481
  }
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;
1482
+ function collectHidden(route, routeModule, loadShared) {
1483
+ let hidden = false;
1484
+ for (const file of route.sharedFiles) {
1485
+ const opinion = hiddenFrom(loadShared(file).openapi);
1486
+ if (opinion !== void 0) {
1487
+ hidden = opinion;
1488
+ }
1494
1489
  }
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];
1490
+ const verb = hiddenFrom(routeModule.openapi);
1491
+ return verb ?? hidden;
1501
1492
  }
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;
1493
+ function collectSecurity(route, routeModule, loadShared) {
1494
+ const skipInherited = Boolean(
1495
+ routeModule.config?.skipInherited
1496
+ );
1497
+ const middleware = [];
1498
+ if (!skipInherited) {
1499
+ for (const file of route.sharedFiles) {
1500
+ middleware.push(...middlewareFunctions(loadShared(file).middleware));
1501
+ }
1512
1502
  }
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;
1503
+ middleware.push(...middlewareFunctions(routeModule.middleware));
1504
+ const security = [];
1505
+ const securitySchemes = {};
1506
+ for (const fn of middleware) {
1507
+ const openapi = fn.openapi;
1508
+ if (openapi?.security) {
1509
+ for (const requirement of openapi.security) {
1510
+ if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
1511
+ security.push(requirement);
1512
+ }
1513
+ }
1526
1514
  }
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;
1515
+ if (openapi?.securitySchemes) {
1516
+ Object.assign(securitySchemes, openapi.securitySchemes);
1517
+ }
1518
+ }
1519
+ return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
1520
+ }
1521
+ async function extractRouteMeta(config, paths, routes) {
1522
+ const byFile = /* @__PURE__ */ new Map();
1523
+ const { unregister } = await safeRegister();
1524
+ const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1525
+ const sharedCache = /* @__PURE__ */ new Map();
1526
+ const loadShared = (file) => {
1527
+ if (!sharedCache.has(file)) {
1528
+ try {
1529
+ sharedCache.set(file, loadModule2(file));
1530
+ } catch {
1531
+ sharedCache.set(file, {});
1533
1532
  }
1534
1533
  }
1535
- if (!matched) {
1536
- result.opaque = true;
1534
+ return sharedCache.get(file);
1535
+ };
1536
+ try {
1537
+ for (const route of routes) {
1538
+ try {
1539
+ const routeModule = loadModule2(route.file);
1540
+ const meta = {};
1541
+ const input = readInput(routeModule);
1542
+ const security = collectSecurity(route, routeModule, loadShared);
1543
+ const hidden = collectHidden(route, routeModule, loadShared);
1544
+ if (input) {
1545
+ meta.input = input;
1546
+ }
1547
+ if (security) {
1548
+ meta.security = security;
1549
+ }
1550
+ if (hidden) {
1551
+ meta.hidden = true;
1552
+ }
1553
+ if (meta.input || meta.security || meta.hidden) {
1554
+ byFile.set(route.file, meta);
1555
+ }
1556
+ } catch {
1557
+ }
1537
1558
  }
1559
+ } finally {
1560
+ unregisterAlias();
1561
+ unregister();
1538
1562
  }
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 });
1563
+ return byFile;
1564
+ }
1565
+
1566
+ // src/generator/route-types.ts
1567
+ var import_node_path9 = require("path");
1568
+ async function writeRouteTypes(paths, routes) {
1569
+ const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
1570
+ const lines = [
1571
+ GENERATED_HEADER,
1572
+ "export interface RouteParams {"
1573
+ ];
1574
+ for (const route of routes) {
1575
+ const typeFile = typeFilePath(paths, route.routeDir);
1576
+ lines.push(
1577
+ ` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
1578
+ importPath(file, typeFile)
1579
+ )}).Params;`
1580
+ );
1542
1581
  }
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;
1582
+ lines.push("}", "");
1583
+ await writeGenerated(file, lines.join("\n"));
1547
1584
  }
1548
1585
 
1549
1586
  // src/generator/tsconfig.ts
@@ -1602,20 +1639,21 @@ async function typeFolders(paths, routes) {
1602
1639
  verbs: verbsByDir.get(slash(dir)) ?? []
1603
1640
  }));
1604
1641
  }
1605
- function extractResponses(paths, routes) {
1642
+ async function extractResponses(paths, routes) {
1606
1643
  const byFile = /* @__PURE__ */ new Map();
1607
1644
  if (routes.length === 0) {
1608
1645
  return byFile;
1609
1646
  }
1610
1647
  try {
1648
+ const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1611
1649
  const files = [...new Set(routes.map((route) => route.file))];
1612
1650
  const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1613
- const program = createSchemaProgram(
1651
+ const program = createSchemaProgram2(
1614
1652
  paths,
1615
1653
  (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1616
1654
  );
1617
1655
  for (const file of files) {
1618
- byFile.set(file, extractRouteResponses(program, file));
1656
+ byFile.set(file, extractRouteResponses2(program, file));
1619
1657
  }
1620
1658
  } catch (error) {
1621
1659
  console.warn(`giri: skipped response schema generation (${error.message}).`);
@@ -1657,7 +1695,7 @@ async function syncProject(config, options = {}) {
1657
1695
  await writeRouteTypes(paths, routes);
1658
1696
  await writeAppTypes(paths);
1659
1697
  await writeTsConfig(paths, config);
1660
- const responsesByFile = extractResponses(paths, routes);
1698
+ const responsesByFile = await extractResponses(paths, routes);
1661
1699
  const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1662
1700
  const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
1663
1701
  await writeManifest(paths, routes, data);