@boon4681/giri 0.0.2 → 0.0.3-alpha-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,10 +47,10 @@ var init_es5 = __esm({
47
47
  // src/generator/schema/program.ts
48
48
  function createSchemaProgram(paths, routeFiles) {
49
49
  let options = { ...DEFAULT_OPTIONS };
50
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
50
+ const configPath = import_typescript3.default.findConfigFile(paths.cwd, import_typescript3.default.sys.fileExists, "tsconfig.json");
51
51
  if (configPath) {
52
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
53
- ...import_typescript.default.sys,
52
+ const parsed = import_typescript3.default.getParsedCommandLineOfConfigFile(configPath, {}, {
53
+ ...import_typescript3.default.sys,
54
54
  onUnRecoverableConfigFileDiagnostic: () => {
55
55
  }
56
56
  });
@@ -58,17 +58,17 @@ function createSchemaProgram(paths, routeFiles) {
58
58
  options = { ...parsed.options, noEmit: true };
59
59
  }
60
60
  }
61
- return import_typescript.default.createProgram(routeFiles, options);
61
+ return import_typescript3.default.createProgram(routeFiles, options);
62
62
  }
63
- var import_typescript, DEFAULT_OPTIONS;
63
+ var import_typescript3, DEFAULT_OPTIONS;
64
64
  var init_program = __esm({
65
65
  "src/generator/schema/program.ts"() {
66
66
  "use strict";
67
- import_typescript = __toESM(require("typescript"));
67
+ import_typescript3 = __toESM(require("typescript"));
68
68
  DEFAULT_OPTIONS = {
69
- target: import_typescript.default.ScriptTarget.ES2022,
70
- module: import_typescript.default.ModuleKind.NodeNext,
71
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
69
+ target: import_typescript3.default.ScriptTarget.ES2022,
70
+ module: import_typescript3.default.ModuleKind.NodeNext,
71
+ moduleResolution: import_typescript3.default.ModuleResolutionKind.NodeNext,
72
72
  strict: true,
73
73
  skipLibCheck: true,
74
74
  noEmit: true
@@ -102,7 +102,7 @@ function literalValuesOf(types) {
102
102
  for (const member of types) {
103
103
  if (member.isStringLiteral() || member.isNumberLiteral()) {
104
104
  values.push(member.value);
105
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
105
+ } else if (member.flags & import_typescript4.default.TypeFlags.BooleanLiteral) {
106
106
  values.push(intrinsicName(member) === "true");
107
107
  } else {
108
108
  return void 0;
@@ -111,7 +111,7 @@ function literalValuesOf(types) {
111
111
  return values;
112
112
  }
113
113
  function walkUnion(type, ctx) {
114
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
114
+ const flag = import_typescript4.default.TypeFlags.Undefined | import_typescript4.default.TypeFlags.Void | import_typescript4.default.TypeFlags.Never;
115
115
  const members = type.types.filter((member) => !(member.flags & flag));
116
116
  if (members.length === 1) {
117
117
  return walkType(members[0], ctx);
@@ -124,13 +124,13 @@ function walkUnion(type, ctx) {
124
124
  }
125
125
  function buildObjectSchema(type, ctx) {
126
126
  const { checker } = ctx;
127
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
127
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript4.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript4.default.IndexKind.Number);
128
128
  const properties = {};
129
129
  const required = [];
130
130
  for (const symbol of checker.getPropertiesOfType(type)) {
131
131
  const name = symbol.getName();
132
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));
133
+ const optional = Boolean(symbol.getFlags() & import_typescript4.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript4.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript4.default.TypeFlags.Undefined));
134
134
  properties[name] = walkType(propType, ctx);
135
135
  if (!optional) {
136
136
  required.push(name);
@@ -189,16 +189,16 @@ function walkObject(type, ctx) {
189
189
  }
190
190
  function walkType(type, ctx) {
191
191
  const flags = type.flags;
192
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
192
+ if (flags & (import_typescript4.default.TypeFlags.Any | import_typescript4.default.TypeFlags.Unknown)) {
193
193
  return {};
194
194
  }
195
- if (flags & import_typescript2.default.TypeFlags.Null) {
195
+ if (flags & import_typescript4.default.TypeFlags.Null) {
196
196
  return { type: "null" };
197
197
  }
198
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
198
+ if (flags & (import_typescript4.default.TypeFlags.Undefined | import_typescript4.default.TypeFlags.Void)) {
199
199
  return {};
200
200
  }
201
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
201
+ if (flags & (import_typescript4.default.TypeFlags.BigInt | import_typescript4.default.TypeFlags.BigIntLiteral)) {
202
202
  ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
203
203
  return { type: "string" };
204
204
  }
@@ -208,45 +208,45 @@ function walkType(type, ctx) {
208
208
  if (type.isNumberLiteral()) {
209
209
  return { type: "number", const: type.value };
210
210
  }
211
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
211
+ if (flags & import_typescript4.default.TypeFlags.BooleanLiteral) {
212
212
  return { type: "boolean", const: intrinsicName(type) === "true" };
213
213
  }
214
- if (flags & import_typescript2.default.TypeFlags.String) {
214
+ if (flags & import_typescript4.default.TypeFlags.String) {
215
215
  return { type: "string" };
216
216
  }
217
- if (flags & import_typescript2.default.TypeFlags.Number) {
217
+ if (flags & import_typescript4.default.TypeFlags.Number) {
218
218
  return { type: "number" };
219
219
  }
220
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
220
+ if (flags & import_typescript4.default.TypeFlags.Boolean) {
221
221
  return { type: "boolean" };
222
222
  }
223
223
  if (type.isUnion()) {
224
224
  return walkUnion(type, ctx);
225
225
  }
226
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
226
+ if (flags & import_typescript4.default.TypeFlags.Object || type.isIntersection()) {
227
227
  return walkObject(type, ctx);
228
228
  }
229
229
  return {};
230
230
  }
231
- var import_typescript2;
231
+ var import_typescript4;
232
232
  var init_json_schema = __esm({
233
233
  "src/generator/schema/json-schema.ts"() {
234
234
  "use strict";
235
- import_typescript2 = __toESM(require("typescript"));
235
+ import_typescript4 = __toESM(require("typescript"));
236
236
  }
237
237
  });
238
238
 
239
239
  // src/generator/schema/responses.ts
240
240
  function findHandleFunction(source) {
241
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);
242
+ const isExported = (node) => import_typescript5.default.canHaveModifiers(node) && (import_typescript5.default.getModifiers(node)?.some((m) => m.kind === import_typescript5.default.SyntaxKind.ExportKeyword) ?? false);
243
243
  for (const statement of source.statements) {
244
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
244
+ if (import_typescript5.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
245
245
  found = statement;
246
246
  }
247
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
247
+ if (import_typescript5.default.isVariableStatement(statement) && isExported(statement)) {
248
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))) {
249
+ if (import_typescript5.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript5.default.isArrowFunction(declaration.initializer) || import_typescript5.default.isFunctionExpression(declaration.initializer))) {
250
250
  found = declaration.initializer;
251
251
  }
252
252
  }
@@ -255,7 +255,7 @@ function findHandleFunction(source) {
255
255
  return found;
256
256
  }
257
257
  function collectReturnExpressions(fn) {
258
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
258
+ if (import_typescript5.default.isArrowFunction(fn) && !import_typescript5.default.isBlock(fn.body)) {
259
259
  return [fn.body];
260
260
  }
261
261
  if (!fn.body) {
@@ -263,15 +263,15 @@ function collectReturnExpressions(fn) {
263
263
  }
264
264
  const expressions = [];
265
265
  const visit = (node) => {
266
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
266
+ if (import_typescript5.default.isFunctionDeclaration(node) || import_typescript5.default.isFunctionExpression(node) || import_typescript5.default.isArrowFunction(node)) {
267
267
  return;
268
268
  }
269
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
269
+ if (import_typescript5.default.isReturnStatement(node) && node.expression) {
270
270
  expressions.push(node.expression);
271
271
  }
272
- import_typescript3.default.forEachChild(node, visit);
272
+ import_typescript5.default.forEachChild(node, visit);
273
273
  };
274
- import_typescript3.default.forEachChild(fn.body, visit);
274
+ import_typescript5.default.forEachChild(fn.body, visit);
275
275
  return expressions;
276
276
  }
277
277
  function propertyType(checker, type, name, location) {
@@ -283,15 +283,21 @@ function isTypedResponse2(checker, type) {
283
283
  checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
284
284
  );
285
285
  }
286
- function readFromCall(checker, expression) {
287
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
286
+ function firstParameterName(fn) {
287
+ const [first] = fn.parameters;
288
+ return first && import_typescript5.default.isIdentifier(first.name) ? first.name.text : void 0;
289
+ }
290
+ function readFromCall(checker, expression, contextName) {
291
+ if (!import_typescript5.default.isCallExpression(expression) || !import_typescript5.default.isPropertyAccessExpression(expression.expression)) {
288
292
  return void 0;
289
293
  }
290
294
  const method = expression.expression.name.text;
291
295
  if (method !== "json" && method !== "text") {
292
296
  return void 0;
293
297
  }
294
- if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
298
+ const target = expression.expression.expression;
299
+ const directContextCall = contextName && import_typescript5.default.isIdentifier(target) && target.text === contextName;
300
+ if (!directContextCall && !isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
295
301
  return void 0;
296
302
  }
297
303
  const [dataArg, statusArg] = expression.arguments;
@@ -331,6 +337,7 @@ function extractRouteResponses(program, file) {
331
337
  return result;
332
338
  }
333
339
  const ctx = createWalkContext(checker, fn);
340
+ const contextName = firstParameterName(fn);
334
341
  const byStatus = /* @__PURE__ */ new Map();
335
342
  const record = (hit) => {
336
343
  const schema = walkType(hit.data, ctx);
@@ -339,7 +346,7 @@ function extractRouteResponses(program, file) {
339
346
  byStatus.set(hit.status, bucket);
340
347
  };
341
348
  for (const expression of collectReturnExpressions(fn)) {
342
- const fromCall = readFromCall(checker, expression);
349
+ const fromCall = readFromCall(checker, expression, contextName);
343
350
  if (fromCall) {
344
351
  record(fromCall);
345
352
  continue;
@@ -365,11 +372,11 @@ function extractRouteResponses(program, file) {
365
372
  result.$defs = ctx.defs;
366
373
  return result;
367
374
  }
368
- var import_typescript3;
375
+ var import_typescript5;
369
376
  var init_responses = __esm({
370
377
  "src/generator/schema/responses.ts"() {
371
378
  "use strict";
372
- import_typescript3 = __toESM(require("typescript"));
379
+ import_typescript5 = __toESM(require("typescript"));
373
380
  init_json_schema();
374
381
  }
375
382
  });
@@ -425,6 +432,21 @@ var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
425
432
 
426
433
  // src/context.ts
427
434
  var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
435
+ var pendingResponseBrand = /* @__PURE__ */ Symbol("giri.pending-response");
436
+ function getPending(context) {
437
+ return context[pendingResponseBrand];
438
+ }
439
+ var unsupportedCookieJar = {
440
+ get: cookiesUnsupported,
441
+ all: cookiesUnsupported,
442
+ set: cookiesUnsupported,
443
+ delete: cookiesUnsupported,
444
+ getSigned: cookiesUnsupported,
445
+ setSigned: cookiesUnsupported
446
+ };
447
+ function cookiesUnsupported() {
448
+ throw new Error("The active adapter does not support cookies.");
449
+ }
428
450
  function createTypedResponse(data, status, format, headers) {
429
451
  return {
430
452
  [typedResponseBrand]: { data, status, format },
@@ -441,6 +463,13 @@ function createContext(options) {
441
463
  const url = new URL(options.request.url);
442
464
  const store = /* @__PURE__ */ new Map();
443
465
  const validated = options.validated ?? {};
466
+ const pending = { headers: new Headers() };
467
+ const defaultStatus = () => pending.status ?? 200;
468
+ const cookies = options.cookies ? options.cookies({
469
+ request: options.request,
470
+ append: (header) => pending.headers.append("set-cookie", header),
471
+ secret: options.cookieSecret
472
+ }) : unsupportedCookieJar;
444
473
  const context = {
445
474
  params: options.params ?? {},
446
475
  app: options.app ?? {},
@@ -458,16 +487,45 @@ function createContext(options) {
458
487
  throw new Error(`No validated ${String(key)} data is available for this route.`);
459
488
  }
460
489
  return validated[key];
461
- }
490
+ },
491
+ cookie: (name) => cookies.get(name),
492
+ cookies: () => cookies.all(),
493
+ signedCookie: (name) => cookies.getSigned(name)
462
494
  },
463
495
  set: (key, value) => {
464
496
  store.set(key, value);
465
497
  },
466
498
  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)
499
+ json: (data, status, headers) => createTypedResponse(data, status ?? defaultStatus(), "json", headers),
500
+ text: (text, status, headers) => createTypedResponse(text, status ?? defaultStatus(), "text", headers),
501
+ html: (html, status, headers) => createTypedResponse(html, status ?? defaultStatus(), "html", headers),
502
+ body: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
503
+ newResponse: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
504
+ redirect: (location, status) => new Response(null, { status: status ?? 302, headers: { Location: location } }),
505
+ notFound: () => new Response("404 Not Found", { status: 404 }),
506
+ header: (name, value, options2) => {
507
+ if (value === void 0) {
508
+ pending.headers.delete(name);
509
+ } else if (options2?.append) {
510
+ pending.headers.append(name, value);
511
+ } else {
512
+ pending.headers.set(name, value);
513
+ }
514
+ },
515
+ status: (code) => {
516
+ pending.status = code;
517
+ },
518
+ cookie: (name, value, options2) => {
519
+ if (value === null) {
520
+ cookies.delete(name, options2);
521
+ } else {
522
+ cookies.set(name, value, options2);
523
+ }
524
+ },
525
+ signedCookie: (name, value, options2) => cookies.setSigned(name, value, options2)
469
526
  };
470
527
  context[nativeContextBrand] = options.native;
528
+ context[pendingResponseBrand] = pending;
471
529
  return context;
472
530
  }
473
531
  function typedResponseToResponse(response) {
@@ -478,14 +536,41 @@ function typedResponseToResponse(response) {
478
536
  if (response.format === "text" && !headers.has("content-type")) {
479
537
  headers.set("content-type", "text/plain; charset=utf-8");
480
538
  }
539
+ if (response.format === "html" && !headers.has("content-type")) {
540
+ headers.set("content-type", "text/html; charset=utf-8");
541
+ }
481
542
  const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
482
543
  return new Response(body, {
483
544
  status: response.status,
484
545
  headers
485
546
  });
486
547
  }
487
- function toResponse(response) {
488
- return isTypedResponse(response) ? typedResponseToResponse(response) : response;
548
+ function toResponse(response, context) {
549
+ const base = isTypedResponse(response) ? typedResponseToResponse(response) : response;
550
+ const pending = context ? getPending(context) : void 0;
551
+ if (!pending) {
552
+ return base;
553
+ }
554
+ let hasPending = false;
555
+ pending.headers.forEach(() => {
556
+ hasPending = true;
557
+ });
558
+ if (!hasPending) {
559
+ return base;
560
+ }
561
+ const headers = new Headers(base.headers);
562
+ pending.headers.forEach((value, key) => {
563
+ if (key === "set-cookie") {
564
+ return;
565
+ }
566
+ if (!headers.has(key)) {
567
+ headers.set(key, value);
568
+ }
569
+ });
570
+ for (const cookie of pending.headers.getSetCookie?.() ?? []) {
571
+ headers.append("set-cookie", cookie);
572
+ }
573
+ return new Response(base.body, { status: base.status, statusText: base.statusText, headers });
489
574
  }
490
575
  async function composeMiddleware(middleware, handle, context) {
491
576
  let index = -1;
@@ -686,7 +771,8 @@ var configSchema = import_typebox.Type.Object({
686
771
  port: import_typebox.Type.Optional(import_typebox.Type.Number()),
687
772
  hostname: import_typebox.Type.Optional(import_typebox.Type.String())
688
773
  }, { additionalProperties: false })),
689
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
774
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any()),
775
+ cookieSecret: import_typebox.Type.Optional(import_typebox.Type.String())
690
776
  }, { additionalProperties: false });
691
777
 
692
778
  // src/loader/loader.ts
@@ -731,6 +817,7 @@ var import_node_fs2 = require("fs");
731
817
  var import_promises = require("fs/promises");
732
818
  var import_node_path2 = require("path");
733
819
  var import_tinyglobby = require("tinyglobby");
820
+ var import_typescript = __toESM(require("typescript"));
734
821
  var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
735
822
  var METHOD_FROM_FILE = new Map(
736
823
  METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
@@ -748,13 +835,120 @@ function methodFromFile(fileName) {
748
835
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
749
836
  return METHOD_FROM_FILE.get(stem);
750
837
  }
751
- function sharedFileIn(dir) {
838
+ function hasExportModifier(node) {
839
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
840
+ }
841
+ function hasDeclareModifier(node) {
842
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.DeclareKeyword) ?? false);
843
+ }
844
+ function propertyName(node) {
845
+ if (import_typescript.default.isIdentifier(node) || import_typescript.default.isStringLiteralLike(node) || import_typescript.default.isNumericLiteral(node)) {
846
+ return node.text;
847
+ }
848
+ if (import_typescript.default.isPropertyAccessExpression(node)) {
849
+ return node.name.text;
850
+ }
851
+ if (import_typescript.default.isElementAccessExpression(node) && node.argumentExpression) {
852
+ const argument = node.argumentExpression;
853
+ if (import_typescript.default.isStringLiteralLike(argument)) {
854
+ return argument.text;
855
+ }
856
+ }
857
+ return void 0;
858
+ }
859
+ function isExportsObject(node) {
860
+ return import_typescript.default.isIdentifier(node) && node.text === "exports";
861
+ }
862
+ function isModuleExports(node) {
863
+ return import_typescript.default.isPropertyAccessExpression(node) && import_typescript.default.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
864
+ }
865
+ function isCommonJsHandleTarget(node) {
866
+ if (!import_typescript.default.isPropertyAccessExpression(node) && !import_typescript.default.isElementAccessExpression(node)) {
867
+ return false;
868
+ }
869
+ return propertyName(node) === "handle" && (isExportsObject(node.expression) || isModuleExports(node.expression));
870
+ }
871
+ function objectExportsHandle(node) {
872
+ if (!import_typescript.default.isObjectLiteralExpression(node)) {
873
+ return false;
874
+ }
875
+ return node.properties.some((property) => {
876
+ if (import_typescript.default.isShorthandPropertyAssignment(property)) {
877
+ return property.name.text === "handle";
878
+ }
879
+ return (import_typescript.default.isPropertyAssignment(property) || import_typescript.default.isMethodDeclaration(property)) && propertyName(property.name) === "handle";
880
+ });
881
+ }
882
+ function hasNamedHandleExport(source) {
883
+ for (const statement of source.statements) {
884
+ if (hasExportModifier(statement) && !hasDeclareModifier(statement) && import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
885
+ return true;
886
+ }
887
+ if (hasExportModifier(statement) && !hasDeclareModifier(statement) && import_typescript.default.isVariableStatement(statement)) {
888
+ if (statement.declarationList.declarations.some(
889
+ (declaration) => import_typescript.default.isIdentifier(declaration.name) && declaration.name.text === "handle"
890
+ )) {
891
+ return true;
892
+ }
893
+ }
894
+ if (import_typescript.default.isExportDeclaration(statement) && !statement.isTypeOnly && statement.exportClause && import_typescript.default.isNamedExports(statement.exportClause)) {
895
+ if (statement.exportClause.elements.some(
896
+ (element) => !element.isTypeOnly && element.name.text === "handle"
897
+ )) {
898
+ return true;
899
+ }
900
+ }
901
+ if (!import_typescript.default.isExpressionStatement(statement) || !import_typescript.default.isBinaryExpression(statement.expression)) {
902
+ continue;
903
+ }
904
+ const assignment = statement.expression;
905
+ if (assignment.operatorToken.kind !== import_typescript.default.SyntaxKind.EqualsToken) {
906
+ continue;
907
+ }
908
+ if (isCommonJsHandleTarget(assignment.left) || isModuleExports(assignment.left) && objectExportsHandle(assignment.right)) {
909
+ return true;
910
+ }
911
+ }
912
+ return false;
913
+ }
914
+ function parseSource(file) {
915
+ return import_typescript.default.createSourceFile(
916
+ file,
917
+ (0, import_node_fs2.readFileSync)(file, "utf8"),
918
+ import_typescript.default.ScriptTarget.Latest,
919
+ true
920
+ );
921
+ }
922
+ function parseDiagnostics(source) {
923
+ return source.parseDiagnostics ?? [];
924
+ }
925
+ function formatSyntaxDiagnostic(diagnostic) {
926
+ const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
927
+ const message = import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
928
+ return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} - error TS${diagnostic.code}: ${message}`;
929
+ }
930
+ function assertRouteHandleExport(file) {
931
+ const source = parseSource(file);
932
+ const diagnostics = parseDiagnostics(source);
933
+ if (diagnostics.length > 0) {
934
+ throw new SyntaxError(diagnostics.map(formatSyntaxDiagnostic).join("\n"));
935
+ }
936
+ if (!hasNamedHandleExport(source)) {
937
+ throw new Error(`${file} must export a named handle function.`);
938
+ }
939
+ }
940
+ function sharedFileIn(dir, cache) {
941
+ if (cache?.has(dir)) {
942
+ return cache.get(dir);
943
+ }
752
944
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
753
945
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
754
946
  if ((0, import_node_fs2.existsSync)(file)) {
947
+ cache?.set(dir, file);
755
948
  return file;
756
949
  }
757
950
  }
951
+ cache?.set(dir, void 0);
758
952
  return void 0;
759
953
  }
760
954
  function physicalRouteSegments(routesDir, routeDir) {
@@ -823,7 +1017,7 @@ async function scanRouteFolders(routesDir) {
823
1017
  function routeParamsForDir(routesDir, dir) {
824
1018
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
825
1019
  }
826
- function sharedFilesForDir(routesDir, dir) {
1020
+ function sharedFilesForDir(routesDir, dir, cache) {
827
1021
  const segments = physicalRouteSegments(routesDir, dir);
828
1022
  const dirs = [routesDir];
829
1023
  let current = routesDir;
@@ -831,7 +1025,7 @@ function sharedFilesForDir(routesDir, dir) {
831
1025
  current = (0, import_node_path2.join)(current, segment);
832
1026
  dirs.push(current);
833
1027
  }
834
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
1028
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
835
1029
  }
836
1030
  async function scanRoutes(routesDir) {
837
1031
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -843,6 +1037,7 @@ async function scanRoutes(routesDir) {
843
1037
  onlyFiles: true
844
1038
  });
845
1039
  const routes = [];
1040
+ const sharedCache = /* @__PURE__ */ new Map();
846
1041
  for (const file of files) {
847
1042
  const method = methodFromFile((0, import_node_path2.basename)(file));
848
1043
  if (!method) {
@@ -858,7 +1053,7 @@ async function scanRoutes(routesDir) {
858
1053
  routeDir,
859
1054
  routeSegments,
860
1055
  params,
861
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
1056
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
862
1057
  });
863
1058
  }
864
1059
  return routes.sort((left, right) => {
@@ -871,9 +1066,11 @@ async function scanRoutes(routesDir) {
871
1066
  }
872
1067
 
873
1068
  // src/app.ts
874
- function loadModule(file) {
1069
+ function loadModule(file, force = true) {
875
1070
  const resolved = require.resolve(file);
876
- delete require.cache[resolved];
1071
+ if (force) {
1072
+ delete require.cache[resolved];
1073
+ }
877
1074
  return require(resolved);
878
1075
  }
879
1076
  function interopDefault(value) {
@@ -935,20 +1132,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
935
1132
  }
936
1133
  function matchAlias(request, key) {
937
1134
  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);
1135
+ const [prefix, suffix = ""] = key.split("*");
1136
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
1137
+ return request.slice(prefix.length, request.length - suffix.length);
941
1138
  }
942
1139
  return void 0;
943
1140
  }
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;
1141
+ return request === key ? "" : void 0;
952
1142
  }
953
1143
  function resolveAliasRequest(request, alias, cwd) {
954
1144
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -1010,40 +1200,85 @@ function resolveGiriPaths(config, cwd = process.cwd()) {
1010
1200
  async function buildGiriApp(config, options = {}) {
1011
1201
  const paths = resolveGiriPaths(config, options.cwd);
1012
1202
  const routes = await scanRoutes(paths.routesDir);
1203
+ if (options.lazy) {
1204
+ for (const route of routes) {
1205
+ assertRouteHandleExport(route.file);
1206
+ }
1207
+ }
1013
1208
  const app = config.adapter.createApp();
1014
1209
  ensureGiriAliasResolver(paths.outDir);
1015
- const { unregister } = await safeRegister();
1016
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
1210
+ if (options.lazy && (!options.loaderRegistered || !options.aliasResolverRegistered)) {
1211
+ throw new Error("Lazy route loading requires persistent loader and alias registrations.");
1212
+ }
1213
+ const loader = options.loaderRegistered ? void 0 : await safeRegister();
1214
+ const unregisterAliasResolver = options.aliasResolverRegistered ? void 0 : registerAliasResolver(config.alias, paths.cwd);
1017
1215
  try {
1018
- for (const route of routes) {
1019
- const routeModule = loadModule(route.file);
1216
+ const dirty = options.dirty;
1217
+ const forceReload = dirty === void 0;
1218
+ const isDirty = (file) => forceReload || dirty.has(file);
1219
+ const sharedCache = /* @__PURE__ */ new Map();
1220
+ const loadShared = (file) => {
1221
+ if (!sharedCache.has(file)) {
1222
+ sharedCache.set(file, loadModule(file, isDirty(file)));
1223
+ }
1224
+ return sharedCache.get(file);
1225
+ };
1226
+ const runtimeFor = (route) => {
1227
+ const routeModule = loadModule(route.file, isDirty(route.file));
1020
1228
  if (typeof routeModule.handle !== "function") {
1021
1229
  throw new Error(`${route.file} must export a named handle function.`);
1022
1230
  }
1023
1231
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
1024
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
1232
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
1025
1233
  );
1026
1234
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
1027
- config.adapter.register(app, {
1235
+ return {
1028
1236
  method: route.method,
1029
1237
  path: route.path,
1030
1238
  handle: routeModule.handle,
1031
1239
  middleware: [...folderMiddleware, ...verbMiddleware],
1032
1240
  input: routeInput(routeModule, route.file),
1033
- services: options.services
1241
+ services: options.services,
1242
+ cookieSecret: config.cookieSecret
1243
+ };
1244
+ };
1245
+ for (const route of routes) {
1246
+ if (!options.lazy) {
1247
+ config.adapter.register(app, runtimeFor(route));
1248
+ continue;
1249
+ }
1250
+ let runtime;
1251
+ const getRuntime = () => {
1252
+ runtime ??= runtimeFor(route);
1253
+ return runtime;
1254
+ };
1255
+ config.adapter.register(app, {
1256
+ method: route.method,
1257
+ path: route.path,
1258
+ get handle() {
1259
+ return getRuntime().handle;
1260
+ },
1261
+ get middleware() {
1262
+ return getRuntime().middleware;
1263
+ },
1264
+ get input() {
1265
+ return getRuntime().input;
1266
+ },
1267
+ services: options.services,
1268
+ cookieSecret: config.cookieSecret
1034
1269
  });
1035
1270
  }
1036
1271
  } finally {
1037
- unregisterAliasResolver();
1038
- unregister();
1272
+ unregisterAliasResolver?.();
1273
+ loader?.unregister();
1039
1274
  }
1040
1275
  return { app, routes, paths };
1041
1276
  }
1042
1277
 
1043
1278
  // 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");
1279
+ var import_node_fs8 = require("fs");
1280
+ var import_promises4 = require("fs/promises");
1281
+ var import_node_path12 = require("path");
1047
1282
 
1048
1283
  // src/generator/app-types.ts
1049
1284
  var import_node_fs4 = require("fs");
@@ -1295,6 +1530,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1295
1530
  const documentPaths = {};
1296
1531
  const schemas = {};
1297
1532
  const securitySchemes = {};
1533
+ const tagOrder = [];
1298
1534
  for (const route of routes) {
1299
1535
  if (data.hiddenFiles?.has(route.file)) {
1300
1536
  continue;
@@ -1302,10 +1538,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1302
1538
  const responses = data.responsesByFile?.get(route.file);
1303
1539
  const input = data.inputsByFile?.get(route.file);
1304
1540
  const security = data.securityByFile?.get(route.file);
1541
+ const meta = data.openapiByFile?.get(route.file);
1305
1542
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1306
1543
  schemas[name] = rewriteRefs(schema);
1307
1544
  }
1308
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1545
+ const operation = {};
1546
+ if (meta?.tags && meta.tags.length > 0) {
1547
+ operation.tags = meta.tags;
1548
+ for (const tag of meta.tags) {
1549
+ if (!tagOrder.includes(tag)) {
1550
+ tagOrder.push(tag);
1551
+ }
1552
+ }
1553
+ }
1554
+ if (meta?.summary) {
1555
+ operation.summary = meta.summary;
1556
+ }
1557
+ if (meta?.description) {
1558
+ operation.description = meta.description;
1559
+ }
1560
+ if (meta?.operationId) {
1561
+ operation.operationId = meta.operationId;
1562
+ }
1563
+ if (meta?.deprecated) {
1564
+ operation.deprecated = true;
1565
+ }
1566
+ operation.responses = buildResponses(responses?.responses ?? []);
1309
1567
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1310
1568
  if (parameters.length > 0) {
1311
1569
  operation.parameters = parameters;
@@ -1337,6 +1595,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1337
1595
  info: readProjectInfo(paths.cwd),
1338
1596
  paths: documentPaths
1339
1597
  };
1598
+ if (tagOrder.length > 0) {
1599
+ document.tags = tagOrder.map((name) => ({ name }));
1600
+ }
1340
1601
  const components = {};
1341
1602
  if (Object.keys(schemas).length > 0) {
1342
1603
  components.schemas = schemas;
@@ -1368,14 +1629,29 @@ function paramsType(params) {
1368
1629
  ${fields}
1369
1630
  }`;
1370
1631
  }
1371
- function varsType(typesDir, sharedFiles) {
1372
- if (sharedFiles.length === 0) {
1373
- return "{}";
1632
+ function middlewareVarsType(typesDir, sharedFile) {
1633
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1634
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1635
+ }
1636
+ function ownSharedFile(dir, sharedFiles) {
1637
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1638
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1639
+ return sharedFiles[index];
1640
+ }
1374
1641
  }
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 & ");
1642
+ return void 0;
1643
+ }
1644
+ function varsType(paths, file, dir, sharedFiles) {
1645
+ const typesDir = (0, import_node_path8.dirname)(file);
1646
+ const parts = [];
1647
+ if (dir !== paths.routesDir) {
1648
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1649
+ }
1650
+ const ownShared = ownSharedFile(dir, sharedFiles);
1651
+ if (ownShared) {
1652
+ parts.push(middlewareVarsType(typesDir, ownShared));
1653
+ }
1654
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1379
1655
  }
1380
1656
  function methodExports(typesDir, verbs) {
1381
1657
  return verbs.map(({ method, file }) => {
@@ -1386,14 +1662,14 @@ function methodExports(typesDir, verbs) {
1386
1662
  });
1387
1663
  }
1388
1664
  async function writeParamTypes(paths, folders) {
1389
- for (const { dir, params, sharedFiles, verbs } of folders) {
1665
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1390
1666
  const file = typeFilePath(paths, dir);
1391
1667
  const typesDir = (0, import_node_path8.dirname)(file);
1392
1668
  const lines = [
1393
1669
  GENERATED_HEADER,
1394
1670
  `export type Params = ${paramsType(params)};`,
1395
1671
  "export type RouteParams = Params;",
1396
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1672
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1397
1673
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1398
1674
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1399
1675
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1403,10 +1679,14 @@ async function writeParamTypes(paths, folders) {
1403
1679
  lines.push(...methodExports(typesDir, verbs));
1404
1680
  }
1405
1681
  lines.push("");
1406
- await writeGenerated(file, lines.join("\n"));
1407
- }
1682
+ return writeGenerated(file, lines.join("\n"));
1683
+ }));
1408
1684
  }
1409
1685
 
1686
+ // src/generator/route-meta.ts
1687
+ var import_node_fs6 = require("fs");
1688
+ var import_typescript2 = __toESM(require("typescript"));
1689
+
1410
1690
  // src/generator/inputs.ts
1411
1691
  function sanitize(schema) {
1412
1692
  const { $schema, ...rest } = schema;
@@ -1467,28 +1747,315 @@ function readInput(routeModule) {
1467
1747
  }
1468
1748
  return input.body || input.query ? input : void 0;
1469
1749
  }
1470
- function hiddenFrom(value) {
1471
- if (value === false) {
1750
+ function hasExportModifier2(node) {
1751
+ return import_typescript2.default.canHaveModifiers(node) && (import_typescript2.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript2.default.SyntaxKind.ExportKeyword) ?? false);
1752
+ }
1753
+ function unwrapExpression(expression) {
1754
+ let current = expression;
1755
+ while (import_typescript2.default.isParenthesizedExpression(current) || import_typescript2.default.isAsExpression(current) || import_typescript2.default.isSatisfiesExpression(current)) {
1756
+ current = current.expression;
1757
+ }
1758
+ return current;
1759
+ }
1760
+ function staticBoolean(expression) {
1761
+ const value = unwrapExpression(expression);
1762
+ if (value.kind === import_typescript2.default.SyntaxKind.TrueKeyword) {
1472
1763
  return true;
1473
1764
  }
1474
- if (value === true) {
1765
+ if (value.kind === import_typescript2.default.SyntaxKind.FalseKeyword) {
1475
1766
  return false;
1476
1767
  }
1477
- if (value && typeof value === "object" && "hidden" in value) {
1478
- return Boolean(value.hidden);
1768
+ return void 0;
1769
+ }
1770
+ function staticString(expression) {
1771
+ const value = unwrapExpression(expression);
1772
+ return import_typescript2.default.isStringLiteralLike(value) ? value.text : void 0;
1773
+ }
1774
+ function staticStringArray(expression) {
1775
+ const value = unwrapExpression(expression);
1776
+ if (!import_typescript2.default.isArrayLiteralExpression(value)) {
1777
+ return void 0;
1778
+ }
1779
+ const strings = [];
1780
+ for (const element of value.elements) {
1781
+ const string = staticString(element);
1782
+ if (string === void 0) {
1783
+ return void 0;
1784
+ }
1785
+ strings.push(string);
1786
+ }
1787
+ return strings;
1788
+ }
1789
+ function propertyName2(name) {
1790
+ if (import_typescript2.default.isIdentifier(name) || import_typescript2.default.isStringLiteral(name) || import_typescript2.default.isNumericLiteral(name)) {
1791
+ return name.text;
1479
1792
  }
1480
1793
  return void 0;
1481
1794
  }
1482
- function collectHidden(route, routeModule, loadShared) {
1795
+ function collectImportedNames(source) {
1796
+ const names = /* @__PURE__ */ new Set();
1797
+ for (const statement of source.statements) {
1798
+ if (!import_typescript2.default.isImportDeclaration(statement)) {
1799
+ continue;
1800
+ }
1801
+ const clause = statement.importClause;
1802
+ if (!clause) {
1803
+ continue;
1804
+ }
1805
+ if (clause.name) {
1806
+ names.add(clause.name.text);
1807
+ }
1808
+ const bindings = clause.namedBindings;
1809
+ if (bindings && import_typescript2.default.isNamespaceImport(bindings)) {
1810
+ names.add(bindings.name.text);
1811
+ }
1812
+ if (bindings && import_typescript2.default.isNamedImports(bindings)) {
1813
+ for (const element of bindings.elements) {
1814
+ names.add(element.name.text);
1815
+ }
1816
+ }
1817
+ }
1818
+ return names;
1819
+ }
1820
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1821
+ let found = false;
1822
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1823
+ const visit = (node) => {
1824
+ if (found) {
1825
+ return;
1826
+ }
1827
+ if (import_typescript2.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1828
+ found = true;
1829
+ return;
1830
+ }
1831
+ import_typescript2.default.forEachChild(node, visit);
1832
+ };
1833
+ visit(expression);
1834
+ return found;
1835
+ }
1836
+ function parseStaticOpenApi(expression) {
1837
+ const value = unwrapExpression(expression);
1838
+ const boolean = staticBoolean(value);
1839
+ if (boolean !== void 0) {
1840
+ return boolean;
1841
+ }
1842
+ if (!import_typescript2.default.isObjectLiteralExpression(value)) {
1843
+ return void 0;
1844
+ }
1845
+ const openapi = {};
1846
+ for (const property of value.properties) {
1847
+ if (!import_typescript2.default.isPropertyAssignment(property)) {
1848
+ return void 0;
1849
+ }
1850
+ const name = propertyName2(property.name);
1851
+ if (!name) {
1852
+ return void 0;
1853
+ }
1854
+ if (name === "hidden") {
1855
+ const hidden = staticBoolean(property.initializer);
1856
+ if (hidden === void 0) {
1857
+ return void 0;
1858
+ }
1859
+ openapi.hidden = hidden;
1860
+ } else if (name === "tags") {
1861
+ const tags = staticStringArray(property.initializer);
1862
+ if (!tags) {
1863
+ return void 0;
1864
+ }
1865
+ openapi.tags = tags;
1866
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1867
+ const string = staticString(property.initializer);
1868
+ if (string === void 0) {
1869
+ return void 0;
1870
+ }
1871
+ openapi[name] = string;
1872
+ } else if (name === "deprecated") {
1873
+ const deprecated = staticBoolean(property.initializer);
1874
+ if (deprecated === void 0) {
1875
+ return void 0;
1876
+ }
1877
+ openapi.deprecated = deprecated;
1878
+ } else {
1879
+ return void 0;
1880
+ }
1881
+ }
1882
+ return openapi;
1883
+ }
1884
+ function readStaticModuleMeta(file) {
1885
+ let source;
1886
+ try {
1887
+ source = import_typescript2.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript2.default.ScriptTarget.Latest, true);
1888
+ } catch {
1889
+ return void 0;
1890
+ }
1891
+ const importedNames = collectImportedNames(source);
1892
+ const sourceText = source.getFullText();
1893
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1894
+ const meta = { middlewareSecurity: false };
1895
+ for (const statement of source.statements) {
1896
+ if (import_typescript2.default.isImportDeclaration(statement) || import_typescript2.default.isInterfaceDeclaration(statement) || import_typescript2.default.isTypeAliasDeclaration(statement) || import_typescript2.default.isEmptyStatement(statement) || !hasExportModifier2(statement)) {
1897
+ continue;
1898
+ }
1899
+ if (import_typescript2.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1900
+ continue;
1901
+ }
1902
+ if (import_typescript2.default.isVariableStatement(statement)) {
1903
+ for (const declaration of statement.declarationList.declarations) {
1904
+ if (!import_typescript2.default.isIdentifier(declaration.name)) {
1905
+ return void 0;
1906
+ }
1907
+ const name = declaration.name.text;
1908
+ if (name === "openapi") {
1909
+ if (!declaration.initializer) {
1910
+ return void 0;
1911
+ }
1912
+ const openapi = parseStaticOpenApi(declaration.initializer);
1913
+ if (openapi === void 0) {
1914
+ return void 0;
1915
+ }
1916
+ meta.openapi = openapi;
1917
+ } else if (name === "handle") {
1918
+ if (!declaration.initializer || !import_typescript2.default.isArrowFunction(declaration.initializer) && !import_typescript2.default.isFunctionExpression(declaration.initializer)) {
1919
+ return void 0;
1920
+ }
1921
+ continue;
1922
+ } else if (name === "middleware") {
1923
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1924
+ return void 0;
1925
+ }
1926
+ meta.middlewareSecurity = false;
1927
+ continue;
1928
+ } else {
1929
+ return void 0;
1930
+ }
1931
+ }
1932
+ continue;
1933
+ }
1934
+ return void 0;
1935
+ }
1936
+ return meta;
1937
+ }
1938
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1483
1939
  let hidden = false;
1940
+ const tags = [];
1941
+ const meta = {};
1942
+ const apply = (value, isVerb) => {
1943
+ if (value === false) {
1944
+ hidden = true;
1945
+ return;
1946
+ }
1947
+ if (value === true) {
1948
+ hidden = false;
1949
+ return;
1950
+ }
1951
+ if (!value) {
1952
+ return;
1953
+ }
1954
+ if (typeof value.hidden === "boolean") {
1955
+ hidden = value.hidden;
1956
+ }
1957
+ if (value.tags) {
1958
+ tags.push(...value.tags);
1959
+ }
1960
+ if (typeof value.summary === "string") {
1961
+ meta.summary = value.summary;
1962
+ }
1963
+ if (typeof value.description === "string") {
1964
+ meta.description = value.description;
1965
+ }
1966
+ if (typeof value.deprecated === "boolean") {
1967
+ meta.deprecated = value.deprecated;
1968
+ }
1969
+ if (isVerb && typeof value.operationId === "string") {
1970
+ meta.operationId = value.operationId;
1971
+ }
1972
+ };
1484
1973
  for (const file of route.sharedFiles) {
1485
- const opinion = hiddenFrom(loadShared(file).openapi);
1486
- if (opinion !== void 0) {
1487
- hidden = opinion;
1974
+ const shared = loadShared(file);
1975
+ if (!shared) {
1976
+ return void 0;
1488
1977
  }
1978
+ apply(shared.openapi, false);
1979
+ }
1980
+ apply(routeModule.openapi, true);
1981
+ if (tags.length > 0) {
1982
+ meta.tags = [...new Set(tags)];
1489
1983
  }
1490
- const verb = hiddenFrom(routeModule.openapi);
1491
- return verb ?? hidden;
1984
+ return { hidden, meta };
1985
+ }
1986
+ function extractStaticMeta(route, routeModule, loadShared) {
1987
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1988
+ if (!openapi) {
1989
+ return void 0;
1990
+ }
1991
+ const meta = {};
1992
+ if (openapi.hidden) {
1993
+ meta.hidden = true;
1994
+ }
1995
+ if (Object.keys(openapi.meta).length > 0) {
1996
+ meta.openapi = openapi.meta;
1997
+ }
1998
+ return meta;
1999
+ }
2000
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
2001
+ const meta = {};
2002
+ const security = collectSecurity(route, {}, loadShared);
2003
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
2004
+ if (security) {
2005
+ meta.security = security;
2006
+ }
2007
+ if (hidden) {
2008
+ meta.hidden = true;
2009
+ }
2010
+ if (Object.keys(openapi).length > 0) {
2011
+ meta.openapi = openapi;
2012
+ }
2013
+ return meta;
2014
+ }
2015
+ function resolveOpenApi(route, routeModule, loadShared) {
2016
+ let hidden = false;
2017
+ const tags = [];
2018
+ const meta = {};
2019
+ const apply = (value, isVerb) => {
2020
+ if (value === false) {
2021
+ hidden = true;
2022
+ return;
2023
+ }
2024
+ if (value === true) {
2025
+ hidden = false;
2026
+ return;
2027
+ }
2028
+ if (!value || typeof value !== "object") {
2029
+ return;
2030
+ }
2031
+ const o = value;
2032
+ if ("hidden" in o) {
2033
+ hidden = Boolean(o.hidden);
2034
+ }
2035
+ if (Array.isArray(o.tags)) {
2036
+ tags.push(...o.tags.filter((tag) => typeof tag === "string"));
2037
+ }
2038
+ if (typeof o.summary === "string") {
2039
+ meta.summary = o.summary;
2040
+ }
2041
+ if (typeof o.description === "string") {
2042
+ meta.description = o.description;
2043
+ }
2044
+ if (typeof o.deprecated === "boolean") {
2045
+ meta.deprecated = o.deprecated;
2046
+ }
2047
+ if (isVerb && typeof o.operationId === "string") {
2048
+ meta.operationId = o.operationId;
2049
+ }
2050
+ };
2051
+ for (const file of route.sharedFiles) {
2052
+ apply(loadShared(file).openapi, false);
2053
+ }
2054
+ apply(routeModule.openapi, true);
2055
+ if (tags.length > 0) {
2056
+ meta.tags = [...new Set(tags)];
2057
+ }
2058
+ return { hidden, meta };
1492
2059
  }
1493
2060
  function collectSecurity(route, routeModule, loadShared) {
1494
2061
  const skipInherited = Boolean(
@@ -1520,6 +2087,33 @@ function collectSecurity(route, routeModule, loadShared) {
1520
2087
  }
1521
2088
  async function extractRouteMeta(config, paths, routes) {
1522
2089
  const byFile = /* @__PURE__ */ new Map();
2090
+ const remainingRoutes = [];
2091
+ const runtimeSharedRoutes = [];
2092
+ const staticCache = /* @__PURE__ */ new Map();
2093
+ const loadStatic = (file) => {
2094
+ if (!staticCache.has(file)) {
2095
+ staticCache.set(file, readStaticModuleMeta(file));
2096
+ }
2097
+ return staticCache.get(file);
2098
+ };
2099
+ for (const route of routes) {
2100
+ const routeModule = loadStatic(route.file);
2101
+ if (!routeModule) {
2102
+ remainingRoutes.push(route);
2103
+ continue;
2104
+ }
2105
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
2106
+ if (meta) {
2107
+ if (meta.hidden || meta.openapi) {
2108
+ byFile.set(route.file, meta);
2109
+ }
2110
+ continue;
2111
+ }
2112
+ runtimeSharedRoutes.push({ route, routeModule });
2113
+ }
2114
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
2115
+ return byFile;
2116
+ }
1523
2117
  const { unregister } = await safeRegister();
1524
2118
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1525
2119
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1534,13 +2128,19 @@ async function extractRouteMeta(config, paths, routes) {
1534
2128
  return sharedCache.get(file);
1535
2129
  };
1536
2130
  try {
1537
- for (const route of routes) {
2131
+ for (const { route, routeModule } of runtimeSharedRoutes) {
2132
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
2133
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
2134
+ byFile.set(route.file, meta);
2135
+ }
2136
+ }
2137
+ for (const route of remainingRoutes) {
1538
2138
  try {
1539
2139
  const routeModule = loadModule2(route.file);
1540
2140
  const meta = {};
1541
2141
  const input = readInput(routeModule);
1542
2142
  const security = collectSecurity(route, routeModule, loadShared);
1543
- const hidden = collectHidden(route, routeModule, loadShared);
2143
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1544
2144
  if (input) {
1545
2145
  meta.input = input;
1546
2146
  }
@@ -1550,7 +2150,10 @@ async function extractRouteMeta(config, paths, routes) {
1550
2150
  if (hidden) {
1551
2151
  meta.hidden = true;
1552
2152
  }
1553
- if (meta.input || meta.security || meta.hidden) {
2153
+ if (Object.keys(openapi).length > 0) {
2154
+ meta.openapi = openapi;
2155
+ }
2156
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1554
2157
  byFile.set(route.file, meta);
1555
2158
  }
1556
2159
  } catch {
@@ -1622,6 +2225,103 @@ async function writeTsConfig(paths, config) {
1622
2225
  });
1623
2226
  }
1624
2227
 
2228
+ // src/generator/cache.ts
2229
+ var import_node_crypto = require("crypto");
2230
+ var import_node_fs7 = require("fs");
2231
+ var import_promises3 = require("fs/promises");
2232
+ var import_node_path11 = require("path");
2233
+ var import_tinyglobby2 = require("tinyglobby");
2234
+ var CACHE_VERSION = 1;
2235
+ var SYNC_CACHE_NAME = ".sync-cache.json";
2236
+ function stableConfig(config) {
2237
+ const alias = Object.entries(config.alias ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]);
2238
+ return { alias, outDir: config.outDir ?? ".giri" };
2239
+ }
2240
+ async function syncFingerprint(config, paths) {
2241
+ const outRelative = slash((0, import_node_path11.relative)(paths.cwd, paths.outDir));
2242
+ const ignore = ["**/node_modules/**", "**/.git/**"];
2243
+ if (outRelative && !outRelative.startsWith("..")) {
2244
+ ignore.push(`${outRelative}/**`);
2245
+ }
2246
+ const files = await (0, import_tinyglobby2.glob)([
2247
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}",
2248
+ "giri.config.{ts,js,mts,cts,mjs,cjs}",
2249
+ "tsconfig*.json",
2250
+ "package.json",
2251
+ "package-lock.json",
2252
+ "npm-shrinkwrap.json",
2253
+ "yarn.lock",
2254
+ "pnpm-lock.yaml",
2255
+ "bun.lock",
2256
+ "bun.lockb"
2257
+ ], {
2258
+ cwd: paths.cwd,
2259
+ absolute: false,
2260
+ onlyFiles: true,
2261
+ dot: true,
2262
+ ignore
2263
+ });
2264
+ const hash = (0, import_node_crypto.createHash)("sha256");
2265
+ hash.update(JSON.stringify(stableConfig(config)));
2266
+ for (const file of files.sort()) {
2267
+ hash.update("\0");
2268
+ hash.update(slash(file));
2269
+ hash.update("\0");
2270
+ hash.update(await (0, import_promises3.readFile)((0, import_node_path11.resolve)(paths.cwd, file)));
2271
+ }
2272
+ return hash.digest("hex");
2273
+ }
2274
+ function cachePath(paths) {
2275
+ return (0, import_node_path11.join)(paths.outDir, SYNC_CACHE_NAME);
2276
+ }
2277
+ function serializePath(paths, file) {
2278
+ return slash((0, import_node_path11.relative)(paths.cwd, file));
2279
+ }
2280
+ function deserializePath(paths, file) {
2281
+ return slash((0, import_node_path11.resolve)(paths.cwd, file.split("/").join(import_node_path11.sep)));
2282
+ }
2283
+ function serializeMap(paths, values) {
2284
+ return [...values].map(([file, value]) => [serializePath(paths, file), value]);
2285
+ }
2286
+ function deserializeMap(paths, values) {
2287
+ return new Map(values.map(([file, value]) => [deserializePath(paths, file), value]));
2288
+ }
2289
+ async function readSyncCache(paths, fingerprint) {
2290
+ const file = cachePath(paths);
2291
+ if (!(0, import_node_fs7.existsSync)(file)) {
2292
+ return void 0;
2293
+ }
2294
+ try {
2295
+ const cache = JSON.parse(await (0, import_promises3.readFile)(file, "utf8"));
2296
+ if (cache.version !== CACHE_VERSION || cache.fingerprint !== fingerprint) {
2297
+ return void 0;
2298
+ }
2299
+ return {
2300
+ responsesByFile: deserializeMap(paths, cache.data.responsesByFile),
2301
+ inputsByFile: deserializeMap(paths, cache.data.inputsByFile),
2302
+ securityByFile: deserializeMap(paths, cache.data.securityByFile),
2303
+ hiddenFiles: new Set(cache.data.hiddenFiles.map((entry) => deserializePath(paths, entry))),
2304
+ openapiByFile: deserializeMap(paths, cache.data.openapiByFile)
2305
+ };
2306
+ } catch {
2307
+ return void 0;
2308
+ }
2309
+ }
2310
+ async function writeSyncCache(paths, fingerprint, data) {
2311
+ const cache = {
2312
+ version: CACHE_VERSION,
2313
+ fingerprint,
2314
+ data: {
2315
+ responsesByFile: serializeMap(paths, data.responsesByFile),
2316
+ inputsByFile: serializeMap(paths, data.inputsByFile),
2317
+ securityByFile: serializeMap(paths, data.securityByFile),
2318
+ hiddenFiles: [...data.hiddenFiles].map((file) => serializePath(paths, file)),
2319
+ openapiByFile: serializeMap(paths, data.openapiByFile)
2320
+ }
2321
+ };
2322
+ await writeJson(cachePath(paths), cache);
2323
+ }
2324
+
1625
2325
  // src/generator/sync.ts
1626
2326
  async function typeFolders(paths, routes) {
1627
2327
  const verbsByDir = /* @__PURE__ */ new Map();
@@ -1632,10 +2332,11 @@ async function typeFolders(paths, routes) {
1632
2332
  verbsByDir.set(key, list);
1633
2333
  }
1634
2334
  const dirs = await scanRouteFolders(paths.routesDir);
2335
+ const sharedCache = /* @__PURE__ */ new Map();
1635
2336
  return dirs.map((dir) => ({
1636
2337
  dir,
1637
2338
  params: routeParamsForDir(paths.routesDir, dir),
1638
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
2339
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1639
2340
  verbs: verbsByDir.get(slash(dir)) ?? []
1640
2341
  }));
1641
2342
  }
@@ -1647,11 +2348,9 @@ async function extractResponses(paths, routes) {
1647
2348
  try {
1648
2349
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1649
2350
  const files = [...new Set(routes.map((route) => route.file))];
1650
- 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
- );
2351
+ const appTypes = (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts");
2352
+ const roots = (0, import_node_fs8.existsSync)(appTypes) ? [...files, appTypes] : files;
2353
+ const program = createSchemaProgram2(paths, roots);
1655
2354
  for (const file of files) {
1656
2355
  byFile.set(file, extractRouteResponses2(program, file));
1657
2356
  }
@@ -1664,8 +2363,9 @@ async function extractMeta(config, paths, routes) {
1664
2363
  const inputsByFile = /* @__PURE__ */ new Map();
1665
2364
  const securityByFile = /* @__PURE__ */ new Map();
1666
2365
  const hiddenFiles = /* @__PURE__ */ new Set();
2366
+ const openapiByFile = /* @__PURE__ */ new Map();
1667
2367
  if (routes.length === 0) {
1668
- return { inputsByFile, securityByFile, hiddenFiles };
2368
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1669
2369
  }
1670
2370
  try {
1671
2371
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1679,63 +2379,91 @@ async function extractMeta(config, paths, routes) {
1679
2379
  if (entry.hidden) {
1680
2380
  hiddenFiles.add(file);
1681
2381
  }
2382
+ if (entry.openapi) {
2383
+ openapiByFile.set(file, entry.openapi);
2384
+ }
1682
2385
  }
1683
2386
  } catch (error) {
1684
2387
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1685
2388
  }
1686
- return { inputsByFile, securityByFile, hiddenFiles };
2389
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1687
2390
  }
1688
2391
  async function syncProject(config, options = {}) {
1689
2392
  const paths = resolveGiriPaths(config, options.cwd);
1690
2393
  assertSafeOutDir(paths);
2394
+ const hadOutDir = (0, import_node_fs8.existsSync)(paths.outDir);
1691
2395
  const routes = await scanRoutes(paths.routesDir);
1692
2396
  const folders = await typeFolders(paths, routes);
1693
- await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
2397
+ const fingerprint = await syncFingerprint(config, paths);
2398
+ const cached = await readSyncCache(paths, fingerprint);
2399
+ const generatedFiles = [
2400
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2401
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2402
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2403
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2404
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2405
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2406
+ ];
2407
+ if (cached && generatedFiles.every(import_node_fs8.existsSync)) {
2408
+ return { paths, routes, folders, data: cached };
2409
+ }
2410
+ await (0, import_promises4.mkdir)(paths.outDir, { recursive: true });
1694
2411
  await writeParamTypes(paths, folders);
1695
2412
  await writeRouteTypes(paths, routes);
1696
2413
  await writeAppTypes(paths);
1697
2414
  await writeTsConfig(paths, config);
1698
- const responsesByFile = await extractResponses(paths, routes);
1699
- const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1700
- const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
2415
+ const data = cached ?? {
2416
+ responsesByFile: await extractResponses(paths, routes),
2417
+ ...await extractMeta(config, paths, routes)
2418
+ };
1701
2419
  await writeManifest(paths, routes, data);
1702
2420
  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
- );
2421
+ await writeSyncCache(paths, fingerprint, data);
2422
+ if (hadOutDir) {
2423
+ await pruneDir(
2424
+ paths.outDir,
2425
+ /* @__PURE__ */ new Set([
2426
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2427
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2428
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2429
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2430
+ (0, import_node_path12.join)(paths.outDir, SYNC_CACHE_NAME),
2431
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2432
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2433
+ ])
2434
+ );
2435
+ }
1714
2436
  return { paths, routes, folders, data };
1715
2437
  }
1716
2438
 
1717
2439
  // src/generator/watch.ts
1718
- var import_node_fs7 = require("fs");
2440
+ var import_node_fs10 = require("fs");
2441
+ var import_node_path15 = require("path");
2442
+
2443
+ // src/loader/import-graph.ts
2444
+ var import_node_fs9 = require("fs");
1719
2445
  var import_node_path13 = require("path");
2446
+ var import_typescript6 = __toESM(require("typescript"));
2447
+ var import_tinyglobby3 = require("tinyglobby");
1720
2448
 
1721
2449
  // src/loader/module-loader.ts
1722
- var import_node_path12 = require("path");
2450
+ var import_node_path14 = require("path");
1723
2451
 
1724
2452
  // src/lifecycle.ts
1725
- var import_node_fs8 = require("fs");
1726
- var import_node_path14 = require("path");
2453
+ var import_node_fs11 = require("fs");
2454
+ var import_node_path16 = require("path");
1727
2455
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1728
2456
  function resolveMainFile(cwd) {
1729
2457
  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)) {
2458
+ const file = (0, import_node_path16.join)(cwd, "src", `main.${ext}`);
2459
+ if ((0, import_node_fs11.existsSync)(file)) {
1732
2460
  return file;
1733
2461
  }
1734
2462
  }
1735
2463
  return void 0;
1736
2464
  }
1737
2465
  async function loadLifecycle(cwd = process.cwd()) {
1738
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2466
+ const file = resolveMainFile((0, import_node_path16.resolve)(cwd));
1739
2467
  if (!file) {
1740
2468
  return {};
1741
2469
  }