@boon4681/giri 0.0.2 → 0.0.3

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
@@ -45,12 +45,18 @@ var init_es5 = __esm({
45
45
  });
46
46
 
47
47
  // src/generator/schema/program.ts
48
- function createSchemaProgram(paths, routeFiles) {
48
+ function leanOptions(options) {
49
+ return {
50
+ ...options,
51
+ types: []
52
+ };
53
+ }
54
+ function createSchemaProgram(paths, routeFiles, programOptions = {}) {
49
55
  let options = { ...DEFAULT_OPTIONS };
50
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
56
+ const configPath = import_typescript2.default.findConfigFile(paths.cwd, import_typescript2.default.sys.fileExists, "tsconfig.json");
51
57
  if (configPath) {
52
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
53
- ...import_typescript.default.sys,
58
+ const parsed = import_typescript2.default.getParsedCommandLineOfConfigFile(configPath, {}, {
59
+ ...import_typescript2.default.sys,
54
60
  onUnRecoverableConfigFileDiagnostic: () => {
55
61
  }
56
62
  });
@@ -58,17 +64,20 @@ function createSchemaProgram(paths, routeFiles) {
58
64
  options = { ...parsed.options, noEmit: true };
59
65
  }
60
66
  }
61
- return import_typescript.default.createProgram(routeFiles, options);
67
+ if (programOptions.lean) {
68
+ options = leanOptions(options);
69
+ }
70
+ return import_typescript2.default.createProgram(routeFiles, options);
62
71
  }
63
- var import_typescript, DEFAULT_OPTIONS;
72
+ var import_typescript2, DEFAULT_OPTIONS;
64
73
  var init_program = __esm({
65
74
  "src/generator/schema/program.ts"() {
66
75
  "use strict";
67
- import_typescript = __toESM(require("typescript"));
76
+ import_typescript2 = __toESM(require("typescript"));
68
77
  DEFAULT_OPTIONS = {
69
- target: import_typescript.default.ScriptTarget.ES2022,
70
- module: import_typescript.default.ModuleKind.NodeNext,
71
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
78
+ target: import_typescript2.default.ScriptTarget.ES2022,
79
+ module: import_typescript2.default.ModuleKind.NodeNext,
80
+ moduleResolution: import_typescript2.default.ModuleResolutionKind.NodeNext,
72
81
  strict: true,
73
82
  skipLibCheck: true,
74
83
  noEmit: true
@@ -102,7 +111,7 @@ function literalValuesOf(types) {
102
111
  for (const member of types) {
103
112
  if (member.isStringLiteral() || member.isNumberLiteral()) {
104
113
  values.push(member.value);
105
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
114
+ } else if (member.flags & import_typescript3.default.TypeFlags.BooleanLiteral) {
106
115
  values.push(intrinsicName(member) === "true");
107
116
  } else {
108
117
  return void 0;
@@ -111,7 +120,7 @@ function literalValuesOf(types) {
111
120
  return values;
112
121
  }
113
122
  function walkUnion(type, ctx) {
114
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
123
+ const flag = import_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void | import_typescript3.default.TypeFlags.Never;
115
124
  const members = type.types.filter((member) => !(member.flags & flag));
116
125
  if (members.length === 1) {
117
126
  return walkType(members[0], ctx);
@@ -124,13 +133,13 @@ function walkUnion(type, ctx) {
124
133
  }
125
134
  function buildObjectSchema(type, ctx) {
126
135
  const { checker } = ctx;
127
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
136
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript3.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript3.default.IndexKind.Number);
128
137
  const properties = {};
129
138
  const required = [];
130
139
  for (const symbol of checker.getPropertiesOfType(type)) {
131
140
  const name = symbol.getName();
132
141
  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));
142
+ const optional = Boolean(symbol.getFlags() & import_typescript3.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript3.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript3.default.TypeFlags.Undefined));
134
143
  properties[name] = walkType(propType, ctx);
135
144
  if (!optional) {
136
145
  required.push(name);
@@ -189,16 +198,16 @@ function walkObject(type, ctx) {
189
198
  }
190
199
  function walkType(type, ctx) {
191
200
  const flags = type.flags;
192
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
201
+ if (flags & (import_typescript3.default.TypeFlags.Any | import_typescript3.default.TypeFlags.Unknown)) {
193
202
  return {};
194
203
  }
195
- if (flags & import_typescript2.default.TypeFlags.Null) {
204
+ if (flags & import_typescript3.default.TypeFlags.Null) {
196
205
  return { type: "null" };
197
206
  }
198
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
207
+ if (flags & (import_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void)) {
199
208
  return {};
200
209
  }
201
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
210
+ if (flags & (import_typescript3.default.TypeFlags.BigInt | import_typescript3.default.TypeFlags.BigIntLiteral)) {
202
211
  ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
203
212
  return { type: "string" };
204
213
  }
@@ -208,45 +217,45 @@ function walkType(type, ctx) {
208
217
  if (type.isNumberLiteral()) {
209
218
  return { type: "number", const: type.value };
210
219
  }
211
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
220
+ if (flags & import_typescript3.default.TypeFlags.BooleanLiteral) {
212
221
  return { type: "boolean", const: intrinsicName(type) === "true" };
213
222
  }
214
- if (flags & import_typescript2.default.TypeFlags.String) {
223
+ if (flags & import_typescript3.default.TypeFlags.String) {
215
224
  return { type: "string" };
216
225
  }
217
- if (flags & import_typescript2.default.TypeFlags.Number) {
226
+ if (flags & import_typescript3.default.TypeFlags.Number) {
218
227
  return { type: "number" };
219
228
  }
220
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
229
+ if (flags & import_typescript3.default.TypeFlags.Boolean) {
221
230
  return { type: "boolean" };
222
231
  }
223
232
  if (type.isUnion()) {
224
233
  return walkUnion(type, ctx);
225
234
  }
226
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
235
+ if (flags & import_typescript3.default.TypeFlags.Object || type.isIntersection()) {
227
236
  return walkObject(type, ctx);
228
237
  }
229
238
  return {};
230
239
  }
231
- var import_typescript2;
240
+ var import_typescript3;
232
241
  var init_json_schema = __esm({
233
242
  "src/generator/schema/json-schema.ts"() {
234
243
  "use strict";
235
- import_typescript2 = __toESM(require("typescript"));
244
+ import_typescript3 = __toESM(require("typescript"));
236
245
  }
237
246
  });
238
247
 
239
248
  // src/generator/schema/responses.ts
240
249
  function findHandleFunction(source) {
241
250
  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);
251
+ const isExported = (node) => import_typescript4.default.canHaveModifiers(node) && (import_typescript4.default.getModifiers(node)?.some((m) => m.kind === import_typescript4.default.SyntaxKind.ExportKeyword) ?? false);
243
252
  for (const statement of source.statements) {
244
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
253
+ if (import_typescript4.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
245
254
  found = statement;
246
255
  }
247
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
256
+ if (import_typescript4.default.isVariableStatement(statement) && isExported(statement)) {
248
257
  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))) {
258
+ if (import_typescript4.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript4.default.isArrowFunction(declaration.initializer) || import_typescript4.default.isFunctionExpression(declaration.initializer))) {
250
259
  found = declaration.initializer;
251
260
  }
252
261
  }
@@ -255,7 +264,7 @@ function findHandleFunction(source) {
255
264
  return found;
256
265
  }
257
266
  function collectReturnExpressions(fn) {
258
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
267
+ if (import_typescript4.default.isArrowFunction(fn) && !import_typescript4.default.isBlock(fn.body)) {
259
268
  return [fn.body];
260
269
  }
261
270
  if (!fn.body) {
@@ -263,15 +272,15 @@ function collectReturnExpressions(fn) {
263
272
  }
264
273
  const expressions = [];
265
274
  const visit = (node) => {
266
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
275
+ if (import_typescript4.default.isFunctionDeclaration(node) || import_typescript4.default.isFunctionExpression(node) || import_typescript4.default.isArrowFunction(node)) {
267
276
  return;
268
277
  }
269
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
278
+ if (import_typescript4.default.isReturnStatement(node) && node.expression) {
270
279
  expressions.push(node.expression);
271
280
  }
272
- import_typescript3.default.forEachChild(node, visit);
281
+ import_typescript4.default.forEachChild(node, visit);
273
282
  };
274
- import_typescript3.default.forEachChild(fn.body, visit);
283
+ import_typescript4.default.forEachChild(fn.body, visit);
275
284
  return expressions;
276
285
  }
277
286
  function propertyType(checker, type, name, location) {
@@ -283,15 +292,21 @@ function isTypedResponse2(checker, type) {
283
292
  checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
284
293
  );
285
294
  }
286
- function readFromCall(checker, expression) {
287
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
295
+ function firstParameterName(fn) {
296
+ const [first] = fn.parameters;
297
+ return first && import_typescript4.default.isIdentifier(first.name) ? first.name.text : void 0;
298
+ }
299
+ function readFromCall(checker, expression, contextName) {
300
+ if (!import_typescript4.default.isCallExpression(expression) || !import_typescript4.default.isPropertyAccessExpression(expression.expression)) {
288
301
  return void 0;
289
302
  }
290
303
  const method = expression.expression.name.text;
291
304
  if (method !== "json" && method !== "text") {
292
305
  return void 0;
293
306
  }
294
- if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
307
+ const target = expression.expression.expression;
308
+ const directContextCall = contextName && import_typescript4.default.isIdentifier(target) && target.text === contextName;
309
+ if (!directContextCall && !isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
295
310
  return void 0;
296
311
  }
297
312
  const [dataArg, statusArg] = expression.arguments;
@@ -331,6 +346,7 @@ function extractRouteResponses(program, file) {
331
346
  return result;
332
347
  }
333
348
  const ctx = createWalkContext(checker, fn);
349
+ const contextName = firstParameterName(fn);
334
350
  const byStatus = /* @__PURE__ */ new Map();
335
351
  const record = (hit) => {
336
352
  const schema = walkType(hit.data, ctx);
@@ -339,7 +355,7 @@ function extractRouteResponses(program, file) {
339
355
  byStatus.set(hit.status, bucket);
340
356
  };
341
357
  for (const expression of collectReturnExpressions(fn)) {
342
- const fromCall = readFromCall(checker, expression);
358
+ const fromCall = readFromCall(checker, expression, contextName);
343
359
  if (fromCall) {
344
360
  record(fromCall);
345
361
  continue;
@@ -365,11 +381,11 @@ function extractRouteResponses(program, file) {
365
381
  result.$defs = ctx.defs;
366
382
  return result;
367
383
  }
368
- var import_typescript3;
384
+ var import_typescript4;
369
385
  var init_responses = __esm({
370
386
  "src/generator/schema/responses.ts"() {
371
387
  "use strict";
372
- import_typescript3 = __toESM(require("typescript"));
388
+ import_typescript4 = __toESM(require("typescript"));
373
389
  init_json_schema();
374
390
  }
375
391
  });
@@ -425,6 +441,21 @@ var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
425
441
 
426
442
  // src/context.ts
427
443
  var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
444
+ var pendingResponseBrand = /* @__PURE__ */ Symbol("giri.pending-response");
445
+ function getPending(context) {
446
+ return context[pendingResponseBrand];
447
+ }
448
+ var unsupportedCookieJar = {
449
+ get: cookiesUnsupported,
450
+ all: cookiesUnsupported,
451
+ set: cookiesUnsupported,
452
+ delete: cookiesUnsupported,
453
+ getSigned: cookiesUnsupported,
454
+ setSigned: cookiesUnsupported
455
+ };
456
+ function cookiesUnsupported() {
457
+ throw new Error("The active adapter does not support cookies.");
458
+ }
428
459
  function createTypedResponse(data, status, format, headers) {
429
460
  return {
430
461
  [typedResponseBrand]: { data, status, format },
@@ -441,6 +472,13 @@ function createContext(options) {
441
472
  const url = new URL(options.request.url);
442
473
  const store = /* @__PURE__ */ new Map();
443
474
  const validated = options.validated ?? {};
475
+ const pending = { headers: new Headers() };
476
+ const defaultStatus = () => pending.status ?? 200;
477
+ const cookies = options.cookies ? options.cookies({
478
+ request: options.request,
479
+ append: (header) => pending.headers.append("set-cookie", header),
480
+ secret: options.cookieSecret
481
+ }) : unsupportedCookieJar;
444
482
  const context = {
445
483
  params: options.params ?? {},
446
484
  app: options.app ?? {},
@@ -458,16 +496,45 @@ function createContext(options) {
458
496
  throw new Error(`No validated ${String(key)} data is available for this route.`);
459
497
  }
460
498
  return validated[key];
461
- }
499
+ },
500
+ cookie: (name) => cookies.get(name),
501
+ cookies: () => cookies.all(),
502
+ signedCookie: (name) => cookies.getSigned(name)
462
503
  },
463
504
  set: (key, value) => {
464
505
  store.set(key, value);
465
506
  },
466
507
  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)
508
+ json: (data, status, headers) => createTypedResponse(data, status ?? defaultStatus(), "json", headers),
509
+ text: (text, status, headers) => createTypedResponse(text, status ?? defaultStatus(), "text", headers),
510
+ html: (html, status, headers) => createTypedResponse(html, status ?? defaultStatus(), "html", headers),
511
+ body: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
512
+ newResponse: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
513
+ redirect: (location, status) => new Response(null, { status: status ?? 302, headers: { Location: location } }),
514
+ notFound: () => new Response("404 Not Found", { status: 404 }),
515
+ header: (name, value, options2) => {
516
+ if (value === void 0) {
517
+ pending.headers.delete(name);
518
+ } else if (options2?.append) {
519
+ pending.headers.append(name, value);
520
+ } else {
521
+ pending.headers.set(name, value);
522
+ }
523
+ },
524
+ status: (code) => {
525
+ pending.status = code;
526
+ },
527
+ cookie: (name, value, options2) => {
528
+ if (value === null) {
529
+ cookies.delete(name, options2);
530
+ } else {
531
+ cookies.set(name, value, options2);
532
+ }
533
+ },
534
+ signedCookie: (name, value, options2) => cookies.setSigned(name, value, options2)
469
535
  };
470
536
  context[nativeContextBrand] = options.native;
537
+ context[pendingResponseBrand] = pending;
471
538
  return context;
472
539
  }
473
540
  function typedResponseToResponse(response) {
@@ -478,14 +545,41 @@ function typedResponseToResponse(response) {
478
545
  if (response.format === "text" && !headers.has("content-type")) {
479
546
  headers.set("content-type", "text/plain; charset=utf-8");
480
547
  }
548
+ if (response.format === "html" && !headers.has("content-type")) {
549
+ headers.set("content-type", "text/html; charset=utf-8");
550
+ }
481
551
  const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
482
552
  return new Response(body, {
483
553
  status: response.status,
484
554
  headers
485
555
  });
486
556
  }
487
- function toResponse(response) {
488
- return isTypedResponse(response) ? typedResponseToResponse(response) : response;
557
+ function toResponse(response, context) {
558
+ const base = isTypedResponse(response) ? typedResponseToResponse(response) : response;
559
+ const pending = context ? getPending(context) : void 0;
560
+ if (!pending) {
561
+ return base;
562
+ }
563
+ let hasPending = false;
564
+ pending.headers.forEach(() => {
565
+ hasPending = true;
566
+ });
567
+ if (!hasPending) {
568
+ return base;
569
+ }
570
+ const headers = new Headers(base.headers);
571
+ pending.headers.forEach((value, key) => {
572
+ if (key === "set-cookie") {
573
+ return;
574
+ }
575
+ if (!headers.has(key)) {
576
+ headers.set(key, value);
577
+ }
578
+ });
579
+ for (const cookie of pending.headers.getSetCookie?.() ?? []) {
580
+ headers.append("set-cookie", cookie);
581
+ }
582
+ return new Response(base.body, { status: base.status, statusText: base.statusText, headers });
489
583
  }
490
584
  async function composeMiddleware(middleware, handle, context) {
491
585
  let index = -1;
@@ -686,7 +780,8 @@ var configSchema = import_typebox.Type.Object({
686
780
  port: import_typebox.Type.Optional(import_typebox.Type.Number()),
687
781
  hostname: import_typebox.Type.Optional(import_typebox.Type.String())
688
782
  }, { additionalProperties: false })),
689
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
783
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any()),
784
+ cookieSecret: import_typebox.Type.Optional(import_typebox.Type.String())
690
785
  }, { additionalProperties: false });
691
786
 
692
787
  // src/loader/loader.ts
@@ -748,13 +843,18 @@ function methodFromFile(fileName) {
748
843
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
749
844
  return METHOD_FROM_FILE.get(stem);
750
845
  }
751
- function sharedFileIn(dir) {
846
+ function sharedFileIn(dir, cache) {
847
+ if (cache?.has(dir)) {
848
+ return cache.get(dir);
849
+ }
752
850
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
753
851
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
754
852
  if ((0, import_node_fs2.existsSync)(file)) {
853
+ cache?.set(dir, file);
755
854
  return file;
756
855
  }
757
856
  }
857
+ cache?.set(dir, void 0);
758
858
  return void 0;
759
859
  }
760
860
  function physicalRouteSegments(routesDir, routeDir) {
@@ -823,7 +923,7 @@ async function scanRouteFolders(routesDir) {
823
923
  function routeParamsForDir(routesDir, dir) {
824
924
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
825
925
  }
826
- function sharedFilesForDir(routesDir, dir) {
926
+ function sharedFilesForDir(routesDir, dir, cache) {
827
927
  const segments = physicalRouteSegments(routesDir, dir);
828
928
  const dirs = [routesDir];
829
929
  let current = routesDir;
@@ -831,7 +931,7 @@ function sharedFilesForDir(routesDir, dir) {
831
931
  current = (0, import_node_path2.join)(current, segment);
832
932
  dirs.push(current);
833
933
  }
834
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
934
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
835
935
  }
836
936
  async function scanRoutes(routesDir) {
837
937
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -843,6 +943,7 @@ async function scanRoutes(routesDir) {
843
943
  onlyFiles: true
844
944
  });
845
945
  const routes = [];
946
+ const sharedCache = /* @__PURE__ */ new Map();
846
947
  for (const file of files) {
847
948
  const method = methodFromFile((0, import_node_path2.basename)(file));
848
949
  if (!method) {
@@ -858,7 +959,7 @@ async function scanRoutes(routesDir) {
858
959
  routeDir,
859
960
  routeSegments,
860
961
  params,
861
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
962
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
862
963
  });
863
964
  }
864
965
  return routes.sort((left, right) => {
@@ -871,9 +972,11 @@ async function scanRoutes(routesDir) {
871
972
  }
872
973
 
873
974
  // src/app.ts
874
- function loadModule(file) {
975
+ function loadModule(file, force = true) {
875
976
  const resolved = require.resolve(file);
876
- delete require.cache[resolved];
977
+ if (force) {
978
+ delete require.cache[resolved];
979
+ }
877
980
  return require(resolved);
878
981
  }
879
982
  function interopDefault(value) {
@@ -935,20 +1038,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
935
1038
  }
936
1039
  function matchAlias(request, key) {
937
1040
  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);
1041
+ const [prefix, suffix = ""] = key.split("*");
1042
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
1043
+ return request.slice(prefix.length, request.length - suffix.length);
941
1044
  }
942
1045
  return void 0;
943
1046
  }
944
- if (request === key) {
945
- return "";
946
- }
947
- const prefix = `${key}/`;
948
- if (request.startsWith(prefix)) {
949
- return request.slice(prefix.length);
950
- }
951
- return void 0;
1047
+ return request === key ? "" : void 0;
952
1048
  }
953
1049
  function resolveAliasRequest(request, alias, cwd) {
954
1050
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -1015,13 +1111,23 @@ async function buildGiriApp(config, options = {}) {
1015
1111
  const { unregister } = await safeRegister();
1016
1112
  const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
1017
1113
  try {
1114
+ const dirty = options.dirty;
1115
+ const forceReload = dirty === void 0;
1116
+ const isDirty = (file) => forceReload || dirty.has(file);
1117
+ const sharedCache = /* @__PURE__ */ new Map();
1118
+ const loadShared = (file) => {
1119
+ if (!sharedCache.has(file)) {
1120
+ sharedCache.set(file, loadModule(file, isDirty(file)));
1121
+ }
1122
+ return sharedCache.get(file);
1123
+ };
1018
1124
  for (const route of routes) {
1019
- const routeModule = loadModule(route.file);
1125
+ const routeModule = loadModule(route.file, isDirty(route.file));
1020
1126
  if (typeof routeModule.handle !== "function") {
1021
1127
  throw new Error(`${route.file} must export a named handle function.`);
1022
1128
  }
1023
1129
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
1024
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
1130
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
1025
1131
  );
1026
1132
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
1027
1133
  config.adapter.register(app, {
@@ -1030,7 +1136,8 @@ async function buildGiriApp(config, options = {}) {
1030
1136
  handle: routeModule.handle,
1031
1137
  middleware: [...folderMiddleware, ...verbMiddleware],
1032
1138
  input: routeInput(routeModule, route.file),
1033
- services: options.services
1139
+ services: options.services,
1140
+ cookieSecret: config.cookieSecret
1034
1141
  });
1035
1142
  }
1036
1143
  } finally {
@@ -1041,7 +1148,7 @@ async function buildGiriApp(config, options = {}) {
1041
1148
  }
1042
1149
 
1043
1150
  // src/generator/sync.ts
1044
- var import_node_fs6 = require("fs");
1151
+ var import_node_fs7 = require("fs");
1045
1152
  var import_promises3 = require("fs/promises");
1046
1153
  var import_node_path11 = require("path");
1047
1154
 
@@ -1295,6 +1402,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1295
1402
  const documentPaths = {};
1296
1403
  const schemas = {};
1297
1404
  const securitySchemes = {};
1405
+ const tagOrder = [];
1298
1406
  for (const route of routes) {
1299
1407
  if (data.hiddenFiles?.has(route.file)) {
1300
1408
  continue;
@@ -1302,10 +1410,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1302
1410
  const responses = data.responsesByFile?.get(route.file);
1303
1411
  const input = data.inputsByFile?.get(route.file);
1304
1412
  const security = data.securityByFile?.get(route.file);
1413
+ const meta = data.openapiByFile?.get(route.file);
1305
1414
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1306
1415
  schemas[name] = rewriteRefs(schema);
1307
1416
  }
1308
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1417
+ const operation = {};
1418
+ if (meta?.tags && meta.tags.length > 0) {
1419
+ operation.tags = meta.tags;
1420
+ for (const tag of meta.tags) {
1421
+ if (!tagOrder.includes(tag)) {
1422
+ tagOrder.push(tag);
1423
+ }
1424
+ }
1425
+ }
1426
+ if (meta?.summary) {
1427
+ operation.summary = meta.summary;
1428
+ }
1429
+ if (meta?.description) {
1430
+ operation.description = meta.description;
1431
+ }
1432
+ if (meta?.operationId) {
1433
+ operation.operationId = meta.operationId;
1434
+ }
1435
+ if (meta?.deprecated) {
1436
+ operation.deprecated = true;
1437
+ }
1438
+ operation.responses = buildResponses(responses?.responses ?? []);
1309
1439
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1310
1440
  if (parameters.length > 0) {
1311
1441
  operation.parameters = parameters;
@@ -1337,6 +1467,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1337
1467
  info: readProjectInfo(paths.cwd),
1338
1468
  paths: documentPaths
1339
1469
  };
1470
+ if (tagOrder.length > 0) {
1471
+ document.tags = tagOrder.map((name) => ({ name }));
1472
+ }
1340
1473
  const components = {};
1341
1474
  if (Object.keys(schemas).length > 0) {
1342
1475
  components.schemas = schemas;
@@ -1368,14 +1501,29 @@ function paramsType(params) {
1368
1501
  ${fields}
1369
1502
  }`;
1370
1503
  }
1371
- function varsType(typesDir, sharedFiles) {
1372
- if (sharedFiles.length === 0) {
1373
- return "{}";
1504
+ function middlewareVarsType(typesDir, sharedFile) {
1505
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1506
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1507
+ }
1508
+ function ownSharedFile(dir, sharedFiles) {
1509
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1510
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1511
+ return sharedFiles[index];
1512
+ }
1374
1513
  }
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 & ");
1514
+ return void 0;
1515
+ }
1516
+ function varsType(paths, file, dir, sharedFiles) {
1517
+ const typesDir = (0, import_node_path8.dirname)(file);
1518
+ const parts = [];
1519
+ if (dir !== paths.routesDir) {
1520
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1521
+ }
1522
+ const ownShared = ownSharedFile(dir, sharedFiles);
1523
+ if (ownShared) {
1524
+ parts.push(middlewareVarsType(typesDir, ownShared));
1525
+ }
1526
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1379
1527
  }
1380
1528
  function methodExports(typesDir, verbs) {
1381
1529
  return verbs.map(({ method, file }) => {
@@ -1386,14 +1534,14 @@ function methodExports(typesDir, verbs) {
1386
1534
  });
1387
1535
  }
1388
1536
  async function writeParamTypes(paths, folders) {
1389
- for (const { dir, params, sharedFiles, verbs } of folders) {
1537
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1390
1538
  const file = typeFilePath(paths, dir);
1391
1539
  const typesDir = (0, import_node_path8.dirname)(file);
1392
1540
  const lines = [
1393
1541
  GENERATED_HEADER,
1394
1542
  `export type Params = ${paramsType(params)};`,
1395
1543
  "export type RouteParams = Params;",
1396
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1544
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1397
1545
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1398
1546
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1399
1547
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1403,10 +1551,14 @@ async function writeParamTypes(paths, folders) {
1403
1551
  lines.push(...methodExports(typesDir, verbs));
1404
1552
  }
1405
1553
  lines.push("");
1406
- await writeGenerated(file, lines.join("\n"));
1407
- }
1554
+ return writeGenerated(file, lines.join("\n"));
1555
+ }));
1408
1556
  }
1409
1557
 
1558
+ // src/generator/route-meta.ts
1559
+ var import_node_fs6 = require("fs");
1560
+ var import_typescript = __toESM(require("typescript"));
1561
+
1410
1562
  // src/generator/inputs.ts
1411
1563
  function sanitize(schema) {
1412
1564
  const { $schema, ...rest } = schema;
@@ -1467,28 +1619,315 @@ function readInput(routeModule) {
1467
1619
  }
1468
1620
  return input.body || input.query ? input : void 0;
1469
1621
  }
1470
- function hiddenFrom(value) {
1471
- if (value === false) {
1622
+ function hasExportModifier(node) {
1623
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
1624
+ }
1625
+ function unwrapExpression(expression) {
1626
+ let current = expression;
1627
+ while (import_typescript.default.isParenthesizedExpression(current) || import_typescript.default.isAsExpression(current) || import_typescript.default.isSatisfiesExpression(current)) {
1628
+ current = current.expression;
1629
+ }
1630
+ return current;
1631
+ }
1632
+ function staticBoolean(expression) {
1633
+ const value = unwrapExpression(expression);
1634
+ if (value.kind === import_typescript.default.SyntaxKind.TrueKeyword) {
1472
1635
  return true;
1473
1636
  }
1474
- if (value === true) {
1637
+ if (value.kind === import_typescript.default.SyntaxKind.FalseKeyword) {
1475
1638
  return false;
1476
1639
  }
1477
- if (value && typeof value === "object" && "hidden" in value) {
1478
- return Boolean(value.hidden);
1640
+ return void 0;
1641
+ }
1642
+ function staticString(expression) {
1643
+ const value = unwrapExpression(expression);
1644
+ return import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
1645
+ }
1646
+ function staticStringArray(expression) {
1647
+ const value = unwrapExpression(expression);
1648
+ if (!import_typescript.default.isArrayLiteralExpression(value)) {
1649
+ return void 0;
1650
+ }
1651
+ const strings = [];
1652
+ for (const element of value.elements) {
1653
+ const string = staticString(element);
1654
+ if (string === void 0) {
1655
+ return void 0;
1656
+ }
1657
+ strings.push(string);
1658
+ }
1659
+ return strings;
1660
+ }
1661
+ function propertyName(name) {
1662
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
1663
+ return name.text;
1479
1664
  }
1480
1665
  return void 0;
1481
1666
  }
1482
- function collectHidden(route, routeModule, loadShared) {
1667
+ function collectImportedNames(source) {
1668
+ const names = /* @__PURE__ */ new Set();
1669
+ for (const statement of source.statements) {
1670
+ if (!import_typescript.default.isImportDeclaration(statement)) {
1671
+ continue;
1672
+ }
1673
+ const clause = statement.importClause;
1674
+ if (!clause) {
1675
+ continue;
1676
+ }
1677
+ if (clause.name) {
1678
+ names.add(clause.name.text);
1679
+ }
1680
+ const bindings = clause.namedBindings;
1681
+ if (bindings && import_typescript.default.isNamespaceImport(bindings)) {
1682
+ names.add(bindings.name.text);
1683
+ }
1684
+ if (bindings && import_typescript.default.isNamedImports(bindings)) {
1685
+ for (const element of bindings.elements) {
1686
+ names.add(element.name.text);
1687
+ }
1688
+ }
1689
+ }
1690
+ return names;
1691
+ }
1692
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1693
+ let found = false;
1694
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1695
+ const visit = (node) => {
1696
+ if (found) {
1697
+ return;
1698
+ }
1699
+ if (import_typescript.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1700
+ found = true;
1701
+ return;
1702
+ }
1703
+ import_typescript.default.forEachChild(node, visit);
1704
+ };
1705
+ visit(expression);
1706
+ return found;
1707
+ }
1708
+ function parseStaticOpenApi(expression) {
1709
+ const value = unwrapExpression(expression);
1710
+ const boolean = staticBoolean(value);
1711
+ if (boolean !== void 0) {
1712
+ return boolean;
1713
+ }
1714
+ if (!import_typescript.default.isObjectLiteralExpression(value)) {
1715
+ return void 0;
1716
+ }
1717
+ const openapi = {};
1718
+ for (const property of value.properties) {
1719
+ if (!import_typescript.default.isPropertyAssignment(property)) {
1720
+ return void 0;
1721
+ }
1722
+ const name = propertyName(property.name);
1723
+ if (!name) {
1724
+ return void 0;
1725
+ }
1726
+ if (name === "hidden") {
1727
+ const hidden = staticBoolean(property.initializer);
1728
+ if (hidden === void 0) {
1729
+ return void 0;
1730
+ }
1731
+ openapi.hidden = hidden;
1732
+ } else if (name === "tags") {
1733
+ const tags = staticStringArray(property.initializer);
1734
+ if (!tags) {
1735
+ return void 0;
1736
+ }
1737
+ openapi.tags = tags;
1738
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1739
+ const string = staticString(property.initializer);
1740
+ if (string === void 0) {
1741
+ return void 0;
1742
+ }
1743
+ openapi[name] = string;
1744
+ } else if (name === "deprecated") {
1745
+ const deprecated = staticBoolean(property.initializer);
1746
+ if (deprecated === void 0) {
1747
+ return void 0;
1748
+ }
1749
+ openapi.deprecated = deprecated;
1750
+ } else {
1751
+ return void 0;
1752
+ }
1753
+ }
1754
+ return openapi;
1755
+ }
1756
+ function readStaticModuleMeta(file) {
1757
+ let source;
1758
+ try {
1759
+ source = import_typescript.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript.default.ScriptTarget.Latest, true);
1760
+ } catch {
1761
+ return void 0;
1762
+ }
1763
+ const importedNames = collectImportedNames(source);
1764
+ const sourceText = source.getFullText();
1765
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1766
+ const meta = { middlewareSecurity: false };
1767
+ for (const statement of source.statements) {
1768
+ if (import_typescript.default.isImportDeclaration(statement) || import_typescript.default.isInterfaceDeclaration(statement) || import_typescript.default.isTypeAliasDeclaration(statement) || import_typescript.default.isEmptyStatement(statement) || !hasExportModifier(statement)) {
1769
+ continue;
1770
+ }
1771
+ if (import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1772
+ continue;
1773
+ }
1774
+ if (import_typescript.default.isVariableStatement(statement)) {
1775
+ for (const declaration of statement.declarationList.declarations) {
1776
+ if (!import_typescript.default.isIdentifier(declaration.name)) {
1777
+ return void 0;
1778
+ }
1779
+ const name = declaration.name.text;
1780
+ if (name === "openapi") {
1781
+ if (!declaration.initializer) {
1782
+ return void 0;
1783
+ }
1784
+ const openapi = parseStaticOpenApi(declaration.initializer);
1785
+ if (openapi === void 0) {
1786
+ return void 0;
1787
+ }
1788
+ meta.openapi = openapi;
1789
+ } else if (name === "handle") {
1790
+ if (!declaration.initializer || !import_typescript.default.isArrowFunction(declaration.initializer) && !import_typescript.default.isFunctionExpression(declaration.initializer)) {
1791
+ return void 0;
1792
+ }
1793
+ continue;
1794
+ } else if (name === "middleware") {
1795
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1796
+ return void 0;
1797
+ }
1798
+ meta.middlewareSecurity = false;
1799
+ continue;
1800
+ } else {
1801
+ return void 0;
1802
+ }
1803
+ }
1804
+ continue;
1805
+ }
1806
+ return void 0;
1807
+ }
1808
+ return meta;
1809
+ }
1810
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1483
1811
  let hidden = false;
1812
+ const tags = [];
1813
+ const meta = {};
1814
+ const apply = (value, isVerb) => {
1815
+ if (value === false) {
1816
+ hidden = true;
1817
+ return;
1818
+ }
1819
+ if (value === true) {
1820
+ hidden = false;
1821
+ return;
1822
+ }
1823
+ if (!value) {
1824
+ return;
1825
+ }
1826
+ if (typeof value.hidden === "boolean") {
1827
+ hidden = value.hidden;
1828
+ }
1829
+ if (value.tags) {
1830
+ tags.push(...value.tags);
1831
+ }
1832
+ if (typeof value.summary === "string") {
1833
+ meta.summary = value.summary;
1834
+ }
1835
+ if (typeof value.description === "string") {
1836
+ meta.description = value.description;
1837
+ }
1838
+ if (typeof value.deprecated === "boolean") {
1839
+ meta.deprecated = value.deprecated;
1840
+ }
1841
+ if (isVerb && typeof value.operationId === "string") {
1842
+ meta.operationId = value.operationId;
1843
+ }
1844
+ };
1484
1845
  for (const file of route.sharedFiles) {
1485
- const opinion = hiddenFrom(loadShared(file).openapi);
1486
- if (opinion !== void 0) {
1487
- hidden = opinion;
1846
+ const shared = loadShared(file);
1847
+ if (!shared) {
1848
+ return void 0;
1488
1849
  }
1850
+ apply(shared.openapi, false);
1851
+ }
1852
+ apply(routeModule.openapi, true);
1853
+ if (tags.length > 0) {
1854
+ meta.tags = [...new Set(tags)];
1855
+ }
1856
+ return { hidden, meta };
1857
+ }
1858
+ function extractStaticMeta(route, routeModule, loadShared) {
1859
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1860
+ if (!openapi) {
1861
+ return void 0;
1862
+ }
1863
+ const meta = {};
1864
+ if (openapi.hidden) {
1865
+ meta.hidden = true;
1866
+ }
1867
+ if (Object.keys(openapi.meta).length > 0) {
1868
+ meta.openapi = openapi.meta;
1489
1869
  }
1490
- const verb = hiddenFrom(routeModule.openapi);
1491
- return verb ?? hidden;
1870
+ return meta;
1871
+ }
1872
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
1873
+ const meta = {};
1874
+ const security = collectSecurity(route, {}, loadShared);
1875
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
1876
+ if (security) {
1877
+ meta.security = security;
1878
+ }
1879
+ if (hidden) {
1880
+ meta.hidden = true;
1881
+ }
1882
+ if (Object.keys(openapi).length > 0) {
1883
+ meta.openapi = openapi;
1884
+ }
1885
+ return meta;
1886
+ }
1887
+ function resolveOpenApi(route, routeModule, loadShared) {
1888
+ let hidden = false;
1889
+ const tags = [];
1890
+ const meta = {};
1891
+ const apply = (value, isVerb) => {
1892
+ if (value === false) {
1893
+ hidden = true;
1894
+ return;
1895
+ }
1896
+ if (value === true) {
1897
+ hidden = false;
1898
+ return;
1899
+ }
1900
+ if (!value || typeof value !== "object") {
1901
+ return;
1902
+ }
1903
+ const o = value;
1904
+ if ("hidden" in o) {
1905
+ hidden = Boolean(o.hidden);
1906
+ }
1907
+ if (Array.isArray(o.tags)) {
1908
+ tags.push(...o.tags.filter((tag) => typeof tag === "string"));
1909
+ }
1910
+ if (typeof o.summary === "string") {
1911
+ meta.summary = o.summary;
1912
+ }
1913
+ if (typeof o.description === "string") {
1914
+ meta.description = o.description;
1915
+ }
1916
+ if (typeof o.deprecated === "boolean") {
1917
+ meta.deprecated = o.deprecated;
1918
+ }
1919
+ if (isVerb && typeof o.operationId === "string") {
1920
+ meta.operationId = o.operationId;
1921
+ }
1922
+ };
1923
+ for (const file of route.sharedFiles) {
1924
+ apply(loadShared(file).openapi, false);
1925
+ }
1926
+ apply(routeModule.openapi, true);
1927
+ if (tags.length > 0) {
1928
+ meta.tags = [...new Set(tags)];
1929
+ }
1930
+ return { hidden, meta };
1492
1931
  }
1493
1932
  function collectSecurity(route, routeModule, loadShared) {
1494
1933
  const skipInherited = Boolean(
@@ -1520,6 +1959,33 @@ function collectSecurity(route, routeModule, loadShared) {
1520
1959
  }
1521
1960
  async function extractRouteMeta(config, paths, routes) {
1522
1961
  const byFile = /* @__PURE__ */ new Map();
1962
+ const remainingRoutes = [];
1963
+ const runtimeSharedRoutes = [];
1964
+ const staticCache = /* @__PURE__ */ new Map();
1965
+ const loadStatic = (file) => {
1966
+ if (!staticCache.has(file)) {
1967
+ staticCache.set(file, readStaticModuleMeta(file));
1968
+ }
1969
+ return staticCache.get(file);
1970
+ };
1971
+ for (const route of routes) {
1972
+ const routeModule = loadStatic(route.file);
1973
+ if (!routeModule) {
1974
+ remainingRoutes.push(route);
1975
+ continue;
1976
+ }
1977
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
1978
+ if (meta) {
1979
+ if (meta.hidden || meta.openapi) {
1980
+ byFile.set(route.file, meta);
1981
+ }
1982
+ continue;
1983
+ }
1984
+ runtimeSharedRoutes.push({ route, routeModule });
1985
+ }
1986
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
1987
+ return byFile;
1988
+ }
1523
1989
  const { unregister } = await safeRegister();
1524
1990
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1525
1991
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1534,13 +2000,19 @@ async function extractRouteMeta(config, paths, routes) {
1534
2000
  return sharedCache.get(file);
1535
2001
  };
1536
2002
  try {
1537
- for (const route of routes) {
2003
+ for (const { route, routeModule } of runtimeSharedRoutes) {
2004
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
2005
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
2006
+ byFile.set(route.file, meta);
2007
+ }
2008
+ }
2009
+ for (const route of remainingRoutes) {
1538
2010
  try {
1539
2011
  const routeModule = loadModule2(route.file);
1540
2012
  const meta = {};
1541
2013
  const input = readInput(routeModule);
1542
2014
  const security = collectSecurity(route, routeModule, loadShared);
1543
- const hidden = collectHidden(route, routeModule, loadShared);
2015
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1544
2016
  if (input) {
1545
2017
  meta.input = input;
1546
2018
  }
@@ -1550,7 +2022,10 @@ async function extractRouteMeta(config, paths, routes) {
1550
2022
  if (hidden) {
1551
2023
  meta.hidden = true;
1552
2024
  }
1553
- if (meta.input || meta.security || meta.hidden) {
2025
+ if (Object.keys(openapi).length > 0) {
2026
+ meta.openapi = openapi;
2027
+ }
2028
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1554
2029
  byFile.set(route.file, meta);
1555
2030
  }
1556
2031
  } catch {
@@ -1632,10 +2107,11 @@ async function typeFolders(paths, routes) {
1632
2107
  verbsByDir.set(key, list);
1633
2108
  }
1634
2109
  const dirs = await scanRouteFolders(paths.routesDir);
2110
+ const sharedCache = /* @__PURE__ */ new Map();
1635
2111
  return dirs.map((dir) => ({
1636
2112
  dir,
1637
2113
  params: routeParamsForDir(paths.routesDir, dir),
1638
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
2114
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1639
2115
  verbs: verbsByDir.get(slash(dir)) ?? []
1640
2116
  }));
1641
2117
  }
@@ -1648,24 +2124,63 @@ async function extractResponses(paths, routes) {
1648
2124
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1649
2125
  const files = [...new Set(routes.map((route) => route.file))];
1650
2126
  const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1651
- const program = createSchemaProgram2(
1652
- paths,
1653
- (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1654
- );
2127
+ const roots = (0, import_node_fs7.existsSync)(appTypes) ? [...files, appTypes] : files;
2128
+ const program = createSchemaProgram2(paths, roots, { lean: true });
2129
+ const fallbackFiles = [];
1655
2130
  for (const file of files) {
1656
- byFile.set(file, extractRouteResponses2(program, file));
2131
+ const responses = extractRouteResponses2(program, file);
2132
+ byFile.set(file, responses);
2133
+ if (hasLooseResponseSchema(responses)) {
2134
+ fallbackFiles.push(file);
2135
+ }
2136
+ }
2137
+ if (fallbackFiles.length > 0) {
2138
+ const fullProgram = createSchemaProgram2(
2139
+ paths,
2140
+ (0, import_node_fs7.existsSync)(appTypes) ? [...fallbackFiles, appTypes] : fallbackFiles
2141
+ );
2142
+ for (const file of fallbackFiles) {
2143
+ byFile.set(file, extractRouteResponses2(fullProgram, file));
2144
+ }
1657
2145
  }
1658
2146
  } catch (error) {
1659
2147
  console.warn(`giri: skipped response schema generation (${error.message}).`);
1660
2148
  }
1661
2149
  return byFile;
1662
2150
  }
2151
+ function isLooseSchema(value) {
2152
+ if (!value || typeof value !== "object") {
2153
+ return false;
2154
+ }
2155
+ const schema = value;
2156
+ const keys = Object.keys(schema);
2157
+ if (keys.length === 0) {
2158
+ return true;
2159
+ }
2160
+ if (typeof schema.$ref === "string") {
2161
+ return false;
2162
+ }
2163
+ if (Array.isArray(schema.anyOf) && schema.anyOf.some(isLooseSchema)) {
2164
+ return true;
2165
+ }
2166
+ if (schema.items && isLooseSchema(schema.items)) {
2167
+ return true;
2168
+ }
2169
+ if (schema.properties && typeof schema.properties === "object") {
2170
+ return Object.values(schema.properties).some(isLooseSchema);
2171
+ }
2172
+ return false;
2173
+ }
2174
+ function hasLooseResponseSchema(responses) {
2175
+ return responses.responses.some((response) => isLooseSchema(response.schema));
2176
+ }
1663
2177
  async function extractMeta(config, paths, routes) {
1664
2178
  const inputsByFile = /* @__PURE__ */ new Map();
1665
2179
  const securityByFile = /* @__PURE__ */ new Map();
1666
2180
  const hiddenFiles = /* @__PURE__ */ new Set();
2181
+ const openapiByFile = /* @__PURE__ */ new Map();
1667
2182
  if (routes.length === 0) {
1668
- return { inputsByFile, securityByFile, hiddenFiles };
2183
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1669
2184
  }
1670
2185
  try {
1671
2186
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1679,15 +2194,19 @@ async function extractMeta(config, paths, routes) {
1679
2194
  if (entry.hidden) {
1680
2195
  hiddenFiles.add(file);
1681
2196
  }
2197
+ if (entry.openapi) {
2198
+ openapiByFile.set(file, entry.openapi);
2199
+ }
1682
2200
  }
1683
2201
  } catch (error) {
1684
2202
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1685
2203
  }
1686
- return { inputsByFile, securityByFile, hiddenFiles };
2204
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1687
2205
  }
1688
2206
  async function syncProject(config, options = {}) {
1689
2207
  const paths = resolveGiriPaths(config, options.cwd);
1690
2208
  assertSafeOutDir(paths);
2209
+ const hadOutDir = (0, import_node_fs7.existsSync)(paths.outDir);
1691
2210
  const routes = await scanRoutes(paths.routesDir);
1692
2211
  const folders = await typeFolders(paths, routes);
1693
2212
  await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
@@ -1696,46 +2215,54 @@ async function syncProject(config, options = {}) {
1696
2215
  await writeAppTypes(paths);
1697
2216
  await writeTsConfig(paths, config);
1698
2217
  const responsesByFile = await extractResponses(paths, routes);
1699
- const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1700
- const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
2218
+ const { inputsByFile, securityByFile, hiddenFiles, openapiByFile } = await extractMeta(config, paths, routes);
2219
+ const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1701
2220
  await writeManifest(paths, routes, data);
1702
2221
  await writeOpenApi(paths, routes, data);
1703
- await pruneDir(
1704
- paths.outDir,
1705
- /* @__PURE__ */ new Set([
1706
- (0, import_node_path11.join)(paths.outDir, "tsconfig.json"),
1707
- (0, import_node_path11.join)(paths.outDir, "manifest.json"),
1708
- (0, import_node_path11.join)(paths.outDir, "openapi.json"),
1709
- (0, import_node_path11.join)(paths.outDir, "routes.d.ts"),
1710
- (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts"),
1711
- ...folders.map((folder) => typeFilePath(paths, folder.dir))
1712
- ])
1713
- );
2222
+ if (hadOutDir) {
2223
+ await pruneDir(
2224
+ paths.outDir,
2225
+ /* @__PURE__ */ new Set([
2226
+ (0, import_node_path11.join)(paths.outDir, "tsconfig.json"),
2227
+ (0, import_node_path11.join)(paths.outDir, "manifest.json"),
2228
+ (0, import_node_path11.join)(paths.outDir, "openapi.json"),
2229
+ (0, import_node_path11.join)(paths.outDir, "routes.d.ts"),
2230
+ (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts"),
2231
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2232
+ ])
2233
+ );
2234
+ }
1714
2235
  return { paths, routes, folders, data };
1715
2236
  }
1716
2237
 
1717
2238
  // src/generator/watch.ts
1718
- var import_node_fs7 = require("fs");
1719
- var import_node_path13 = require("path");
2239
+ var import_node_fs9 = require("fs");
2240
+ var import_node_path14 = require("path");
1720
2241
 
1721
- // src/loader/module-loader.ts
2242
+ // src/loader/import-graph.ts
2243
+ var import_node_fs8 = require("fs");
1722
2244
  var import_node_path12 = require("path");
2245
+ var import_typescript5 = __toESM(require("typescript"));
2246
+ var import_tinyglobby2 = require("tinyglobby");
2247
+
2248
+ // src/loader/module-loader.ts
2249
+ var import_node_path13 = require("path");
1723
2250
 
1724
2251
  // src/lifecycle.ts
1725
- var import_node_fs8 = require("fs");
1726
- var import_node_path14 = require("path");
2252
+ var import_node_fs10 = require("fs");
2253
+ var import_node_path15 = require("path");
1727
2254
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1728
2255
  function resolveMainFile(cwd) {
1729
2256
  for (const ext of MAIN_EXTENSIONS2) {
1730
- const file = (0, import_node_path14.join)(cwd, "src", `main.${ext}`);
1731
- if ((0, import_node_fs8.existsSync)(file)) {
2257
+ const file = (0, import_node_path15.join)(cwd, "src", `main.${ext}`);
2258
+ if ((0, import_node_fs10.existsSync)(file)) {
1732
2259
  return file;
1733
2260
  }
1734
2261
  }
1735
2262
  return void 0;
1736
2263
  }
1737
2264
  async function loadLifecycle(cwd = process.cwd()) {
1738
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2265
+ const file = resolveMainFile((0, import_node_path15.resolve)(cwd));
1739
2266
  if (!file) {
1740
2267
  return {};
1741
2268
  }