@boon4681/giri 0.0.2 → 0.0.3-alpha-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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_typescript2.default.findConfigFile(paths.cwd, import_typescript2.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_typescript2.default.getParsedCommandLineOfConfigFile(configPath, {}, {
53
+ ...import_typescript2.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_typescript2.default.createProgram(routeFiles, options);
62
62
  }
63
- var import_typescript, DEFAULT_OPTIONS;
63
+ var import_typescript2, 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_typescript2 = __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_typescript2.default.ScriptTarget.ES2022,
70
+ module: import_typescript2.default.ModuleKind.NodeNext,
71
+ moduleResolution: import_typescript2.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_typescript3.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_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void | import_typescript3.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_typescript3.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript3.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_typescript3.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript3.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript3.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_typescript3.default.TypeFlags.Any | import_typescript3.default.TypeFlags.Unknown)) {
193
193
  return {};
194
194
  }
195
- if (flags & import_typescript2.default.TypeFlags.Null) {
195
+ if (flags & import_typescript3.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_typescript3.default.TypeFlags.Undefined | import_typescript3.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_typescript3.default.TypeFlags.BigInt | import_typescript3.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_typescript3.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_typescript3.default.TypeFlags.String) {
215
215
  return { type: "string" };
216
216
  }
217
- if (flags & import_typescript2.default.TypeFlags.Number) {
217
+ if (flags & import_typescript3.default.TypeFlags.Number) {
218
218
  return { type: "number" };
219
219
  }
220
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
220
+ if (flags & import_typescript3.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_typescript3.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_typescript3;
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_typescript3 = __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_typescript4.default.canHaveModifiers(node) && (import_typescript4.default.getModifiers(node)?.some((m) => m.kind === import_typescript4.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_typescript4.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_typescript4.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_typescript4.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript4.default.isArrowFunction(declaration.initializer) || import_typescript4.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_typescript4.default.isArrowFunction(fn) && !import_typescript4.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_typescript4.default.isFunctionDeclaration(node) || import_typescript4.default.isFunctionExpression(node) || import_typescript4.default.isArrowFunction(node)) {
267
267
  return;
268
268
  }
269
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
269
+ if (import_typescript4.default.isReturnStatement(node) && node.expression) {
270
270
  expressions.push(node.expression);
271
271
  }
272
- import_typescript3.default.forEachChild(node, visit);
272
+ import_typescript4.default.forEachChild(node, visit);
273
273
  };
274
- import_typescript3.default.forEachChild(fn.body, visit);
274
+ import_typescript4.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_typescript4.default.isIdentifier(first.name) ? first.name.text : void 0;
289
+ }
290
+ function readFromCall(checker, expression, contextName) {
291
+ if (!import_typescript4.default.isCallExpression(expression) || !import_typescript4.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_typescript4.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_typescript4;
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_typescript4 = __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
@@ -748,13 +834,18 @@ function methodFromFile(fileName) {
748
834
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
749
835
  return METHOD_FROM_FILE.get(stem);
750
836
  }
751
- function sharedFileIn(dir) {
837
+ function sharedFileIn(dir, cache) {
838
+ if (cache?.has(dir)) {
839
+ return cache.get(dir);
840
+ }
752
841
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
753
842
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
754
843
  if ((0, import_node_fs2.existsSync)(file)) {
844
+ cache?.set(dir, file);
755
845
  return file;
756
846
  }
757
847
  }
848
+ cache?.set(dir, void 0);
758
849
  return void 0;
759
850
  }
760
851
  function physicalRouteSegments(routesDir, routeDir) {
@@ -823,7 +914,7 @@ async function scanRouteFolders(routesDir) {
823
914
  function routeParamsForDir(routesDir, dir) {
824
915
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
825
916
  }
826
- function sharedFilesForDir(routesDir, dir) {
917
+ function sharedFilesForDir(routesDir, dir, cache) {
827
918
  const segments = physicalRouteSegments(routesDir, dir);
828
919
  const dirs = [routesDir];
829
920
  let current = routesDir;
@@ -831,7 +922,7 @@ function sharedFilesForDir(routesDir, dir) {
831
922
  current = (0, import_node_path2.join)(current, segment);
832
923
  dirs.push(current);
833
924
  }
834
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
925
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
835
926
  }
836
927
  async function scanRoutes(routesDir) {
837
928
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -843,6 +934,7 @@ async function scanRoutes(routesDir) {
843
934
  onlyFiles: true
844
935
  });
845
936
  const routes = [];
937
+ const sharedCache = /* @__PURE__ */ new Map();
846
938
  for (const file of files) {
847
939
  const method = methodFromFile((0, import_node_path2.basename)(file));
848
940
  if (!method) {
@@ -858,7 +950,7 @@ async function scanRoutes(routesDir) {
858
950
  routeDir,
859
951
  routeSegments,
860
952
  params,
861
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
953
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
862
954
  });
863
955
  }
864
956
  return routes.sort((left, right) => {
@@ -871,9 +963,11 @@ async function scanRoutes(routesDir) {
871
963
  }
872
964
 
873
965
  // src/app.ts
874
- function loadModule(file) {
966
+ function loadModule(file, force = true) {
875
967
  const resolved = require.resolve(file);
876
- delete require.cache[resolved];
968
+ if (force) {
969
+ delete require.cache[resolved];
970
+ }
877
971
  return require(resolved);
878
972
  }
879
973
  function interopDefault(value) {
@@ -935,20 +1029,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
935
1029
  }
936
1030
  function matchAlias(request, key) {
937
1031
  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);
1032
+ const [prefix, suffix = ""] = key.split("*");
1033
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
1034
+ return request.slice(prefix.length, request.length - suffix.length);
941
1035
  }
942
1036
  return void 0;
943
1037
  }
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;
1038
+ return request === key ? "" : void 0;
952
1039
  }
953
1040
  function resolveAliasRequest(request, alias, cwd) {
954
1041
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -1012,38 +1099,78 @@ async function buildGiriApp(config, options = {}) {
1012
1099
  const routes = await scanRoutes(paths.routesDir);
1013
1100
  const app = config.adapter.createApp();
1014
1101
  ensureGiriAliasResolver(paths.outDir);
1015
- const { unregister } = await safeRegister();
1016
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
1102
+ if (options.lazy && (!options.loaderRegistered || !options.aliasResolverRegistered)) {
1103
+ throw new Error("Lazy route loading requires persistent loader and alias registrations.");
1104
+ }
1105
+ const loader = options.loaderRegistered ? void 0 : await safeRegister();
1106
+ const unregisterAliasResolver = options.aliasResolverRegistered ? void 0 : registerAliasResolver(config.alias, paths.cwd);
1017
1107
  try {
1018
- for (const route of routes) {
1019
- const routeModule = loadModule(route.file);
1108
+ const dirty = options.dirty;
1109
+ const forceReload = dirty === void 0;
1110
+ const isDirty = (file) => forceReload || dirty.has(file);
1111
+ const sharedCache = /* @__PURE__ */ new Map();
1112
+ const loadShared = (file) => {
1113
+ if (!sharedCache.has(file)) {
1114
+ sharedCache.set(file, loadModule(file, isDirty(file)));
1115
+ }
1116
+ return sharedCache.get(file);
1117
+ };
1118
+ const runtimeFor = (route) => {
1119
+ const routeModule = loadModule(route.file, isDirty(route.file));
1020
1120
  if (typeof routeModule.handle !== "function") {
1021
1121
  throw new Error(`${route.file} must export a named handle function.`);
1022
1122
  }
1023
1123
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
1024
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
1124
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
1025
1125
  );
1026
1126
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
1027
- config.adapter.register(app, {
1127
+ return {
1028
1128
  method: route.method,
1029
1129
  path: route.path,
1030
1130
  handle: routeModule.handle,
1031
1131
  middleware: [...folderMiddleware, ...verbMiddleware],
1032
1132
  input: routeInput(routeModule, route.file),
1033
- services: options.services
1133
+ services: options.services,
1134
+ cookieSecret: config.cookieSecret
1135
+ };
1136
+ };
1137
+ for (const route of routes) {
1138
+ if (!options.lazy) {
1139
+ config.adapter.register(app, runtimeFor(route));
1140
+ continue;
1141
+ }
1142
+ let runtime;
1143
+ const getRuntime = () => {
1144
+ runtime ??= runtimeFor(route);
1145
+ return runtime;
1146
+ };
1147
+ config.adapter.register(app, {
1148
+ method: route.method,
1149
+ path: route.path,
1150
+ get handle() {
1151
+ return getRuntime().handle;
1152
+ },
1153
+ get middleware() {
1154
+ return getRuntime().middleware;
1155
+ },
1156
+ get input() {
1157
+ return getRuntime().input;
1158
+ },
1159
+ services: options.services,
1160
+ cookieSecret: config.cookieSecret
1034
1161
  });
1035
1162
  }
1036
1163
  } finally {
1037
- unregisterAliasResolver();
1038
- unregister();
1164
+ unregisterAliasResolver?.();
1165
+ loader?.unregister();
1039
1166
  }
1040
1167
  return { app, routes, paths };
1041
1168
  }
1042
1169
 
1043
1170
  // 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");
1171
+ var import_node_fs8 = require("fs");
1172
+ var import_promises4 = require("fs/promises");
1173
+ var import_node_path12 = require("path");
1047
1174
 
1048
1175
  // src/generator/app-types.ts
1049
1176
  var import_node_fs4 = require("fs");
@@ -1295,6 +1422,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1295
1422
  const documentPaths = {};
1296
1423
  const schemas = {};
1297
1424
  const securitySchemes = {};
1425
+ const tagOrder = [];
1298
1426
  for (const route of routes) {
1299
1427
  if (data.hiddenFiles?.has(route.file)) {
1300
1428
  continue;
@@ -1302,10 +1430,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1302
1430
  const responses = data.responsesByFile?.get(route.file);
1303
1431
  const input = data.inputsByFile?.get(route.file);
1304
1432
  const security = data.securityByFile?.get(route.file);
1433
+ const meta = data.openapiByFile?.get(route.file);
1305
1434
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1306
1435
  schemas[name] = rewriteRefs(schema);
1307
1436
  }
1308
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1437
+ const operation = {};
1438
+ if (meta?.tags && meta.tags.length > 0) {
1439
+ operation.tags = meta.tags;
1440
+ for (const tag of meta.tags) {
1441
+ if (!tagOrder.includes(tag)) {
1442
+ tagOrder.push(tag);
1443
+ }
1444
+ }
1445
+ }
1446
+ if (meta?.summary) {
1447
+ operation.summary = meta.summary;
1448
+ }
1449
+ if (meta?.description) {
1450
+ operation.description = meta.description;
1451
+ }
1452
+ if (meta?.operationId) {
1453
+ operation.operationId = meta.operationId;
1454
+ }
1455
+ if (meta?.deprecated) {
1456
+ operation.deprecated = true;
1457
+ }
1458
+ operation.responses = buildResponses(responses?.responses ?? []);
1309
1459
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1310
1460
  if (parameters.length > 0) {
1311
1461
  operation.parameters = parameters;
@@ -1337,6 +1487,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1337
1487
  info: readProjectInfo(paths.cwd),
1338
1488
  paths: documentPaths
1339
1489
  };
1490
+ if (tagOrder.length > 0) {
1491
+ document.tags = tagOrder.map((name) => ({ name }));
1492
+ }
1340
1493
  const components = {};
1341
1494
  if (Object.keys(schemas).length > 0) {
1342
1495
  components.schemas = schemas;
@@ -1368,14 +1521,29 @@ function paramsType(params) {
1368
1521
  ${fields}
1369
1522
  }`;
1370
1523
  }
1371
- function varsType(typesDir, sharedFiles) {
1372
- if (sharedFiles.length === 0) {
1373
- return "{}";
1524
+ function middlewareVarsType(typesDir, sharedFile) {
1525
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1526
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1527
+ }
1528
+ function ownSharedFile(dir, sharedFiles) {
1529
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1530
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1531
+ return sharedFiles[index];
1532
+ }
1374
1533
  }
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 & ");
1534
+ return void 0;
1535
+ }
1536
+ function varsType(paths, file, dir, sharedFiles) {
1537
+ const typesDir = (0, import_node_path8.dirname)(file);
1538
+ const parts = [];
1539
+ if (dir !== paths.routesDir) {
1540
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1541
+ }
1542
+ const ownShared = ownSharedFile(dir, sharedFiles);
1543
+ if (ownShared) {
1544
+ parts.push(middlewareVarsType(typesDir, ownShared));
1545
+ }
1546
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1379
1547
  }
1380
1548
  function methodExports(typesDir, verbs) {
1381
1549
  return verbs.map(({ method, file }) => {
@@ -1386,14 +1554,14 @@ function methodExports(typesDir, verbs) {
1386
1554
  });
1387
1555
  }
1388
1556
  async function writeParamTypes(paths, folders) {
1389
- for (const { dir, params, sharedFiles, verbs } of folders) {
1557
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1390
1558
  const file = typeFilePath(paths, dir);
1391
1559
  const typesDir = (0, import_node_path8.dirname)(file);
1392
1560
  const lines = [
1393
1561
  GENERATED_HEADER,
1394
1562
  `export type Params = ${paramsType(params)};`,
1395
1563
  "export type RouteParams = Params;",
1396
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1564
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1397
1565
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1398
1566
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1399
1567
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1403,10 +1571,14 @@ async function writeParamTypes(paths, folders) {
1403
1571
  lines.push(...methodExports(typesDir, verbs));
1404
1572
  }
1405
1573
  lines.push("");
1406
- await writeGenerated(file, lines.join("\n"));
1407
- }
1574
+ return writeGenerated(file, lines.join("\n"));
1575
+ }));
1408
1576
  }
1409
1577
 
1578
+ // src/generator/route-meta.ts
1579
+ var import_node_fs6 = require("fs");
1580
+ var import_typescript = __toESM(require("typescript"));
1581
+
1410
1582
  // src/generator/inputs.ts
1411
1583
  function sanitize(schema) {
1412
1584
  const { $schema, ...rest } = schema;
@@ -1467,28 +1639,315 @@ function readInput(routeModule) {
1467
1639
  }
1468
1640
  return input.body || input.query ? input : void 0;
1469
1641
  }
1470
- function hiddenFrom(value) {
1471
- if (value === false) {
1642
+ function hasExportModifier(node) {
1643
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
1644
+ }
1645
+ function unwrapExpression(expression) {
1646
+ let current = expression;
1647
+ while (import_typescript.default.isParenthesizedExpression(current) || import_typescript.default.isAsExpression(current) || import_typescript.default.isSatisfiesExpression(current)) {
1648
+ current = current.expression;
1649
+ }
1650
+ return current;
1651
+ }
1652
+ function staticBoolean(expression) {
1653
+ const value = unwrapExpression(expression);
1654
+ if (value.kind === import_typescript.default.SyntaxKind.TrueKeyword) {
1472
1655
  return true;
1473
1656
  }
1474
- if (value === true) {
1657
+ if (value.kind === import_typescript.default.SyntaxKind.FalseKeyword) {
1475
1658
  return false;
1476
1659
  }
1477
- if (value && typeof value === "object" && "hidden" in value) {
1478
- return Boolean(value.hidden);
1660
+ return void 0;
1661
+ }
1662
+ function staticString(expression) {
1663
+ const value = unwrapExpression(expression);
1664
+ return import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
1665
+ }
1666
+ function staticStringArray(expression) {
1667
+ const value = unwrapExpression(expression);
1668
+ if (!import_typescript.default.isArrayLiteralExpression(value)) {
1669
+ return void 0;
1670
+ }
1671
+ const strings = [];
1672
+ for (const element of value.elements) {
1673
+ const string = staticString(element);
1674
+ if (string === void 0) {
1675
+ return void 0;
1676
+ }
1677
+ strings.push(string);
1678
+ }
1679
+ return strings;
1680
+ }
1681
+ function propertyName(name) {
1682
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
1683
+ return name.text;
1479
1684
  }
1480
1685
  return void 0;
1481
1686
  }
1482
- function collectHidden(route, routeModule, loadShared) {
1687
+ function collectImportedNames(source) {
1688
+ const names = /* @__PURE__ */ new Set();
1689
+ for (const statement of source.statements) {
1690
+ if (!import_typescript.default.isImportDeclaration(statement)) {
1691
+ continue;
1692
+ }
1693
+ const clause = statement.importClause;
1694
+ if (!clause) {
1695
+ continue;
1696
+ }
1697
+ if (clause.name) {
1698
+ names.add(clause.name.text);
1699
+ }
1700
+ const bindings = clause.namedBindings;
1701
+ if (bindings && import_typescript.default.isNamespaceImport(bindings)) {
1702
+ names.add(bindings.name.text);
1703
+ }
1704
+ if (bindings && import_typescript.default.isNamedImports(bindings)) {
1705
+ for (const element of bindings.elements) {
1706
+ names.add(element.name.text);
1707
+ }
1708
+ }
1709
+ }
1710
+ return names;
1711
+ }
1712
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1713
+ let found = false;
1714
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1715
+ const visit = (node) => {
1716
+ if (found) {
1717
+ return;
1718
+ }
1719
+ if (import_typescript.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1720
+ found = true;
1721
+ return;
1722
+ }
1723
+ import_typescript.default.forEachChild(node, visit);
1724
+ };
1725
+ visit(expression);
1726
+ return found;
1727
+ }
1728
+ function parseStaticOpenApi(expression) {
1729
+ const value = unwrapExpression(expression);
1730
+ const boolean = staticBoolean(value);
1731
+ if (boolean !== void 0) {
1732
+ return boolean;
1733
+ }
1734
+ if (!import_typescript.default.isObjectLiteralExpression(value)) {
1735
+ return void 0;
1736
+ }
1737
+ const openapi = {};
1738
+ for (const property of value.properties) {
1739
+ if (!import_typescript.default.isPropertyAssignment(property)) {
1740
+ return void 0;
1741
+ }
1742
+ const name = propertyName(property.name);
1743
+ if (!name) {
1744
+ return void 0;
1745
+ }
1746
+ if (name === "hidden") {
1747
+ const hidden = staticBoolean(property.initializer);
1748
+ if (hidden === void 0) {
1749
+ return void 0;
1750
+ }
1751
+ openapi.hidden = hidden;
1752
+ } else if (name === "tags") {
1753
+ const tags = staticStringArray(property.initializer);
1754
+ if (!tags) {
1755
+ return void 0;
1756
+ }
1757
+ openapi.tags = tags;
1758
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1759
+ const string = staticString(property.initializer);
1760
+ if (string === void 0) {
1761
+ return void 0;
1762
+ }
1763
+ openapi[name] = string;
1764
+ } else if (name === "deprecated") {
1765
+ const deprecated = staticBoolean(property.initializer);
1766
+ if (deprecated === void 0) {
1767
+ return void 0;
1768
+ }
1769
+ openapi.deprecated = deprecated;
1770
+ } else {
1771
+ return void 0;
1772
+ }
1773
+ }
1774
+ return openapi;
1775
+ }
1776
+ function readStaticModuleMeta(file) {
1777
+ let source;
1778
+ try {
1779
+ source = import_typescript.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript.default.ScriptTarget.Latest, true);
1780
+ } catch {
1781
+ return void 0;
1782
+ }
1783
+ const importedNames = collectImportedNames(source);
1784
+ const sourceText = source.getFullText();
1785
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1786
+ const meta = { middlewareSecurity: false };
1787
+ for (const statement of source.statements) {
1788
+ if (import_typescript.default.isImportDeclaration(statement) || import_typescript.default.isInterfaceDeclaration(statement) || import_typescript.default.isTypeAliasDeclaration(statement) || import_typescript.default.isEmptyStatement(statement) || !hasExportModifier(statement)) {
1789
+ continue;
1790
+ }
1791
+ if (import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1792
+ continue;
1793
+ }
1794
+ if (import_typescript.default.isVariableStatement(statement)) {
1795
+ for (const declaration of statement.declarationList.declarations) {
1796
+ if (!import_typescript.default.isIdentifier(declaration.name)) {
1797
+ return void 0;
1798
+ }
1799
+ const name = declaration.name.text;
1800
+ if (name === "openapi") {
1801
+ if (!declaration.initializer) {
1802
+ return void 0;
1803
+ }
1804
+ const openapi = parseStaticOpenApi(declaration.initializer);
1805
+ if (openapi === void 0) {
1806
+ return void 0;
1807
+ }
1808
+ meta.openapi = openapi;
1809
+ } else if (name === "handle") {
1810
+ if (!declaration.initializer || !import_typescript.default.isArrowFunction(declaration.initializer) && !import_typescript.default.isFunctionExpression(declaration.initializer)) {
1811
+ return void 0;
1812
+ }
1813
+ continue;
1814
+ } else if (name === "middleware") {
1815
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1816
+ return void 0;
1817
+ }
1818
+ meta.middlewareSecurity = false;
1819
+ continue;
1820
+ } else {
1821
+ return void 0;
1822
+ }
1823
+ }
1824
+ continue;
1825
+ }
1826
+ return void 0;
1827
+ }
1828
+ return meta;
1829
+ }
1830
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1483
1831
  let hidden = false;
1832
+ const tags = [];
1833
+ const meta = {};
1834
+ const apply = (value, isVerb) => {
1835
+ if (value === false) {
1836
+ hidden = true;
1837
+ return;
1838
+ }
1839
+ if (value === true) {
1840
+ hidden = false;
1841
+ return;
1842
+ }
1843
+ if (!value) {
1844
+ return;
1845
+ }
1846
+ if (typeof value.hidden === "boolean") {
1847
+ hidden = value.hidden;
1848
+ }
1849
+ if (value.tags) {
1850
+ tags.push(...value.tags);
1851
+ }
1852
+ if (typeof value.summary === "string") {
1853
+ meta.summary = value.summary;
1854
+ }
1855
+ if (typeof value.description === "string") {
1856
+ meta.description = value.description;
1857
+ }
1858
+ if (typeof value.deprecated === "boolean") {
1859
+ meta.deprecated = value.deprecated;
1860
+ }
1861
+ if (isVerb && typeof value.operationId === "string") {
1862
+ meta.operationId = value.operationId;
1863
+ }
1864
+ };
1484
1865
  for (const file of route.sharedFiles) {
1485
- const opinion = hiddenFrom(loadShared(file).openapi);
1486
- if (opinion !== void 0) {
1487
- hidden = opinion;
1866
+ const shared = loadShared(file);
1867
+ if (!shared) {
1868
+ return void 0;
1869
+ }
1870
+ apply(shared.openapi, false);
1871
+ }
1872
+ apply(routeModule.openapi, true);
1873
+ if (tags.length > 0) {
1874
+ meta.tags = [...new Set(tags)];
1875
+ }
1876
+ return { hidden, meta };
1877
+ }
1878
+ function extractStaticMeta(route, routeModule, loadShared) {
1879
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1880
+ if (!openapi) {
1881
+ return void 0;
1882
+ }
1883
+ const meta = {};
1884
+ if (openapi.hidden) {
1885
+ meta.hidden = true;
1886
+ }
1887
+ if (Object.keys(openapi.meta).length > 0) {
1888
+ meta.openapi = openapi.meta;
1889
+ }
1890
+ return meta;
1891
+ }
1892
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
1893
+ const meta = {};
1894
+ const security = collectSecurity(route, {}, loadShared);
1895
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
1896
+ if (security) {
1897
+ meta.security = security;
1898
+ }
1899
+ if (hidden) {
1900
+ meta.hidden = true;
1901
+ }
1902
+ if (Object.keys(openapi).length > 0) {
1903
+ meta.openapi = openapi;
1904
+ }
1905
+ return meta;
1906
+ }
1907
+ function resolveOpenApi(route, routeModule, loadShared) {
1908
+ let hidden = false;
1909
+ const tags = [];
1910
+ const meta = {};
1911
+ const apply = (value, isVerb) => {
1912
+ if (value === false) {
1913
+ hidden = true;
1914
+ return;
1915
+ }
1916
+ if (value === true) {
1917
+ hidden = false;
1918
+ return;
1919
+ }
1920
+ if (!value || typeof value !== "object") {
1921
+ return;
1922
+ }
1923
+ const o = value;
1924
+ if ("hidden" in o) {
1925
+ hidden = Boolean(o.hidden);
1926
+ }
1927
+ if (Array.isArray(o.tags)) {
1928
+ tags.push(...o.tags.filter((tag) => typeof tag === "string"));
1929
+ }
1930
+ if (typeof o.summary === "string") {
1931
+ meta.summary = o.summary;
1932
+ }
1933
+ if (typeof o.description === "string") {
1934
+ meta.description = o.description;
1935
+ }
1936
+ if (typeof o.deprecated === "boolean") {
1937
+ meta.deprecated = o.deprecated;
1488
1938
  }
1939
+ if (isVerb && typeof o.operationId === "string") {
1940
+ meta.operationId = o.operationId;
1941
+ }
1942
+ };
1943
+ for (const file of route.sharedFiles) {
1944
+ apply(loadShared(file).openapi, false);
1945
+ }
1946
+ apply(routeModule.openapi, true);
1947
+ if (tags.length > 0) {
1948
+ meta.tags = [...new Set(tags)];
1489
1949
  }
1490
- const verb = hiddenFrom(routeModule.openapi);
1491
- return verb ?? hidden;
1950
+ return { hidden, meta };
1492
1951
  }
1493
1952
  function collectSecurity(route, routeModule, loadShared) {
1494
1953
  const skipInherited = Boolean(
@@ -1520,6 +1979,33 @@ function collectSecurity(route, routeModule, loadShared) {
1520
1979
  }
1521
1980
  async function extractRouteMeta(config, paths, routes) {
1522
1981
  const byFile = /* @__PURE__ */ new Map();
1982
+ const remainingRoutes = [];
1983
+ const runtimeSharedRoutes = [];
1984
+ const staticCache = /* @__PURE__ */ new Map();
1985
+ const loadStatic = (file) => {
1986
+ if (!staticCache.has(file)) {
1987
+ staticCache.set(file, readStaticModuleMeta(file));
1988
+ }
1989
+ return staticCache.get(file);
1990
+ };
1991
+ for (const route of routes) {
1992
+ const routeModule = loadStatic(route.file);
1993
+ if (!routeModule) {
1994
+ remainingRoutes.push(route);
1995
+ continue;
1996
+ }
1997
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
1998
+ if (meta) {
1999
+ if (meta.hidden || meta.openapi) {
2000
+ byFile.set(route.file, meta);
2001
+ }
2002
+ continue;
2003
+ }
2004
+ runtimeSharedRoutes.push({ route, routeModule });
2005
+ }
2006
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
2007
+ return byFile;
2008
+ }
1523
2009
  const { unregister } = await safeRegister();
1524
2010
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1525
2011
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1534,13 +2020,19 @@ async function extractRouteMeta(config, paths, routes) {
1534
2020
  return sharedCache.get(file);
1535
2021
  };
1536
2022
  try {
1537
- for (const route of routes) {
2023
+ for (const { route, routeModule } of runtimeSharedRoutes) {
2024
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
2025
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
2026
+ byFile.set(route.file, meta);
2027
+ }
2028
+ }
2029
+ for (const route of remainingRoutes) {
1538
2030
  try {
1539
2031
  const routeModule = loadModule2(route.file);
1540
2032
  const meta = {};
1541
2033
  const input = readInput(routeModule);
1542
2034
  const security = collectSecurity(route, routeModule, loadShared);
1543
- const hidden = collectHidden(route, routeModule, loadShared);
2035
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1544
2036
  if (input) {
1545
2037
  meta.input = input;
1546
2038
  }
@@ -1550,7 +2042,10 @@ async function extractRouteMeta(config, paths, routes) {
1550
2042
  if (hidden) {
1551
2043
  meta.hidden = true;
1552
2044
  }
1553
- if (meta.input || meta.security || meta.hidden) {
2045
+ if (Object.keys(openapi).length > 0) {
2046
+ meta.openapi = openapi;
2047
+ }
2048
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1554
2049
  byFile.set(route.file, meta);
1555
2050
  }
1556
2051
  } catch {
@@ -1622,6 +2117,103 @@ async function writeTsConfig(paths, config) {
1622
2117
  });
1623
2118
  }
1624
2119
 
2120
+ // src/generator/cache.ts
2121
+ var import_node_crypto = require("crypto");
2122
+ var import_node_fs7 = require("fs");
2123
+ var import_promises3 = require("fs/promises");
2124
+ var import_node_path11 = require("path");
2125
+ var import_tinyglobby2 = require("tinyglobby");
2126
+ var CACHE_VERSION = 1;
2127
+ var SYNC_CACHE_NAME = ".sync-cache.json";
2128
+ function stableConfig(config) {
2129
+ const alias = Object.entries(config.alias ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]);
2130
+ return { alias, outDir: config.outDir ?? ".giri" };
2131
+ }
2132
+ async function syncFingerprint(config, paths) {
2133
+ const outRelative = slash((0, import_node_path11.relative)(paths.cwd, paths.outDir));
2134
+ const ignore = ["**/node_modules/**", "**/.git/**"];
2135
+ if (outRelative && !outRelative.startsWith("..")) {
2136
+ ignore.push(`${outRelative}/**`);
2137
+ }
2138
+ const files = await (0, import_tinyglobby2.glob)([
2139
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}",
2140
+ "giri.config.{ts,js,mts,cts,mjs,cjs}",
2141
+ "tsconfig*.json",
2142
+ "package.json",
2143
+ "package-lock.json",
2144
+ "npm-shrinkwrap.json",
2145
+ "yarn.lock",
2146
+ "pnpm-lock.yaml",
2147
+ "bun.lock",
2148
+ "bun.lockb"
2149
+ ], {
2150
+ cwd: paths.cwd,
2151
+ absolute: false,
2152
+ onlyFiles: true,
2153
+ dot: true,
2154
+ ignore
2155
+ });
2156
+ const hash = (0, import_node_crypto.createHash)("sha256");
2157
+ hash.update(JSON.stringify(stableConfig(config)));
2158
+ for (const file of files.sort()) {
2159
+ hash.update("\0");
2160
+ hash.update(slash(file));
2161
+ hash.update("\0");
2162
+ hash.update(await (0, import_promises3.readFile)((0, import_node_path11.resolve)(paths.cwd, file)));
2163
+ }
2164
+ return hash.digest("hex");
2165
+ }
2166
+ function cachePath(paths) {
2167
+ return (0, import_node_path11.join)(paths.outDir, SYNC_CACHE_NAME);
2168
+ }
2169
+ function serializePath(paths, file) {
2170
+ return slash((0, import_node_path11.relative)(paths.cwd, file));
2171
+ }
2172
+ function deserializePath(paths, file) {
2173
+ return slash((0, import_node_path11.resolve)(paths.cwd, file.split("/").join(import_node_path11.sep)));
2174
+ }
2175
+ function serializeMap(paths, values) {
2176
+ return [...values].map(([file, value]) => [serializePath(paths, file), value]);
2177
+ }
2178
+ function deserializeMap(paths, values) {
2179
+ return new Map(values.map(([file, value]) => [deserializePath(paths, file), value]));
2180
+ }
2181
+ async function readSyncCache(paths, fingerprint) {
2182
+ const file = cachePath(paths);
2183
+ if (!(0, import_node_fs7.existsSync)(file)) {
2184
+ return void 0;
2185
+ }
2186
+ try {
2187
+ const cache = JSON.parse(await (0, import_promises3.readFile)(file, "utf8"));
2188
+ if (cache.version !== CACHE_VERSION || cache.fingerprint !== fingerprint) {
2189
+ return void 0;
2190
+ }
2191
+ return {
2192
+ responsesByFile: deserializeMap(paths, cache.data.responsesByFile),
2193
+ inputsByFile: deserializeMap(paths, cache.data.inputsByFile),
2194
+ securityByFile: deserializeMap(paths, cache.data.securityByFile),
2195
+ hiddenFiles: new Set(cache.data.hiddenFiles.map((entry) => deserializePath(paths, entry))),
2196
+ openapiByFile: deserializeMap(paths, cache.data.openapiByFile)
2197
+ };
2198
+ } catch {
2199
+ return void 0;
2200
+ }
2201
+ }
2202
+ async function writeSyncCache(paths, fingerprint, data) {
2203
+ const cache = {
2204
+ version: CACHE_VERSION,
2205
+ fingerprint,
2206
+ data: {
2207
+ responsesByFile: serializeMap(paths, data.responsesByFile),
2208
+ inputsByFile: serializeMap(paths, data.inputsByFile),
2209
+ securityByFile: serializeMap(paths, data.securityByFile),
2210
+ hiddenFiles: [...data.hiddenFiles].map((file) => serializePath(paths, file)),
2211
+ openapiByFile: serializeMap(paths, data.openapiByFile)
2212
+ }
2213
+ };
2214
+ await writeJson(cachePath(paths), cache);
2215
+ }
2216
+
1625
2217
  // src/generator/sync.ts
1626
2218
  async function typeFolders(paths, routes) {
1627
2219
  const verbsByDir = /* @__PURE__ */ new Map();
@@ -1632,10 +2224,11 @@ async function typeFolders(paths, routes) {
1632
2224
  verbsByDir.set(key, list);
1633
2225
  }
1634
2226
  const dirs = await scanRouteFolders(paths.routesDir);
2227
+ const sharedCache = /* @__PURE__ */ new Map();
1635
2228
  return dirs.map((dir) => ({
1636
2229
  dir,
1637
2230
  params: routeParamsForDir(paths.routesDir, dir),
1638
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
2231
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1639
2232
  verbs: verbsByDir.get(slash(dir)) ?? []
1640
2233
  }));
1641
2234
  }
@@ -1647,11 +2240,9 @@ async function extractResponses(paths, routes) {
1647
2240
  try {
1648
2241
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1649
2242
  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
- );
2243
+ const appTypes = (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts");
2244
+ const roots = (0, import_node_fs8.existsSync)(appTypes) ? [...files, appTypes] : files;
2245
+ const program = createSchemaProgram2(paths, roots);
1655
2246
  for (const file of files) {
1656
2247
  byFile.set(file, extractRouteResponses2(program, file));
1657
2248
  }
@@ -1664,8 +2255,9 @@ async function extractMeta(config, paths, routes) {
1664
2255
  const inputsByFile = /* @__PURE__ */ new Map();
1665
2256
  const securityByFile = /* @__PURE__ */ new Map();
1666
2257
  const hiddenFiles = /* @__PURE__ */ new Set();
2258
+ const openapiByFile = /* @__PURE__ */ new Map();
1667
2259
  if (routes.length === 0) {
1668
- return { inputsByFile, securityByFile, hiddenFiles };
2260
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1669
2261
  }
1670
2262
  try {
1671
2263
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1679,63 +2271,91 @@ async function extractMeta(config, paths, routes) {
1679
2271
  if (entry.hidden) {
1680
2272
  hiddenFiles.add(file);
1681
2273
  }
2274
+ if (entry.openapi) {
2275
+ openapiByFile.set(file, entry.openapi);
2276
+ }
1682
2277
  }
1683
2278
  } catch (error) {
1684
2279
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1685
2280
  }
1686
- return { inputsByFile, securityByFile, hiddenFiles };
2281
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1687
2282
  }
1688
2283
  async function syncProject(config, options = {}) {
1689
2284
  const paths = resolveGiriPaths(config, options.cwd);
1690
2285
  assertSafeOutDir(paths);
2286
+ const hadOutDir = (0, import_node_fs8.existsSync)(paths.outDir);
1691
2287
  const routes = await scanRoutes(paths.routesDir);
1692
2288
  const folders = await typeFolders(paths, routes);
1693
- await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
2289
+ const fingerprint = await syncFingerprint(config, paths);
2290
+ const cached = await readSyncCache(paths, fingerprint);
2291
+ const generatedFiles = [
2292
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2293
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2294
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2295
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2296
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2297
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2298
+ ];
2299
+ if (cached && generatedFiles.every(import_node_fs8.existsSync)) {
2300
+ return { paths, routes, folders, data: cached };
2301
+ }
2302
+ await (0, import_promises4.mkdir)(paths.outDir, { recursive: true });
1694
2303
  await writeParamTypes(paths, folders);
1695
2304
  await writeRouteTypes(paths, routes);
1696
2305
  await writeAppTypes(paths);
1697
2306
  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 };
2307
+ const data = cached ?? {
2308
+ responsesByFile: await extractResponses(paths, routes),
2309
+ ...await extractMeta(config, paths, routes)
2310
+ };
1701
2311
  await writeManifest(paths, routes, data);
1702
2312
  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
- );
2313
+ await writeSyncCache(paths, fingerprint, data);
2314
+ if (hadOutDir) {
2315
+ await pruneDir(
2316
+ paths.outDir,
2317
+ /* @__PURE__ */ new Set([
2318
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2319
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2320
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2321
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2322
+ (0, import_node_path12.join)(paths.outDir, SYNC_CACHE_NAME),
2323
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2324
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2325
+ ])
2326
+ );
2327
+ }
1714
2328
  return { paths, routes, folders, data };
1715
2329
  }
1716
2330
 
1717
2331
  // src/generator/watch.ts
1718
- var import_node_fs7 = require("fs");
2332
+ var import_node_fs10 = require("fs");
2333
+ var import_node_path15 = require("path");
2334
+
2335
+ // src/loader/import-graph.ts
2336
+ var import_node_fs9 = require("fs");
1719
2337
  var import_node_path13 = require("path");
2338
+ var import_typescript5 = __toESM(require("typescript"));
2339
+ var import_tinyglobby3 = require("tinyglobby");
1720
2340
 
1721
2341
  // src/loader/module-loader.ts
1722
- var import_node_path12 = require("path");
2342
+ var import_node_path14 = require("path");
1723
2343
 
1724
2344
  // src/lifecycle.ts
1725
- var import_node_fs8 = require("fs");
1726
- var import_node_path14 = require("path");
2345
+ var import_node_fs11 = require("fs");
2346
+ var import_node_path16 = require("path");
1727
2347
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1728
2348
  function resolveMainFile(cwd) {
1729
2349
  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)) {
2350
+ const file = (0, import_node_path16.join)(cwd, "src", `main.${ext}`);
2351
+ if ((0, import_node_fs11.existsSync)(file)) {
1732
2352
  return file;
1733
2353
  }
1734
2354
  }
1735
2355
  return void 0;
1736
2356
  }
1737
2357
  async function loadLifecycle(cwd = process.cwd()) {
1738
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2358
+ const file = resolveMainFile((0, import_node_path16.resolve)(cwd));
1739
2359
  if (!file) {
1740
2360
  return {};
1741
2361
  }