@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/cli.js CHANGED
@@ -48,10 +48,10 @@ var init_es5 = __esm({
48
48
  // src/generator/schema/program.ts
49
49
  function createSchemaProgram(paths, routeFiles) {
50
50
  let options = { ...DEFAULT_OPTIONS };
51
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
51
+ const configPath = import_typescript3.default.findConfigFile(paths.cwd, import_typescript3.default.sys.fileExists, "tsconfig.json");
52
52
  if (configPath) {
53
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
54
- ...import_typescript.default.sys,
53
+ const parsed = import_typescript3.default.getParsedCommandLineOfConfigFile(configPath, {}, {
54
+ ...import_typescript3.default.sys,
55
55
  onUnRecoverableConfigFileDiagnostic: () => {
56
56
  }
57
57
  });
@@ -59,17 +59,17 @@ function createSchemaProgram(paths, routeFiles) {
59
59
  options = { ...parsed.options, noEmit: true };
60
60
  }
61
61
  }
62
- return import_typescript.default.createProgram(routeFiles, options);
62
+ return import_typescript3.default.createProgram(routeFiles, options);
63
63
  }
64
- var import_typescript, DEFAULT_OPTIONS;
64
+ var import_typescript3, DEFAULT_OPTIONS;
65
65
  var init_program = __esm({
66
66
  "src/generator/schema/program.ts"() {
67
67
  "use strict";
68
- import_typescript = __toESM(require("typescript"));
68
+ import_typescript3 = __toESM(require("typescript"));
69
69
  DEFAULT_OPTIONS = {
70
- target: import_typescript.default.ScriptTarget.ES2022,
71
- module: import_typescript.default.ModuleKind.NodeNext,
72
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
70
+ target: import_typescript3.default.ScriptTarget.ES2022,
71
+ module: import_typescript3.default.ModuleKind.NodeNext,
72
+ moduleResolution: import_typescript3.default.ModuleResolutionKind.NodeNext,
73
73
  strict: true,
74
74
  skipLibCheck: true,
75
75
  noEmit: true
@@ -103,7 +103,7 @@ function literalValuesOf(types) {
103
103
  for (const member of types) {
104
104
  if (member.isStringLiteral() || member.isNumberLiteral()) {
105
105
  values.push(member.value);
106
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
106
+ } else if (member.flags & import_typescript4.default.TypeFlags.BooleanLiteral) {
107
107
  values.push(intrinsicName(member) === "true");
108
108
  } else {
109
109
  return void 0;
@@ -112,7 +112,7 @@ function literalValuesOf(types) {
112
112
  return values;
113
113
  }
114
114
  function walkUnion(type, ctx) {
115
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
115
+ const flag = import_typescript4.default.TypeFlags.Undefined | import_typescript4.default.TypeFlags.Void | import_typescript4.default.TypeFlags.Never;
116
116
  const members = type.types.filter((member) => !(member.flags & flag));
117
117
  if (members.length === 1) {
118
118
  return walkType(members[0], ctx);
@@ -125,13 +125,13 @@ function walkUnion(type, ctx) {
125
125
  }
126
126
  function buildObjectSchema(type, ctx) {
127
127
  const { checker } = ctx;
128
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
128
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript4.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript4.default.IndexKind.Number);
129
129
  const properties = {};
130
130
  const required = [];
131
131
  for (const symbol of checker.getPropertiesOfType(type)) {
132
132
  const name = symbol.getName();
133
133
  const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
134
- const optional = Boolean(symbol.getFlags() & import_typescript2.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript2.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript2.default.TypeFlags.Undefined));
134
+ 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));
135
135
  properties[name] = walkType(propType, ctx);
136
136
  if (!optional) {
137
137
  required.push(name);
@@ -190,16 +190,16 @@ function walkObject(type, ctx) {
190
190
  }
191
191
  function walkType(type, ctx) {
192
192
  const flags = type.flags;
193
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
193
+ if (flags & (import_typescript4.default.TypeFlags.Any | import_typescript4.default.TypeFlags.Unknown)) {
194
194
  return {};
195
195
  }
196
- if (flags & import_typescript2.default.TypeFlags.Null) {
196
+ if (flags & import_typescript4.default.TypeFlags.Null) {
197
197
  return { type: "null" };
198
198
  }
199
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
199
+ if (flags & (import_typescript4.default.TypeFlags.Undefined | import_typescript4.default.TypeFlags.Void)) {
200
200
  return {};
201
201
  }
202
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
202
+ if (flags & (import_typescript4.default.TypeFlags.BigInt | import_typescript4.default.TypeFlags.BigIntLiteral)) {
203
203
  ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
204
204
  return { type: "string" };
205
205
  }
@@ -209,45 +209,45 @@ function walkType(type, ctx) {
209
209
  if (type.isNumberLiteral()) {
210
210
  return { type: "number", const: type.value };
211
211
  }
212
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
212
+ if (flags & import_typescript4.default.TypeFlags.BooleanLiteral) {
213
213
  return { type: "boolean", const: intrinsicName(type) === "true" };
214
214
  }
215
- if (flags & import_typescript2.default.TypeFlags.String) {
215
+ if (flags & import_typescript4.default.TypeFlags.String) {
216
216
  return { type: "string" };
217
217
  }
218
- if (flags & import_typescript2.default.TypeFlags.Number) {
218
+ if (flags & import_typescript4.default.TypeFlags.Number) {
219
219
  return { type: "number" };
220
220
  }
221
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
221
+ if (flags & import_typescript4.default.TypeFlags.Boolean) {
222
222
  return { type: "boolean" };
223
223
  }
224
224
  if (type.isUnion()) {
225
225
  return walkUnion(type, ctx);
226
226
  }
227
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
227
+ if (flags & import_typescript4.default.TypeFlags.Object || type.isIntersection()) {
228
228
  return walkObject(type, ctx);
229
229
  }
230
230
  return {};
231
231
  }
232
- var import_typescript2;
232
+ var import_typescript4;
233
233
  var init_json_schema = __esm({
234
234
  "src/generator/schema/json-schema.ts"() {
235
235
  "use strict";
236
- import_typescript2 = __toESM(require("typescript"));
236
+ import_typescript4 = __toESM(require("typescript"));
237
237
  }
238
238
  });
239
239
 
240
240
  // src/generator/schema/responses.ts
241
241
  function findHandleFunction(source) {
242
242
  let found;
243
- const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
243
+ const isExported = (node) => import_typescript5.default.canHaveModifiers(node) && (import_typescript5.default.getModifiers(node)?.some((m) => m.kind === import_typescript5.default.SyntaxKind.ExportKeyword) ?? false);
244
244
  for (const statement of source.statements) {
245
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
245
+ if (import_typescript5.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
246
246
  found = statement;
247
247
  }
248
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
248
+ if (import_typescript5.default.isVariableStatement(statement) && isExported(statement)) {
249
249
  for (const declaration of statement.declarationList.declarations) {
250
- if (import_typescript3.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript3.default.isArrowFunction(declaration.initializer) || import_typescript3.default.isFunctionExpression(declaration.initializer))) {
250
+ 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))) {
251
251
  found = declaration.initializer;
252
252
  }
253
253
  }
@@ -256,7 +256,7 @@ function findHandleFunction(source) {
256
256
  return found;
257
257
  }
258
258
  function collectReturnExpressions(fn) {
259
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
259
+ if (import_typescript5.default.isArrowFunction(fn) && !import_typescript5.default.isBlock(fn.body)) {
260
260
  return [fn.body];
261
261
  }
262
262
  if (!fn.body) {
@@ -264,15 +264,15 @@ function collectReturnExpressions(fn) {
264
264
  }
265
265
  const expressions = [];
266
266
  const visit = (node) => {
267
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
267
+ if (import_typescript5.default.isFunctionDeclaration(node) || import_typescript5.default.isFunctionExpression(node) || import_typescript5.default.isArrowFunction(node)) {
268
268
  return;
269
269
  }
270
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
270
+ if (import_typescript5.default.isReturnStatement(node) && node.expression) {
271
271
  expressions.push(node.expression);
272
272
  }
273
- import_typescript3.default.forEachChild(node, visit);
273
+ import_typescript5.default.forEachChild(node, visit);
274
274
  };
275
- import_typescript3.default.forEachChild(fn.body, visit);
275
+ import_typescript5.default.forEachChild(fn.body, visit);
276
276
  return expressions;
277
277
  }
278
278
  function propertyType(checker, type, name, location) {
@@ -284,15 +284,21 @@ function isTypedResponse(checker, type) {
284
284
  checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
285
285
  );
286
286
  }
287
- function readFromCall(checker, expression) {
288
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
287
+ function firstParameterName(fn) {
288
+ const [first] = fn.parameters;
289
+ return first && import_typescript5.default.isIdentifier(first.name) ? first.name.text : void 0;
290
+ }
291
+ function readFromCall(checker, expression, contextName) {
292
+ if (!import_typescript5.default.isCallExpression(expression) || !import_typescript5.default.isPropertyAccessExpression(expression.expression)) {
289
293
  return void 0;
290
294
  }
291
295
  const method = expression.expression.name.text;
292
296
  if (method !== "json" && method !== "text") {
293
297
  return void 0;
294
298
  }
295
- if (!isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
299
+ const target = expression.expression.expression;
300
+ const directContextCall = contextName && import_typescript5.default.isIdentifier(target) && target.text === contextName;
301
+ if (!directContextCall && !isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
296
302
  return void 0;
297
303
  }
298
304
  const [dataArg, statusArg] = expression.arguments;
@@ -332,6 +338,7 @@ function extractRouteResponses(program, file) {
332
338
  return result;
333
339
  }
334
340
  const ctx = createWalkContext(checker, fn);
341
+ const contextName = firstParameterName(fn);
335
342
  const byStatus = /* @__PURE__ */ new Map();
336
343
  const record = (hit) => {
337
344
  const schema = walkType(hit.data, ctx);
@@ -340,7 +347,7 @@ function extractRouteResponses(program, file) {
340
347
  byStatus.set(hit.status, bucket);
341
348
  };
342
349
  for (const expression of collectReturnExpressions(fn)) {
343
- const fromCall = readFromCall(checker, expression);
350
+ const fromCall = readFromCall(checker, expression, contextName);
344
351
  if (fromCall) {
345
352
  record(fromCall);
346
353
  continue;
@@ -366,11 +373,11 @@ function extractRouteResponses(program, file) {
366
373
  result.$defs = ctx.defs;
367
374
  return result;
368
375
  }
369
- var import_typescript3;
376
+ var import_typescript5;
370
377
  var init_responses = __esm({
371
378
  "src/generator/schema/responses.ts"() {
372
379
  "use strict";
373
- import_typescript3 = __toESM(require("typescript"));
380
+ import_typescript5 = __toESM(require("typescript"));
374
381
  init_json_schema();
375
382
  }
376
383
  });
@@ -394,9 +401,9 @@ var init_schema = __esm({
394
401
 
395
402
  // src/cli.ts
396
403
  var import_node_child_process = require("child_process");
397
- var import_node_fs9 = require("fs");
398
- var import_promises4 = require("fs/promises");
399
- var import_node_path15 = require("path");
404
+ var import_node_fs12 = require("fs");
405
+ var import_promises5 = require("fs/promises");
406
+ var import_node_path17 = require("path");
400
407
  var prompts = __toESM(require("@clack/prompts"));
401
408
 
402
409
  // src/app.ts
@@ -422,7 +429,8 @@ var configSchema = import_typebox.Type.Object({
422
429
  port: import_typebox.Type.Optional(import_typebox.Type.Number()),
423
430
  hostname: import_typebox.Type.Optional(import_typebox.Type.String())
424
431
  }, { additionalProperties: false })),
425
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
432
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any()),
433
+ cookieSecret: import_typebox.Type.Optional(import_typebox.Type.String())
426
434
  }, { additionalProperties: false });
427
435
 
428
436
  // src/loader/loader.ts
@@ -461,29 +469,44 @@ var safeRegister = async () => {
461
469
  await assertES5(res.unregister);
462
470
  return res;
463
471
  };
464
- var load = async () => {
465
- const defaultTsConfigExists = (0, import_node_fs.existsSync)((0, import_node_path.resolve)("giri.config.ts"));
466
- const defaultJsConfigExists = (0, import_node_fs.existsSync)((0, import_node_path.resolve)("giri.config.js"));
467
- const defaultConfigPath = defaultTsConfigExists ? "giri.config.ts" : defaultJsConfigExists ? "giri.config.js" : void 0;
468
- if (!defaultConfigPath) {
469
- import_prompts.log.error("Config file not found.");
470
- (0, import_node_process.exit)(1);
472
+ var findConfigPath = (cwd = (0, import_node_path.resolve)()) => {
473
+ for (const name of ["giri.config.ts", "giri.config.js"]) {
474
+ const path = (0, import_node_path.resolve)(cwd, name);
475
+ if ((0, import_node_fs.existsSync)(path)) {
476
+ return path;
477
+ }
471
478
  }
472
- const path = (0, import_node_path.resolve)(defaultConfigPath);
473
- if (!(0, import_node_fs.existsSync)(path)) {
474
- import_prompts.log.error(`${path} file does not exist`);
479
+ return void 0;
480
+ };
481
+ var load = async (opts = {}) => {
482
+ const fail = (message) => {
483
+ if (opts.throwOnError) {
484
+ throw new Error(message);
485
+ }
486
+ import_prompts.log.error(message);
475
487
  (0, import_node_process.exit)(1);
488
+ };
489
+ const path = findConfigPath();
490
+ if (!path) {
491
+ fail("Config file not found.");
476
492
  }
477
493
  const { unregister } = await safeRegister();
478
- const required = require(`${path}`);
479
- const content = required.default ?? required;
494
+ let content;
495
+ try {
496
+ const required = require(`${path}`);
497
+ content = required.default ?? required;
498
+ } finally {
499
+ }
480
500
  unregister();
481
501
  const res = import_value.Value.Check(configSchema, content);
482
502
  if (!res) {
483
- for (const error of [...import_value.Value.Errors(configSchema, content)]) {
484
- import_prompts.log.error(error.message);
503
+ const messages = [...import_value.Value.Errors(configSchema, content)].map((error) => error.message);
504
+ if (!opts.throwOnError) {
505
+ for (const message of messages) {
506
+ import_prompts.log.error(message);
507
+ }
485
508
  }
486
- (0, import_node_process.exit)(1);
509
+ fail(messages.join("\n"));
487
510
  }
488
511
  return content;
489
512
  };
@@ -493,6 +516,7 @@ var import_node_fs2 = require("fs");
493
516
  var import_promises = require("fs/promises");
494
517
  var import_node_path2 = require("path");
495
518
  var import_tinyglobby = require("tinyglobby");
519
+ var import_typescript = __toESM(require("typescript"));
496
520
  var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
497
521
  var METHOD_FROM_FILE = new Map(
498
522
  METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
@@ -510,13 +534,129 @@ function methodFromFile(fileName) {
510
534
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
511
535
  return METHOD_FROM_FILE.get(stem);
512
536
  }
513
- function sharedFileIn(dir) {
537
+ function hasExportModifier(node) {
538
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
539
+ }
540
+ function hasDeclareModifier(node) {
541
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.DeclareKeyword) ?? false);
542
+ }
543
+ function propertyName(node) {
544
+ if (import_typescript.default.isIdentifier(node) || import_typescript.default.isStringLiteralLike(node) || import_typescript.default.isNumericLiteral(node)) {
545
+ return node.text;
546
+ }
547
+ if (import_typescript.default.isPropertyAccessExpression(node)) {
548
+ return node.name.text;
549
+ }
550
+ if (import_typescript.default.isElementAccessExpression(node) && node.argumentExpression) {
551
+ const argument = node.argumentExpression;
552
+ if (import_typescript.default.isStringLiteralLike(argument)) {
553
+ return argument.text;
554
+ }
555
+ }
556
+ return void 0;
557
+ }
558
+ function isExportsObject(node) {
559
+ return import_typescript.default.isIdentifier(node) && node.text === "exports";
560
+ }
561
+ function isModuleExports(node) {
562
+ return import_typescript.default.isPropertyAccessExpression(node) && import_typescript.default.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
563
+ }
564
+ function isCommonJsHandleTarget(node) {
565
+ if (!import_typescript.default.isPropertyAccessExpression(node) && !import_typescript.default.isElementAccessExpression(node)) {
566
+ return false;
567
+ }
568
+ return propertyName(node) === "handle" && (isExportsObject(node.expression) || isModuleExports(node.expression));
569
+ }
570
+ function objectExportsHandle(node) {
571
+ if (!import_typescript.default.isObjectLiteralExpression(node)) {
572
+ return false;
573
+ }
574
+ return node.properties.some((property) => {
575
+ if (import_typescript.default.isShorthandPropertyAssignment(property)) {
576
+ return property.name.text === "handle";
577
+ }
578
+ return (import_typescript.default.isPropertyAssignment(property) || import_typescript.default.isMethodDeclaration(property)) && propertyName(property.name) === "handle";
579
+ });
580
+ }
581
+ function hasNamedHandleExport(source) {
582
+ for (const statement of source.statements) {
583
+ if (hasExportModifier(statement) && !hasDeclareModifier(statement) && import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
584
+ return true;
585
+ }
586
+ if (hasExportModifier(statement) && !hasDeclareModifier(statement) && import_typescript.default.isVariableStatement(statement)) {
587
+ if (statement.declarationList.declarations.some(
588
+ (declaration) => import_typescript.default.isIdentifier(declaration.name) && declaration.name.text === "handle"
589
+ )) {
590
+ return true;
591
+ }
592
+ }
593
+ if (import_typescript.default.isExportDeclaration(statement) && !statement.isTypeOnly && statement.exportClause && import_typescript.default.isNamedExports(statement.exportClause)) {
594
+ if (statement.exportClause.elements.some(
595
+ (element) => !element.isTypeOnly && element.name.text === "handle"
596
+ )) {
597
+ return true;
598
+ }
599
+ }
600
+ if (!import_typescript.default.isExpressionStatement(statement) || !import_typescript.default.isBinaryExpression(statement.expression)) {
601
+ continue;
602
+ }
603
+ const assignment = statement.expression;
604
+ if (assignment.operatorToken.kind !== import_typescript.default.SyntaxKind.EqualsToken) {
605
+ continue;
606
+ }
607
+ if (isCommonJsHandleTarget(assignment.left) || isModuleExports(assignment.left) && objectExportsHandle(assignment.right)) {
608
+ return true;
609
+ }
610
+ }
611
+ return false;
612
+ }
613
+ function parseSource(file) {
614
+ return import_typescript.default.createSourceFile(
615
+ file,
616
+ (0, import_node_fs2.readFileSync)(file, "utf8"),
617
+ import_typescript.default.ScriptTarget.Latest,
618
+ true
619
+ );
620
+ }
621
+ function parseDiagnostics(source) {
622
+ return source.parseDiagnostics ?? [];
623
+ }
624
+ function formatSyntaxDiagnostic(diagnostic) {
625
+ const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
626
+ const message = import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
627
+ return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} - error TS${diagnostic.code}: ${message}`;
628
+ }
629
+ function assertSourceSyntax(file) {
630
+ if (!/\.(?:[cm]?[jt]s|[jt]sx)$/i.test(file)) {
631
+ return;
632
+ }
633
+ const diagnostics = parseDiagnostics(parseSource(file));
634
+ if (diagnostics.length > 0) {
635
+ throw new SyntaxError(diagnostics.map(formatSyntaxDiagnostic).join("\n"));
636
+ }
637
+ }
638
+ function assertRouteHandleExport(file) {
639
+ const source = parseSource(file);
640
+ const diagnostics = parseDiagnostics(source);
641
+ if (diagnostics.length > 0) {
642
+ throw new SyntaxError(diagnostics.map(formatSyntaxDiagnostic).join("\n"));
643
+ }
644
+ if (!hasNamedHandleExport(source)) {
645
+ throw new Error(`${file} must export a named handle function.`);
646
+ }
647
+ }
648
+ function sharedFileIn(dir, cache) {
649
+ if (cache?.has(dir)) {
650
+ return cache.get(dir);
651
+ }
514
652
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
515
653
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
516
654
  if ((0, import_node_fs2.existsSync)(file)) {
655
+ cache?.set(dir, file);
517
656
  return file;
518
657
  }
519
658
  }
659
+ cache?.set(dir, void 0);
520
660
  return void 0;
521
661
  }
522
662
  function physicalRouteSegments(routesDir, routeDir) {
@@ -585,7 +725,7 @@ async function scanRouteFolders(routesDir) {
585
725
  function routeParamsForDir(routesDir, dir) {
586
726
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
587
727
  }
588
- function sharedFilesForDir(routesDir, dir) {
728
+ function sharedFilesForDir(routesDir, dir, cache) {
589
729
  const segments = physicalRouteSegments(routesDir, dir);
590
730
  const dirs = [routesDir];
591
731
  let current = routesDir;
@@ -593,7 +733,7 @@ function sharedFilesForDir(routesDir, dir) {
593
733
  current = (0, import_node_path2.join)(current, segment);
594
734
  dirs.push(current);
595
735
  }
596
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
736
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
597
737
  }
598
738
  async function scanRoutes(routesDir) {
599
739
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -605,6 +745,7 @@ async function scanRoutes(routesDir) {
605
745
  onlyFiles: true
606
746
  });
607
747
  const routes = [];
748
+ const sharedCache = /* @__PURE__ */ new Map();
608
749
  for (const file of files) {
609
750
  const method = methodFromFile((0, import_node_path2.basename)(file));
610
751
  if (!method) {
@@ -620,7 +761,7 @@ async function scanRoutes(routesDir) {
620
761
  routeDir,
621
762
  routeSegments,
622
763
  params,
623
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
764
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
624
765
  });
625
766
  }
626
767
  return routes.sort((left, right) => {
@@ -649,9 +790,11 @@ function isGiriBodySchema(value) {
649
790
  }
650
791
 
651
792
  // src/app.ts
652
- function loadModule(file) {
793
+ function loadModule(file, force = true) {
653
794
  const resolved = require.resolve(file);
654
- delete require.cache[resolved];
795
+ if (force) {
796
+ delete require.cache[resolved];
797
+ }
655
798
  return require(resolved);
656
799
  }
657
800
  function interopDefault(value) {
@@ -713,20 +856,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
713
856
  }
714
857
  function matchAlias(request, key) {
715
858
  if (key.includes("*")) {
716
- const [prefix2, suffix = ""] = key.split("*");
717
- if (request.startsWith(prefix2) && request.endsWith(suffix)) {
718
- return request.slice(prefix2.length, request.length - suffix.length);
859
+ const [prefix, suffix = ""] = key.split("*");
860
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
861
+ return request.slice(prefix.length, request.length - suffix.length);
719
862
  }
720
863
  return void 0;
721
864
  }
722
- if (request === key) {
723
- return "";
724
- }
725
- const prefix = `${key}/`;
726
- if (request.startsWith(prefix)) {
727
- return request.slice(prefix.length);
728
- }
729
- return void 0;
865
+ return request === key ? "" : void 0;
730
866
  }
731
867
  function resolveAliasRequest(request, alias, cwd) {
732
868
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -788,40 +924,85 @@ function resolveGiriPaths(config, cwd = process.cwd()) {
788
924
  async function buildGiriApp(config, options = {}) {
789
925
  const paths = resolveGiriPaths(config, options.cwd);
790
926
  const routes = await scanRoutes(paths.routesDir);
927
+ if (options.lazy) {
928
+ for (const route of routes) {
929
+ assertRouteHandleExport(route.file);
930
+ }
931
+ }
791
932
  const app = config.adapter.createApp();
792
933
  ensureGiriAliasResolver(paths.outDir);
793
- const { unregister } = await safeRegister();
794
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
934
+ if (options.lazy && (!options.loaderRegistered || !options.aliasResolverRegistered)) {
935
+ throw new Error("Lazy route loading requires persistent loader and alias registrations.");
936
+ }
937
+ const loader = options.loaderRegistered ? void 0 : await safeRegister();
938
+ const unregisterAliasResolver = options.aliasResolverRegistered ? void 0 : registerAliasResolver(config.alias, paths.cwd);
795
939
  try {
796
- for (const route of routes) {
797
- const routeModule = loadModule(route.file);
940
+ const dirty = options.dirty;
941
+ const forceReload = dirty === void 0;
942
+ const isDirty = (file) => forceReload || dirty.has(file);
943
+ const sharedCache = /* @__PURE__ */ new Map();
944
+ const loadShared = (file) => {
945
+ if (!sharedCache.has(file)) {
946
+ sharedCache.set(file, loadModule(file, isDirty(file)));
947
+ }
948
+ return sharedCache.get(file);
949
+ };
950
+ const runtimeFor = (route) => {
951
+ const routeModule = loadModule(route.file, isDirty(route.file));
798
952
  if (typeof routeModule.handle !== "function") {
799
953
  throw new Error(`${route.file} must export a named handle function.`);
800
954
  }
801
955
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
802
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
956
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
803
957
  );
804
958
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
805
- config.adapter.register(app, {
959
+ return {
806
960
  method: route.method,
807
961
  path: route.path,
808
962
  handle: routeModule.handle,
809
963
  middleware: [...folderMiddleware, ...verbMiddleware],
810
964
  input: routeInput(routeModule, route.file),
811
- services: options.services
965
+ services: options.services,
966
+ cookieSecret: config.cookieSecret
967
+ };
968
+ };
969
+ for (const route of routes) {
970
+ if (!options.lazy) {
971
+ config.adapter.register(app, runtimeFor(route));
972
+ continue;
973
+ }
974
+ let runtime;
975
+ const getRuntime = () => {
976
+ runtime ??= runtimeFor(route);
977
+ return runtime;
978
+ };
979
+ config.adapter.register(app, {
980
+ method: route.method,
981
+ path: route.path,
982
+ get handle() {
983
+ return getRuntime().handle;
984
+ },
985
+ get middleware() {
986
+ return getRuntime().middleware;
987
+ },
988
+ get input() {
989
+ return getRuntime().input;
990
+ },
991
+ services: options.services,
992
+ cookieSecret: config.cookieSecret
812
993
  });
813
994
  }
814
995
  } finally {
815
- unregisterAliasResolver();
816
- unregister();
996
+ unregisterAliasResolver?.();
997
+ loader?.unregister();
817
998
  }
818
999
  return { app, routes, paths };
819
1000
  }
820
1001
 
821
1002
  // src/generator/sync.ts
822
- var import_node_fs6 = require("fs");
823
- var import_promises3 = require("fs/promises");
824
- var import_node_path11 = require("path");
1003
+ var import_node_fs8 = require("fs");
1004
+ var import_promises4 = require("fs/promises");
1005
+ var import_node_path12 = require("path");
825
1006
 
826
1007
  // src/generator/app-types.ts
827
1008
  var import_node_fs4 = require("fs");
@@ -1073,6 +1254,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1073
1254
  const documentPaths = {};
1074
1255
  const schemas = {};
1075
1256
  const securitySchemes = {};
1257
+ const tagOrder = [];
1076
1258
  for (const route of routes) {
1077
1259
  if (data.hiddenFiles?.has(route.file)) {
1078
1260
  continue;
@@ -1080,10 +1262,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1080
1262
  const responses = data.responsesByFile?.get(route.file);
1081
1263
  const input = data.inputsByFile?.get(route.file);
1082
1264
  const security = data.securityByFile?.get(route.file);
1265
+ const meta = data.openapiByFile?.get(route.file);
1083
1266
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1084
1267
  schemas[name] = rewriteRefs(schema);
1085
1268
  }
1086
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1269
+ const operation = {};
1270
+ if (meta?.tags && meta.tags.length > 0) {
1271
+ operation.tags = meta.tags;
1272
+ for (const tag2 of meta.tags) {
1273
+ if (!tagOrder.includes(tag2)) {
1274
+ tagOrder.push(tag2);
1275
+ }
1276
+ }
1277
+ }
1278
+ if (meta?.summary) {
1279
+ operation.summary = meta.summary;
1280
+ }
1281
+ if (meta?.description) {
1282
+ operation.description = meta.description;
1283
+ }
1284
+ if (meta?.operationId) {
1285
+ operation.operationId = meta.operationId;
1286
+ }
1287
+ if (meta?.deprecated) {
1288
+ operation.deprecated = true;
1289
+ }
1290
+ operation.responses = buildResponses(responses?.responses ?? []);
1087
1291
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1088
1292
  if (parameters.length > 0) {
1089
1293
  operation.parameters = parameters;
@@ -1115,6 +1319,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1115
1319
  info: readProjectInfo(paths.cwd),
1116
1320
  paths: documentPaths
1117
1321
  };
1322
+ if (tagOrder.length > 0) {
1323
+ document.tags = tagOrder.map((name) => ({ name }));
1324
+ }
1118
1325
  const components = {};
1119
1326
  if (Object.keys(schemas).length > 0) {
1120
1327
  components.schemas = schemas;
@@ -1146,14 +1353,29 @@ function paramsType(params) {
1146
1353
  ${fields}
1147
1354
  }`;
1148
1355
  }
1149
- function varsType(typesDir, sharedFiles) {
1150
- if (sharedFiles.length === 0) {
1151
- return "{}";
1356
+ function middlewareVarsType(typesDir, sharedFile) {
1357
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1358
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1359
+ }
1360
+ function ownSharedFile(dir, sharedFiles) {
1361
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1362
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1363
+ return sharedFiles[index];
1364
+ }
1152
1365
  }
1153
- return sharedFiles.map((file) => {
1154
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1155
- return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1156
- }).join("\n & ");
1366
+ return void 0;
1367
+ }
1368
+ function varsType(paths, file, dir, sharedFiles) {
1369
+ const typesDir = (0, import_node_path8.dirname)(file);
1370
+ const parts = [];
1371
+ if (dir !== paths.routesDir) {
1372
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1373
+ }
1374
+ const ownShared = ownSharedFile(dir, sharedFiles);
1375
+ if (ownShared) {
1376
+ parts.push(middlewareVarsType(typesDir, ownShared));
1377
+ }
1378
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1157
1379
  }
1158
1380
  function methodExports(typesDir, verbs) {
1159
1381
  return verbs.map(({ method, file }) => {
@@ -1164,14 +1386,14 @@ function methodExports(typesDir, verbs) {
1164
1386
  });
1165
1387
  }
1166
1388
  async function writeParamTypes(paths, folders) {
1167
- for (const { dir, params, sharedFiles, verbs } of folders) {
1389
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1168
1390
  const file = typeFilePath(paths, dir);
1169
1391
  const typesDir = (0, import_node_path8.dirname)(file);
1170
1392
  const lines = [
1171
1393
  GENERATED_HEADER,
1172
1394
  `export type Params = ${paramsType(params)};`,
1173
1395
  "export type RouteParams = Params;",
1174
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1396
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1175
1397
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1176
1398
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1177
1399
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1181,10 +1403,14 @@ async function writeParamTypes(paths, folders) {
1181
1403
  lines.push(...methodExports(typesDir, verbs));
1182
1404
  }
1183
1405
  lines.push("");
1184
- await writeGenerated(file, lines.join("\n"));
1185
- }
1406
+ return writeGenerated(file, lines.join("\n"));
1407
+ }));
1186
1408
  }
1187
1409
 
1410
+ // src/generator/route-meta.ts
1411
+ var import_node_fs6 = require("fs");
1412
+ var import_typescript2 = __toESM(require("typescript"));
1413
+
1188
1414
  // src/generator/inputs.ts
1189
1415
  function sanitize(schema) {
1190
1416
  const { $schema, ...rest } = schema;
@@ -1245,28 +1471,315 @@ function readInput(routeModule) {
1245
1471
  }
1246
1472
  return input.body || input.query ? input : void 0;
1247
1473
  }
1248
- function hiddenFrom(value) {
1249
- if (value === false) {
1474
+ function hasExportModifier2(node) {
1475
+ return import_typescript2.default.canHaveModifiers(node) && (import_typescript2.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript2.default.SyntaxKind.ExportKeyword) ?? false);
1476
+ }
1477
+ function unwrapExpression(expression) {
1478
+ let current = expression;
1479
+ while (import_typescript2.default.isParenthesizedExpression(current) || import_typescript2.default.isAsExpression(current) || import_typescript2.default.isSatisfiesExpression(current)) {
1480
+ current = current.expression;
1481
+ }
1482
+ return current;
1483
+ }
1484
+ function staticBoolean(expression) {
1485
+ const value = unwrapExpression(expression);
1486
+ if (value.kind === import_typescript2.default.SyntaxKind.TrueKeyword) {
1250
1487
  return true;
1251
1488
  }
1252
- if (value === true) {
1489
+ if (value.kind === import_typescript2.default.SyntaxKind.FalseKeyword) {
1253
1490
  return false;
1254
1491
  }
1255
- if (value && typeof value === "object" && "hidden" in value) {
1256
- return Boolean(value.hidden);
1492
+ return void 0;
1493
+ }
1494
+ function staticString(expression) {
1495
+ const value = unwrapExpression(expression);
1496
+ return import_typescript2.default.isStringLiteralLike(value) ? value.text : void 0;
1497
+ }
1498
+ function staticStringArray(expression) {
1499
+ const value = unwrapExpression(expression);
1500
+ if (!import_typescript2.default.isArrayLiteralExpression(value)) {
1501
+ return void 0;
1502
+ }
1503
+ const strings = [];
1504
+ for (const element of value.elements) {
1505
+ const string = staticString(element);
1506
+ if (string === void 0) {
1507
+ return void 0;
1508
+ }
1509
+ strings.push(string);
1510
+ }
1511
+ return strings;
1512
+ }
1513
+ function propertyName2(name) {
1514
+ if (import_typescript2.default.isIdentifier(name) || import_typescript2.default.isStringLiteral(name) || import_typescript2.default.isNumericLiteral(name)) {
1515
+ return name.text;
1257
1516
  }
1258
1517
  return void 0;
1259
1518
  }
1260
- function collectHidden(route, routeModule, loadShared) {
1519
+ function collectImportedNames(source) {
1520
+ const names = /* @__PURE__ */ new Set();
1521
+ for (const statement of source.statements) {
1522
+ if (!import_typescript2.default.isImportDeclaration(statement)) {
1523
+ continue;
1524
+ }
1525
+ const clause = statement.importClause;
1526
+ if (!clause) {
1527
+ continue;
1528
+ }
1529
+ if (clause.name) {
1530
+ names.add(clause.name.text);
1531
+ }
1532
+ const bindings = clause.namedBindings;
1533
+ if (bindings && import_typescript2.default.isNamespaceImport(bindings)) {
1534
+ names.add(bindings.name.text);
1535
+ }
1536
+ if (bindings && import_typescript2.default.isNamedImports(bindings)) {
1537
+ for (const element of bindings.elements) {
1538
+ names.add(element.name.text);
1539
+ }
1540
+ }
1541
+ }
1542
+ return names;
1543
+ }
1544
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1545
+ let found = false;
1546
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1547
+ const visit = (node) => {
1548
+ if (found) {
1549
+ return;
1550
+ }
1551
+ if (import_typescript2.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1552
+ found = true;
1553
+ return;
1554
+ }
1555
+ import_typescript2.default.forEachChild(node, visit);
1556
+ };
1557
+ visit(expression);
1558
+ return found;
1559
+ }
1560
+ function parseStaticOpenApi(expression) {
1561
+ const value = unwrapExpression(expression);
1562
+ const boolean = staticBoolean(value);
1563
+ if (boolean !== void 0) {
1564
+ return boolean;
1565
+ }
1566
+ if (!import_typescript2.default.isObjectLiteralExpression(value)) {
1567
+ return void 0;
1568
+ }
1569
+ const openapi = {};
1570
+ for (const property of value.properties) {
1571
+ if (!import_typescript2.default.isPropertyAssignment(property)) {
1572
+ return void 0;
1573
+ }
1574
+ const name = propertyName2(property.name);
1575
+ if (!name) {
1576
+ return void 0;
1577
+ }
1578
+ if (name === "hidden") {
1579
+ const hidden = staticBoolean(property.initializer);
1580
+ if (hidden === void 0) {
1581
+ return void 0;
1582
+ }
1583
+ openapi.hidden = hidden;
1584
+ } else if (name === "tags") {
1585
+ const tags = staticStringArray(property.initializer);
1586
+ if (!tags) {
1587
+ return void 0;
1588
+ }
1589
+ openapi.tags = tags;
1590
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1591
+ const string = staticString(property.initializer);
1592
+ if (string === void 0) {
1593
+ return void 0;
1594
+ }
1595
+ openapi[name] = string;
1596
+ } else if (name === "deprecated") {
1597
+ const deprecated = staticBoolean(property.initializer);
1598
+ if (deprecated === void 0) {
1599
+ return void 0;
1600
+ }
1601
+ openapi.deprecated = deprecated;
1602
+ } else {
1603
+ return void 0;
1604
+ }
1605
+ }
1606
+ return openapi;
1607
+ }
1608
+ function readStaticModuleMeta(file) {
1609
+ let source;
1610
+ try {
1611
+ source = import_typescript2.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript2.default.ScriptTarget.Latest, true);
1612
+ } catch {
1613
+ return void 0;
1614
+ }
1615
+ const importedNames = collectImportedNames(source);
1616
+ const sourceText = source.getFullText();
1617
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1618
+ const meta = { middlewareSecurity: false };
1619
+ for (const statement of source.statements) {
1620
+ if (import_typescript2.default.isImportDeclaration(statement) || import_typescript2.default.isInterfaceDeclaration(statement) || import_typescript2.default.isTypeAliasDeclaration(statement) || import_typescript2.default.isEmptyStatement(statement) || !hasExportModifier2(statement)) {
1621
+ continue;
1622
+ }
1623
+ if (import_typescript2.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1624
+ continue;
1625
+ }
1626
+ if (import_typescript2.default.isVariableStatement(statement)) {
1627
+ for (const declaration of statement.declarationList.declarations) {
1628
+ if (!import_typescript2.default.isIdentifier(declaration.name)) {
1629
+ return void 0;
1630
+ }
1631
+ const name = declaration.name.text;
1632
+ if (name === "openapi") {
1633
+ if (!declaration.initializer) {
1634
+ return void 0;
1635
+ }
1636
+ const openapi = parseStaticOpenApi(declaration.initializer);
1637
+ if (openapi === void 0) {
1638
+ return void 0;
1639
+ }
1640
+ meta.openapi = openapi;
1641
+ } else if (name === "handle") {
1642
+ if (!declaration.initializer || !import_typescript2.default.isArrowFunction(declaration.initializer) && !import_typescript2.default.isFunctionExpression(declaration.initializer)) {
1643
+ return void 0;
1644
+ }
1645
+ continue;
1646
+ } else if (name === "middleware") {
1647
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1648
+ return void 0;
1649
+ }
1650
+ meta.middlewareSecurity = false;
1651
+ continue;
1652
+ } else {
1653
+ return void 0;
1654
+ }
1655
+ }
1656
+ continue;
1657
+ }
1658
+ return void 0;
1659
+ }
1660
+ return meta;
1661
+ }
1662
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1261
1663
  let hidden = false;
1664
+ const tags = [];
1665
+ const meta = {};
1666
+ const apply = (value, isVerb) => {
1667
+ if (value === false) {
1668
+ hidden = true;
1669
+ return;
1670
+ }
1671
+ if (value === true) {
1672
+ hidden = false;
1673
+ return;
1674
+ }
1675
+ if (!value) {
1676
+ return;
1677
+ }
1678
+ if (typeof value.hidden === "boolean") {
1679
+ hidden = value.hidden;
1680
+ }
1681
+ if (value.tags) {
1682
+ tags.push(...value.tags);
1683
+ }
1684
+ if (typeof value.summary === "string") {
1685
+ meta.summary = value.summary;
1686
+ }
1687
+ if (typeof value.description === "string") {
1688
+ meta.description = value.description;
1689
+ }
1690
+ if (typeof value.deprecated === "boolean") {
1691
+ meta.deprecated = value.deprecated;
1692
+ }
1693
+ if (isVerb && typeof value.operationId === "string") {
1694
+ meta.operationId = value.operationId;
1695
+ }
1696
+ };
1262
1697
  for (const file of route.sharedFiles) {
1263
- const opinion = hiddenFrom(loadShared(file).openapi);
1264
- if (opinion !== void 0) {
1265
- hidden = opinion;
1698
+ const shared = loadShared(file);
1699
+ if (!shared) {
1700
+ return void 0;
1701
+ }
1702
+ apply(shared.openapi, false);
1703
+ }
1704
+ apply(routeModule.openapi, true);
1705
+ if (tags.length > 0) {
1706
+ meta.tags = [...new Set(tags)];
1707
+ }
1708
+ return { hidden, meta };
1709
+ }
1710
+ function extractStaticMeta(route, routeModule, loadShared) {
1711
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1712
+ if (!openapi) {
1713
+ return void 0;
1714
+ }
1715
+ const meta = {};
1716
+ if (openapi.hidden) {
1717
+ meta.hidden = true;
1718
+ }
1719
+ if (Object.keys(openapi.meta).length > 0) {
1720
+ meta.openapi = openapi.meta;
1721
+ }
1722
+ return meta;
1723
+ }
1724
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
1725
+ const meta = {};
1726
+ const security = collectSecurity(route, {}, loadShared);
1727
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
1728
+ if (security) {
1729
+ meta.security = security;
1730
+ }
1731
+ if (hidden) {
1732
+ meta.hidden = true;
1733
+ }
1734
+ if (Object.keys(openapi).length > 0) {
1735
+ meta.openapi = openapi;
1736
+ }
1737
+ return meta;
1738
+ }
1739
+ function resolveOpenApi(route, routeModule, loadShared) {
1740
+ let hidden = false;
1741
+ const tags = [];
1742
+ const meta = {};
1743
+ const apply = (value, isVerb) => {
1744
+ if (value === false) {
1745
+ hidden = true;
1746
+ return;
1747
+ }
1748
+ if (value === true) {
1749
+ hidden = false;
1750
+ return;
1751
+ }
1752
+ if (!value || typeof value !== "object") {
1753
+ return;
1266
1754
  }
1755
+ const o = value;
1756
+ if ("hidden" in o) {
1757
+ hidden = Boolean(o.hidden);
1758
+ }
1759
+ if (Array.isArray(o.tags)) {
1760
+ tags.push(...o.tags.filter((tag2) => typeof tag2 === "string"));
1761
+ }
1762
+ if (typeof o.summary === "string") {
1763
+ meta.summary = o.summary;
1764
+ }
1765
+ if (typeof o.description === "string") {
1766
+ meta.description = o.description;
1767
+ }
1768
+ if (typeof o.deprecated === "boolean") {
1769
+ meta.deprecated = o.deprecated;
1770
+ }
1771
+ if (isVerb && typeof o.operationId === "string") {
1772
+ meta.operationId = o.operationId;
1773
+ }
1774
+ };
1775
+ for (const file of route.sharedFiles) {
1776
+ apply(loadShared(file).openapi, false);
1777
+ }
1778
+ apply(routeModule.openapi, true);
1779
+ if (tags.length > 0) {
1780
+ meta.tags = [...new Set(tags)];
1267
1781
  }
1268
- const verb = hiddenFrom(routeModule.openapi);
1269
- return verb ?? hidden;
1782
+ return { hidden, meta };
1270
1783
  }
1271
1784
  function collectSecurity(route, routeModule, loadShared) {
1272
1785
  const skipInherited = Boolean(
@@ -1298,6 +1811,33 @@ function collectSecurity(route, routeModule, loadShared) {
1298
1811
  }
1299
1812
  async function extractRouteMeta(config, paths, routes) {
1300
1813
  const byFile = /* @__PURE__ */ new Map();
1814
+ const remainingRoutes = [];
1815
+ const runtimeSharedRoutes = [];
1816
+ const staticCache = /* @__PURE__ */ new Map();
1817
+ const loadStatic = (file) => {
1818
+ if (!staticCache.has(file)) {
1819
+ staticCache.set(file, readStaticModuleMeta(file));
1820
+ }
1821
+ return staticCache.get(file);
1822
+ };
1823
+ for (const route of routes) {
1824
+ const routeModule = loadStatic(route.file);
1825
+ if (!routeModule) {
1826
+ remainingRoutes.push(route);
1827
+ continue;
1828
+ }
1829
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
1830
+ if (meta) {
1831
+ if (meta.hidden || meta.openapi) {
1832
+ byFile.set(route.file, meta);
1833
+ }
1834
+ continue;
1835
+ }
1836
+ runtimeSharedRoutes.push({ route, routeModule });
1837
+ }
1838
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
1839
+ return byFile;
1840
+ }
1301
1841
  const { unregister } = await safeRegister();
1302
1842
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1303
1843
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1312,13 +1852,19 @@ async function extractRouteMeta(config, paths, routes) {
1312
1852
  return sharedCache.get(file);
1313
1853
  };
1314
1854
  try {
1315
- for (const route of routes) {
1855
+ for (const { route, routeModule } of runtimeSharedRoutes) {
1856
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
1857
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1858
+ byFile.set(route.file, meta);
1859
+ }
1860
+ }
1861
+ for (const route of remainingRoutes) {
1316
1862
  try {
1317
1863
  const routeModule = loadModule2(route.file);
1318
1864
  const meta = {};
1319
1865
  const input = readInput(routeModule);
1320
1866
  const security = collectSecurity(route, routeModule, loadShared);
1321
- const hidden = collectHidden(route, routeModule, loadShared);
1867
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1322
1868
  if (input) {
1323
1869
  meta.input = input;
1324
1870
  }
@@ -1328,7 +1874,10 @@ async function extractRouteMeta(config, paths, routes) {
1328
1874
  if (hidden) {
1329
1875
  meta.hidden = true;
1330
1876
  }
1331
- if (meta.input || meta.security || meta.hidden) {
1877
+ if (Object.keys(openapi).length > 0) {
1878
+ meta.openapi = openapi;
1879
+ }
1880
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1332
1881
  byFile.set(route.file, meta);
1333
1882
  }
1334
1883
  } catch {
@@ -1400,6 +1949,103 @@ async function writeTsConfig(paths, config) {
1400
1949
  });
1401
1950
  }
1402
1951
 
1952
+ // src/generator/cache.ts
1953
+ var import_node_crypto = require("crypto");
1954
+ var import_node_fs7 = require("fs");
1955
+ var import_promises3 = require("fs/promises");
1956
+ var import_node_path11 = require("path");
1957
+ var import_tinyglobby2 = require("tinyglobby");
1958
+ var CACHE_VERSION = 1;
1959
+ var SYNC_CACHE_NAME = ".sync-cache.json";
1960
+ function stableConfig(config) {
1961
+ const alias = Object.entries(config.alias ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]);
1962
+ return { alias, outDir: config.outDir ?? ".giri" };
1963
+ }
1964
+ async function syncFingerprint(config, paths) {
1965
+ const outRelative = slash((0, import_node_path11.relative)(paths.cwd, paths.outDir));
1966
+ const ignore = ["**/node_modules/**", "**/.git/**"];
1967
+ if (outRelative && !outRelative.startsWith("..")) {
1968
+ ignore.push(`${outRelative}/**`);
1969
+ }
1970
+ const files = await (0, import_tinyglobby2.glob)([
1971
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}",
1972
+ "giri.config.{ts,js,mts,cts,mjs,cjs}",
1973
+ "tsconfig*.json",
1974
+ "package.json",
1975
+ "package-lock.json",
1976
+ "npm-shrinkwrap.json",
1977
+ "yarn.lock",
1978
+ "pnpm-lock.yaml",
1979
+ "bun.lock",
1980
+ "bun.lockb"
1981
+ ], {
1982
+ cwd: paths.cwd,
1983
+ absolute: false,
1984
+ onlyFiles: true,
1985
+ dot: true,
1986
+ ignore
1987
+ });
1988
+ const hash = (0, import_node_crypto.createHash)("sha256");
1989
+ hash.update(JSON.stringify(stableConfig(config)));
1990
+ for (const file of files.sort()) {
1991
+ hash.update("\0");
1992
+ hash.update(slash(file));
1993
+ hash.update("\0");
1994
+ hash.update(await (0, import_promises3.readFile)((0, import_node_path11.resolve)(paths.cwd, file)));
1995
+ }
1996
+ return hash.digest("hex");
1997
+ }
1998
+ function cachePath(paths) {
1999
+ return (0, import_node_path11.join)(paths.outDir, SYNC_CACHE_NAME);
2000
+ }
2001
+ function serializePath(paths, file) {
2002
+ return slash((0, import_node_path11.relative)(paths.cwd, file));
2003
+ }
2004
+ function deserializePath(paths, file) {
2005
+ return slash((0, import_node_path11.resolve)(paths.cwd, file.split("/").join(import_node_path11.sep)));
2006
+ }
2007
+ function serializeMap(paths, values) {
2008
+ return [...values].map(([file, value]) => [serializePath(paths, file), value]);
2009
+ }
2010
+ function deserializeMap(paths, values) {
2011
+ return new Map(values.map(([file, value]) => [deserializePath(paths, file), value]));
2012
+ }
2013
+ async function readSyncCache(paths, fingerprint) {
2014
+ const file = cachePath(paths);
2015
+ if (!(0, import_node_fs7.existsSync)(file)) {
2016
+ return void 0;
2017
+ }
2018
+ try {
2019
+ const cache = JSON.parse(await (0, import_promises3.readFile)(file, "utf8"));
2020
+ if (cache.version !== CACHE_VERSION || cache.fingerprint !== fingerprint) {
2021
+ return void 0;
2022
+ }
2023
+ return {
2024
+ responsesByFile: deserializeMap(paths, cache.data.responsesByFile),
2025
+ inputsByFile: deserializeMap(paths, cache.data.inputsByFile),
2026
+ securityByFile: deserializeMap(paths, cache.data.securityByFile),
2027
+ hiddenFiles: new Set(cache.data.hiddenFiles.map((entry) => deserializePath(paths, entry))),
2028
+ openapiByFile: deserializeMap(paths, cache.data.openapiByFile)
2029
+ };
2030
+ } catch {
2031
+ return void 0;
2032
+ }
2033
+ }
2034
+ async function writeSyncCache(paths, fingerprint, data) {
2035
+ const cache = {
2036
+ version: CACHE_VERSION,
2037
+ fingerprint,
2038
+ data: {
2039
+ responsesByFile: serializeMap(paths, data.responsesByFile),
2040
+ inputsByFile: serializeMap(paths, data.inputsByFile),
2041
+ securityByFile: serializeMap(paths, data.securityByFile),
2042
+ hiddenFiles: [...data.hiddenFiles].map((file) => serializePath(paths, file)),
2043
+ openapiByFile: serializeMap(paths, data.openapiByFile)
2044
+ }
2045
+ };
2046
+ await writeJson(cachePath(paths), cache);
2047
+ }
2048
+
1403
2049
  // src/generator/sync.ts
1404
2050
  async function typeFolders(paths, routes) {
1405
2051
  const verbsByDir = /* @__PURE__ */ new Map();
@@ -1410,10 +2056,11 @@ async function typeFolders(paths, routes) {
1410
2056
  verbsByDir.set(key, list);
1411
2057
  }
1412
2058
  const dirs = await scanRouteFolders(paths.routesDir);
2059
+ const sharedCache = /* @__PURE__ */ new Map();
1413
2060
  return dirs.map((dir) => ({
1414
2061
  dir,
1415
2062
  params: routeParamsForDir(paths.routesDir, dir),
1416
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
2063
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1417
2064
  verbs: verbsByDir.get(slash(dir)) ?? []
1418
2065
  }));
1419
2066
  }
@@ -1425,11 +2072,9 @@ async function extractResponses(paths, routes) {
1425
2072
  try {
1426
2073
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1427
2074
  const files = [...new Set(routes.map((route) => route.file))];
1428
- const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1429
- const program = createSchemaProgram2(
1430
- paths,
1431
- (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1432
- );
2075
+ const appTypes = (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts");
2076
+ const roots = (0, import_node_fs8.existsSync)(appTypes) ? [...files, appTypes] : files;
2077
+ const program = createSchemaProgram2(paths, roots);
1433
2078
  for (const file of files) {
1434
2079
  byFile.set(file, extractRouteResponses2(program, file));
1435
2080
  }
@@ -1442,8 +2087,9 @@ async function extractMeta(config, paths, routes) {
1442
2087
  const inputsByFile = /* @__PURE__ */ new Map();
1443
2088
  const securityByFile = /* @__PURE__ */ new Map();
1444
2089
  const hiddenFiles = /* @__PURE__ */ new Set();
2090
+ const openapiByFile = /* @__PURE__ */ new Map();
1445
2091
  if (routes.length === 0) {
1446
- return { inputsByFile, securityByFile, hiddenFiles };
2092
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1447
2093
  }
1448
2094
  try {
1449
2095
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1457,79 +2103,180 @@ async function extractMeta(config, paths, routes) {
1457
2103
  if (entry.hidden) {
1458
2104
  hiddenFiles.add(file);
1459
2105
  }
2106
+ if (entry.openapi) {
2107
+ openapiByFile.set(file, entry.openapi);
2108
+ }
1460
2109
  }
1461
2110
  } catch (error) {
1462
2111
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1463
2112
  }
1464
- return { inputsByFile, securityByFile, hiddenFiles };
2113
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1465
2114
  }
1466
2115
  async function syncProject(config, options = {}) {
1467
2116
  const paths = resolveGiriPaths(config, options.cwd);
1468
2117
  assertSafeOutDir(paths);
2118
+ const hadOutDir = (0, import_node_fs8.existsSync)(paths.outDir);
1469
2119
  const routes = await scanRoutes(paths.routesDir);
1470
2120
  const folders = await typeFolders(paths, routes);
1471
- await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
2121
+ const fingerprint = await syncFingerprint(config, paths);
2122
+ const cached = await readSyncCache(paths, fingerprint);
2123
+ const generatedFiles = [
2124
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2125
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2126
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2127
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2128
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2129
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2130
+ ];
2131
+ if (cached && generatedFiles.every(import_node_fs8.existsSync)) {
2132
+ return { paths, routes, folders, data: cached };
2133
+ }
2134
+ await (0, import_promises4.mkdir)(paths.outDir, { recursive: true });
1472
2135
  await writeParamTypes(paths, folders);
1473
2136
  await writeRouteTypes(paths, routes);
1474
2137
  await writeAppTypes(paths);
1475
2138
  await writeTsConfig(paths, config);
1476
- const responsesByFile = await extractResponses(paths, routes);
1477
- const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1478
- const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
2139
+ const data = cached ?? {
2140
+ responsesByFile: await extractResponses(paths, routes),
2141
+ ...await extractMeta(config, paths, routes)
2142
+ };
1479
2143
  await writeManifest(paths, routes, data);
1480
2144
  await writeOpenApi(paths, routes, data);
1481
- await pruneDir(
1482
- paths.outDir,
1483
- /* @__PURE__ */ new Set([
1484
- (0, import_node_path11.join)(paths.outDir, "tsconfig.json"),
1485
- (0, import_node_path11.join)(paths.outDir, "manifest.json"),
1486
- (0, import_node_path11.join)(paths.outDir, "openapi.json"),
1487
- (0, import_node_path11.join)(paths.outDir, "routes.d.ts"),
1488
- (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts"),
1489
- ...folders.map((folder) => typeFilePath(paths, folder.dir))
1490
- ])
1491
- );
2145
+ await writeSyncCache(paths, fingerprint, data);
2146
+ if (hadOutDir) {
2147
+ await pruneDir(
2148
+ paths.outDir,
2149
+ /* @__PURE__ */ new Set([
2150
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2151
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2152
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2153
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2154
+ (0, import_node_path12.join)(paths.outDir, SYNC_CACHE_NAME),
2155
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2156
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2157
+ ])
2158
+ );
2159
+ }
1492
2160
  return { paths, routes, folders, data };
1493
2161
  }
1494
2162
 
1495
2163
  // src/generator/watch.ts
1496
- var import_node_fs7 = require("fs");
1497
- var import_node_path13 = require("path");
2164
+ var import_node_fs10 = require("fs");
2165
+ var import_node_path15 = require("path");
1498
2166
 
1499
- // src/loader/module-loader.ts
1500
- var import_node_path12 = require("path");
1501
- var toSlash = (path) => path.split(import_node_path12.sep).join("/");
1502
- var isProjectModule = (id, root) => {
1503
- return id.startsWith(root) && !id.includes(`${import_node_path12.sep}node_modules${import_node_path12.sep}`) && !id.includes(`${import_node_path12.sep}.giri${import_node_path12.sep}`);
1504
- };
1505
- var buildModuleGraph = (cwd) => {
1506
- const root = (0, import_node_path12.resolve)(cwd) + import_node_path12.sep;
2167
+ // src/loader/import-graph.ts
2168
+ var import_node_fs9 = require("fs");
2169
+ var import_node_path13 = require("path");
2170
+ var import_typescript6 = __toESM(require("typescript"));
2171
+ var import_tinyglobby3 = require("tinyglobby");
2172
+ var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
2173
+ var JS_EXT = /\.(?:c|m)?jsx?$/;
2174
+ var toSlash = (path) => path.split(import_node_path13.sep).join("/");
2175
+ function probeFile(base) {
2176
+ if ((0, import_node_fs9.existsSync)(base) && (0, import_node_fs9.statSync)(base).isFile()) {
2177
+ return base;
2178
+ }
2179
+ for (const ext of RESOLVE_EXTS) {
2180
+ const candidate = base + ext;
2181
+ if ((0, import_node_fs9.existsSync)(candidate)) {
2182
+ return candidate;
2183
+ }
2184
+ }
2185
+ for (const ext of RESOLVE_EXTS) {
2186
+ const candidate = (0, import_node_path13.join)(base, `index${ext}`);
2187
+ if ((0, import_node_fs9.existsSync)(candidate)) {
2188
+ return candidate;
2189
+ }
2190
+ }
2191
+ return void 0;
2192
+ }
2193
+ function resolveSpecifier(specifier, fromFile, alias, cwd) {
2194
+ let target;
2195
+ if (specifier.startsWith(".")) {
2196
+ target = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(fromFile), specifier);
2197
+ } else {
2198
+ const aliased = resolveAliasRequest(specifier, alias, cwd);
2199
+ if (aliased === void 0) {
2200
+ return void 0;
2201
+ }
2202
+ target = aliased;
2203
+ }
2204
+ const resolved = probeFile(target);
2205
+ if (resolved) {
2206
+ return resolved;
2207
+ }
2208
+ if (JS_EXT.test(target)) {
2209
+ return probeFile(target.replace(JS_EXT, ""));
2210
+ }
2211
+ return void 0;
2212
+ }
2213
+ function importSpecifiers(file) {
2214
+ let source;
2215
+ try {
2216
+ source = import_typescript6.default.createSourceFile(file, (0, import_node_fs9.readFileSync)(file, "utf8"), import_typescript6.default.ScriptTarget.Latest, false);
2217
+ } catch {
2218
+ return [];
2219
+ }
2220
+ const specifiers = [];
2221
+ const visit = (node) => {
2222
+ if ((import_typescript6.default.isImportDeclaration(node) || import_typescript6.default.isExportDeclaration(node)) && node.moduleSpecifier && import_typescript6.default.isStringLiteral(node.moduleSpecifier)) {
2223
+ specifiers.push(node.moduleSpecifier.text);
2224
+ } else if (import_typescript6.default.isImportEqualsDeclaration(node) && import_typescript6.default.isExternalModuleReference(node.moduleReference) && import_typescript6.default.isStringLiteralLike(node.moduleReference.expression)) {
2225
+ specifiers.push(node.moduleReference.expression.text);
2226
+ } else if (import_typescript6.default.isCallExpression(node)) {
2227
+ const isRequire = import_typescript6.default.isIdentifier(node.expression) && node.expression.text === "require";
2228
+ const isDynamicImport = node.expression.kind === import_typescript6.default.SyntaxKind.ImportKeyword;
2229
+ const [first] = node.arguments;
2230
+ if ((isRequire || isDynamicImport) && first && import_typescript6.default.isStringLiteralLike(first)) {
2231
+ specifiers.push(first.text);
2232
+ }
2233
+ }
2234
+ import_typescript6.default.forEachChild(node, visit);
2235
+ };
2236
+ visit(source);
2237
+ return specifiers;
2238
+ }
2239
+ async function buildImportGraph(config, cwd) {
2240
+ const root = (0, import_node_path13.resolve)(cwd);
2241
+ const outDir = (0, import_node_path13.resolve)(root, config.outDir ?? ".giri") + import_node_path13.sep;
2242
+ const outRel = (0, import_node_path13.relative)(root, outDir).split(import_node_path13.sep).join("/").replace(/\/$/, "");
2243
+ const files = await (0, import_tinyglobby3.glob)("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", {
2244
+ cwd: root,
2245
+ absolute: true,
2246
+ onlyFiles: true,
2247
+ ignore: ["**/node_modules/**", "**/.git/**", outRel ? `${outRel}/**` : ".giri/**"]
2248
+ });
1507
2249
  const importers = /* @__PURE__ */ new Map();
1508
2250
  const nodes = /* @__PURE__ */ new Set();
1509
- for (const id of Object.keys(require.cache)) {
1510
- if (!isProjectModule(id, root)) {
1511
- continue;
1512
- }
1513
- const mod = require.cache[id];
1514
- if (!mod) {
2251
+ for (const file of files) {
2252
+ if (file.startsWith(outDir)) {
1515
2253
  continue;
1516
2254
  }
1517
- nodes.add(toSlash(id));
1518
- for (const child of mod.children) {
1519
- if (!isProjectModule(child.id, root)) {
2255
+ nodes.add(toSlash(file));
2256
+ for (const specifier of importSpecifiers(file)) {
2257
+ const dep = resolveSpecifier(specifier, file, config.alias, root);
2258
+ if (!dep || dep.startsWith(outDir)) {
1520
2259
  continue;
1521
2260
  }
1522
- nodes.add(toSlash(child.id));
1523
- const dep = toSlash(child.id);
1524
- let set = importers.get(dep);
2261
+ const from = toSlash(file);
2262
+ const to = toSlash(dep);
2263
+ nodes.add(to);
2264
+ let set = importers.get(to);
1525
2265
  if (!set) {
1526
2266
  set = /* @__PURE__ */ new Set();
1527
- importers.set(dep, set);
2267
+ importers.set(to, set);
1528
2268
  }
1529
- set.add(toSlash(id));
2269
+ set.add(from);
1530
2270
  }
1531
2271
  }
1532
2272
  return { importers, nodes };
2273
+ }
2274
+
2275
+ // src/loader/module-loader.ts
2276
+ var import_node_path14 = require("path");
2277
+ var toSlash2 = (path) => path.split(import_node_path14.sep).join("/");
2278
+ var isProjectModule = (id, root) => {
2279
+ return id.startsWith(root) && !id.includes(`${import_node_path14.sep}node_modules${import_node_path14.sep}`) && !id.includes(`${import_node_path14.sep}.giri${import_node_path14.sep}`);
1533
2280
  };
1534
2281
  var collectDependents = (graph, start) => {
1535
2282
  const out = /* @__PURE__ */ new Set([start]);
@@ -1547,26 +2294,36 @@ var collectDependents = (graph, start) => {
1547
2294
  };
1548
2295
  var purgeModules = (files) => {
1549
2296
  for (const id of Object.keys(require.cache)) {
1550
- if (files.has(toSlash(id))) {
2297
+ if (files.has(toSlash2(id))) {
1551
2298
  delete require.cache[id];
1552
2299
  }
1553
2300
  }
1554
2301
  };
1555
2302
  var purgeProjectModules = (cwd) => {
1556
- const root = (0, import_node_path12.resolve)(cwd) + import_node_path12.sep;
2303
+ const root = (0, import_node_path14.resolve)(cwd) + import_node_path14.sep;
1557
2304
  for (const id of Object.keys(require.cache)) {
1558
2305
  if (isProjectModule(id, root)) {
1559
2306
  delete require.cache[id];
1560
2307
  }
1561
2308
  }
1562
2309
  };
2310
+ var purgeGeneratedModules = (outDir) => {
2311
+ const root = (0, import_node_path14.resolve)(outDir) + import_node_path14.sep;
2312
+ for (const id of Object.keys(require.cache)) {
2313
+ if ((0, import_node_path14.resolve)(id).startsWith(root)) {
2314
+ delete require.cache[id];
2315
+ }
2316
+ }
2317
+ };
1563
2318
 
1564
2319
  // src/generator/watch.ts
1565
2320
  function createWatchUpdater(config, initial) {
1566
2321
  const paths = initial.paths;
1567
2322
  let routes = initial.routes;
1568
2323
  const data = initial.data;
2324
+ let metadataQueue = Promise.resolve();
1569
2325
  const fullResync = async () => {
2326
+ await metadataQueue;
1570
2327
  purgeProjectModules(paths.cwd);
1571
2328
  const result = await syncProject(config, { cwd: paths.cwd });
1572
2329
  routes = result.routes;
@@ -1574,48 +2331,84 @@ function createWatchUpdater(config, initial) {
1574
2331
  data.inputsByFile = result.data.inputsByFile;
1575
2332
  data.securityByFile = result.data.securityByFile;
1576
2333
  data.hiddenFiles = result.data.hiddenFiles;
2334
+ data.openapiByFile = result.data.openapiByFile;
2335
+ purgeGeneratedModules(paths.outDir);
1577
2336
  return "full";
1578
2337
  };
1579
- const reextractRoute = async (route) => {
1580
- const key = route.file;
2338
+ const reextractRoutes = async (affected) => {
2339
+ if (affected.length === 0) {
2340
+ return;
2341
+ }
1581
2342
  try {
1582
2343
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1583
- const appTypes = (0, import_node_path13.join)(paths.outDir, "types", "app.d.ts");
1584
- const program = createSchemaProgram2(paths, (0, import_node_fs7.existsSync)(appTypes) ? [key, appTypes] : [key]);
1585
- data.responsesByFile.set(key, extractRouteResponses2(program, key));
2344
+ const appTypes = (0, import_node_path15.join)(paths.outDir, "types", "app.d.ts");
2345
+ const files = affected.map((route) => route.file);
2346
+ const program = createSchemaProgram2(
2347
+ paths,
2348
+ (0, import_node_fs10.existsSync)(appTypes) ? [...files, appTypes] : files
2349
+ );
2350
+ for (const route of affected) {
2351
+ data.responsesByFile.set(
2352
+ route.file,
2353
+ extractRouteResponses2(program, route.file)
2354
+ );
2355
+ }
1586
2356
  } catch {
1587
2357
  }
1588
2358
  try {
1589
- const meta = await extractRouteMeta(config, paths, [route]);
1590
- const entry = meta.get(key);
1591
- data.inputsByFile.delete(key);
1592
- data.securityByFile.delete(key);
1593
- data.hiddenFiles.delete(key);
1594
- if (entry?.input) {
1595
- data.inputsByFile.set(key, entry.input);
1596
- }
1597
- if (entry?.security) {
1598
- data.securityByFile.set(key, entry.security);
1599
- }
1600
- if (entry?.hidden) {
1601
- data.hiddenFiles.add(key);
2359
+ const meta = await extractRouteMeta(config, paths, affected);
2360
+ for (const route of affected) {
2361
+ const key = route.file;
2362
+ const entry = meta.get(key);
2363
+ data.inputsByFile.delete(key);
2364
+ data.securityByFile.delete(key);
2365
+ data.hiddenFiles.delete(key);
2366
+ data.openapiByFile.delete(key);
2367
+ if (entry?.input) {
2368
+ data.inputsByFile.set(key, entry.input);
2369
+ }
2370
+ if (entry?.security) {
2371
+ data.securityByFile.set(key, entry.security);
2372
+ }
2373
+ if (entry?.hidden) {
2374
+ data.hiddenFiles.add(key);
2375
+ }
2376
+ if (entry?.openapi) {
2377
+ data.openapiByFile.set(key, entry.openapi);
2378
+ }
1602
2379
  }
1603
2380
  } catch {
1604
2381
  }
1605
2382
  };
1606
2383
  return {
1607
- async apply(filename) {
2384
+ settled: () => metadataQueue,
2385
+ async apply(filename, options = {}) {
1608
2386
  if (!filename) {
1609
2387
  return fullResync();
1610
2388
  }
1611
- const abs = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(paths.routesDir), filename);
2389
+ const abs = (0, import_node_path15.resolve)((0, import_node_path15.dirname)(paths.routesDir), filename);
1612
2390
  const file = slash(abs);
1613
- if (!(0, import_node_fs7.existsSync)(abs)) {
2391
+ if (!(0, import_node_fs10.existsSync)(abs)) {
1614
2392
  return fullResync();
1615
2393
  }
1616
- const graph = buildModuleGraph(paths.cwd);
2394
+ if ((0, import_node_fs10.statSync)(abs).isDirectory()) {
2395
+ return "skip";
2396
+ }
2397
+ assertSourceSyntax(abs);
1617
2398
  const isRoute = routes.some((candidate) => slash(candidate.file) === file);
1618
- if (!graph.nodes.has(file) && !isRoute) {
2399
+ const isShared = routes.some(
2400
+ (route) => route.sharedFiles.some((shared) => slash(shared) === file)
2401
+ );
2402
+ const isMethodFile = /^\+(?:get|post|put|patch|delete|options|head)\.(?:[cm]?[jt]s|[jt]sx)$/i.test((0, import_node_path15.basename)(file));
2403
+ const isNewRouteStructure = file.startsWith(`${slash(paths.routesDir)}/`) && /^\+(?:get|post|put|patch|delete|options|head|shared)\.(?:[cm]?[jt]s|[jt]sx)$/i.test((0, import_node_path15.basename)(file)) && !isRoute && !isShared;
2404
+ if (isRoute || isNewRouteStructure && isMethodFile) {
2405
+ assertRouteHandleExport(abs);
2406
+ }
2407
+ if (isNewRouteStructure) {
2408
+ return fullResync();
2409
+ }
2410
+ const graph = await buildImportGraph(config, paths.cwd);
2411
+ if (!graph.nodes.has(file) && !isRoute && !isShared) {
1619
2412
  return fullResync();
1620
2413
  }
1621
2414
  const dependents = collectDependents(graph, file);
@@ -1623,31 +2416,40 @@ function createWatchUpdater(config, initial) {
1623
2416
  (route) => dependents.has(slash(route.file)) || route.sharedFiles.some((shared) => dependents.has(slash(shared)))
1624
2417
  );
1625
2418
  purgeModules(dependents);
1626
- for (const route of affected) {
1627
- await reextractRoute(route);
2419
+ const refreshMetadata = async () => {
2420
+ await reextractRoutes(affected);
2421
+ await writeManifest(paths, routes, data);
2422
+ await writeOpenApi(paths, routes, data);
2423
+ await writeSyncCache(paths, await syncFingerprint(config, paths), data);
2424
+ purgeGeneratedModules(paths.outDir);
2425
+ };
2426
+ const task = metadataQueue.then(refreshMetadata);
2427
+ metadataQueue = task.catch((error) => {
2428
+ console.error("giri: deferred metadata update failed", error);
2429
+ });
2430
+ if (!options.deferMetadata) {
2431
+ await task;
1628
2432
  }
1629
- await writeManifest(paths, routes, data);
1630
- await writeOpenApi(paths, routes, data);
1631
2433
  return "incremental";
1632
2434
  }
1633
2435
  };
1634
2436
  }
1635
2437
 
1636
2438
  // src/lifecycle.ts
1637
- var import_node_fs8 = require("fs");
1638
- var import_node_path14 = require("path");
2439
+ var import_node_fs11 = require("fs");
2440
+ var import_node_path16 = require("path");
1639
2441
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1640
2442
  function resolveMainFile(cwd) {
1641
2443
  for (const ext of MAIN_EXTENSIONS2) {
1642
- const file = (0, import_node_path14.join)(cwd, "src", `main.${ext}`);
1643
- if ((0, import_node_fs8.existsSync)(file)) {
2444
+ const file = (0, import_node_path16.join)(cwd, "src", `main.${ext}`);
2445
+ if ((0, import_node_fs11.existsSync)(file)) {
1644
2446
  return file;
1645
2447
  }
1646
2448
  }
1647
2449
  return void 0;
1648
2450
  }
1649
2451
  async function loadLifecycle(cwd = process.cwd()) {
1650
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2452
+ const file = resolveMainFile((0, import_node_path16.resolve)(cwd));
1651
2453
  if (!file) {
1652
2454
  return {};
1653
2455
  }
@@ -1719,6 +2521,12 @@ var tag = {
1719
2521
  warn: color.bold(color.yellow(`[${TAG}]`)),
1720
2522
  error: color.bold(color.red(`[${TAG}]`))
1721
2523
  };
2524
+ function formatError(error) {
2525
+ if (error instanceof Error) {
2526
+ return error.stack ?? `${error.name}: ${error.message}`;
2527
+ }
2528
+ return String(error);
2529
+ }
1722
2530
  var log2 = {
1723
2531
  info(message, scope) {
1724
2532
  console.log(line(tag.info, message, scope));
@@ -1805,25 +2613,25 @@ function parseFlags(args) {
1805
2613
  return flags;
1806
2614
  }
1807
2615
  async function ensureGitignore(cwd) {
1808
- const file = (0, import_node_path15.join)(cwd, ".gitignore");
2616
+ const file = (0, import_node_path17.join)(cwd, ".gitignore");
1809
2617
  const entry = ".giri";
1810
- if (!(0, import_node_fs9.existsSync)(file)) {
1811
- await (0, import_promises4.writeFile)(file, `${entry}
2618
+ if (!(0, import_node_fs12.existsSync)(file)) {
2619
+ await (0, import_promises5.writeFile)(file, `${entry}
1812
2620
  `);
1813
2621
  return;
1814
2622
  }
1815
- const content = await (0, import_promises4.readFile)(file, "utf8");
2623
+ const content = await (0, import_promises5.readFile)(file, "utf8");
1816
2624
  if (!content.split(/\r?\n/).includes(entry)) {
1817
- await (0, import_promises4.appendFile)(file, `${content.endsWith("\n") ? "" : "\n"}${entry}
2625
+ await (0, import_promises5.appendFile)(file, `${content.endsWith("\n") ? "" : "\n"}${entry}
1818
2626
  `);
1819
2627
  }
1820
2628
  }
1821
2629
  async function ensureTsConfig(cwd) {
1822
- const file = (0, import_node_path15.join)(cwd, "tsconfig.json");
1823
- if ((0, import_node_fs9.existsSync)(file)) {
2630
+ const file = (0, import_node_path17.join)(cwd, "tsconfig.json");
2631
+ if ((0, import_node_fs12.existsSync)(file)) {
1824
2632
  return;
1825
2633
  }
1826
- await (0, import_promises4.writeFile)(
2634
+ await (0, import_promises5.writeFile)(
1827
2635
  file,
1828
2636
  `${JSON.stringify(
1829
2637
  {
@@ -1849,7 +2657,7 @@ async function ensureTsConfig(cwd) {
1849
2657
  async function missingDeps(cwd, candidates) {
1850
2658
  let pkg = {};
1851
2659
  try {
1852
- pkg = JSON.parse(await (0, import_promises4.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8"));
2660
+ pkg = JSON.parse(await (0, import_promises5.readFile)((0, import_node_path17.join)(cwd, "package.json"), "utf8"));
1853
2661
  } catch {
1854
2662
  }
1855
2663
  const present = /* @__PURE__ */ new Set();
@@ -1918,7 +2726,7 @@ async function selectAdapter(interactive) {
1918
2726
  return ADAPTERS.find((adapter) => adapter.value === picked) ?? null;
1919
2727
  }
1920
2728
  async function initProject(cwd, flags) {
1921
- if (!(0, import_node_fs9.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
2729
+ if (!(0, import_node_fs12.existsSync)((0, import_node_path17.join)(cwd, "package.json"))) {
1922
2730
  throw new Error(
1923
2731
  "No package.json found. Run `giri init` inside an existing project - set one up first (e.g. `npm init -y` and install typescript), then re-run."
1924
2732
  );
@@ -1943,14 +2751,14 @@ async function initProject(cwd, flags) {
1943
2751
  prompts.cancel(`The ${adapter.label} adapter isn't available yet - only Hono ships today.`);
1944
2752
  return;
1945
2753
  }
1946
- const configPath = (0, import_node_path15.join)(cwd, "giri.config.ts");
1947
- if (!(0, import_node_fs9.existsSync)(configPath)) {
1948
- await (0, import_promises4.writeFile)(configPath, configSource(adapter));
2754
+ const configPath = (0, import_node_path17.join)(cwd, "giri.config.ts");
2755
+ if (!(0, import_node_fs12.existsSync)(configPath)) {
2756
+ await (0, import_promises5.writeFile)(configPath, configSource(adapter));
1949
2757
  }
1950
- const routePath = (0, import_node_path15.join)(cwd, "src", "routes", "+get.ts");
1951
- if (!(0, import_node_fs9.existsSync)(routePath)) {
1952
- await (0, import_promises4.mkdir)((0, import_node_path15.join)(cwd, "src", "routes"), { recursive: true });
1953
- await (0, import_promises4.writeFile)(
2758
+ const routePath = (0, import_node_path17.join)(cwd, "src", "routes", "+get.ts");
2759
+ if (!(0, import_node_fs12.existsSync)(routePath)) {
2760
+ await (0, import_promises5.mkdir)((0, import_node_path17.join)(cwd, "src", "routes"), { recursive: true });
2761
+ await (0, import_promises5.writeFile)(
1954
2762
  routePath,
1955
2763
  [
1956
2764
  'import type { Handle } from "@boon4681/giri";',
@@ -2017,69 +2825,158 @@ function displayHost(address) {
2017
2825
  return address.includes(":") ? `[${address}]` : address;
2018
2826
  }
2019
2827
  async function serveProject(config, flags) {
2020
- const initial = await syncProject(config);
2021
- log2.success(
2022
- `synced ${initial.routes.length} route${initial.routes.length === 1 ? "" : "s"} ${muted(`at ${initial.paths.outDir}`)}`,
2023
- "sync"
2024
- );
2025
- const lifecycle = await loadLifecycle();
2026
- const services = await runInit(lifecycle);
2027
- let current = await buildGiriApp(config, { services });
2028
- const port = flags.port ?? config.server?.port ?? 3e3;
2029
- const hostname = flags.hostname ?? config.server?.hostname;
2030
- if (flags.watch) {
2031
- const srcDir = (0, import_node_path15.resolve)(current.paths.routesDir, "..");
2032
- if ((0, import_node_fs9.existsSync)(srcDir)) {
2033
- let timer;
2034
- let syncing = false;
2035
- const changed = /* @__PURE__ */ new Set();
2036
- const { watch } = await import("fs");
2037
- const updater = createWatchUpdater(config, initial);
2038
- const hmrCount = /* @__PURE__ */ new Map();
2039
- const bump = (key) => {
2040
- const next = (hmrCount.get(key) ?? 0) + 1;
2041
- hmrCount.set(key, next);
2042
- return next;
2043
- };
2044
- const flush = async () => {
2045
- if (syncing) {
2046
- return;
2047
- }
2048
- syncing = true;
2049
- try {
2050
- while (changed.size > 0) {
2051
- const batch = [...changed];
2052
- changed.clear();
2053
- for (const name of batch) {
2054
- const outcome = await updater.apply(name || null);
2055
- const rel = name ? `src/${name.replace(/\\/g, "/")}` : "src";
2056
- log2.change(outcome === "full" ? "sync" : "update", rel, bump(rel));
2828
+ Error.stackTraceLimit = 30;
2829
+ const { watch } = await import("chokidar");
2830
+ let stop;
2831
+ const boot = async (cfg) => {
2832
+ const closers = [];
2833
+ closers.push(registerAliasResolver(cfg.alias, (0, import_node_path17.resolve)(process.cwd())));
2834
+ const lifecycle = await loadLifecycle();
2835
+ const sync = syncProject(cfg).then((initial2) => {
2836
+ log2.success(
2837
+ `synced ${initial2.routes.length} route${initial2.routes.length === 1 ? "" : "s"} ${muted(`at ${initial2.paths.outDir}`)}`,
2838
+ "sync"
2839
+ );
2840
+ return initial2;
2841
+ });
2842
+ const [initial, services] = await Promise.all([sync, runInit(lifecycle)]);
2843
+ const loader = await safeRegister();
2844
+ closers.push(loader.unregister);
2845
+ let current = await buildGiriApp(cfg, {
2846
+ services,
2847
+ lazy: true,
2848
+ loaderRegistered: true,
2849
+ aliasResolverRegistered: true
2850
+ });
2851
+ const port = flags.port ?? cfg.server?.port ?? 3e3;
2852
+ const hostname = flags.hostname ?? cfg.server?.hostname;
2853
+ if (flags.watch) {
2854
+ const srcDir = (0, import_node_path17.resolve)(current.paths.routesDir, "..");
2855
+ if ((0, import_node_fs12.existsSync)(srcDir)) {
2856
+ let timer;
2857
+ let syncing = false;
2858
+ const changed = /* @__PURE__ */ new Set();
2859
+ const updater = createWatchUpdater(cfg, initial);
2860
+ const hmrCount = /* @__PURE__ */ new Map();
2861
+ const bump = (key) => {
2862
+ const next = (hmrCount.get(key) ?? 0) + 1;
2863
+ hmrCount.set(key, next);
2864
+ return next;
2865
+ };
2866
+ const flush = async () => {
2867
+ if (syncing) {
2868
+ return;
2869
+ }
2870
+ syncing = true;
2871
+ try {
2872
+ while (changed.size > 0) {
2873
+ const batch = [...changed];
2874
+ changed.clear();
2875
+ let rebuild = false;
2876
+ let fullSync = false;
2877
+ const dirtySet = /* @__PURE__ */ new Set();
2878
+ for (const name of batch) {
2879
+ const outcome = await updater.apply(name || null, {
2880
+ deferMetadata: true
2881
+ });
2882
+ if (outcome === "skip") {
2883
+ continue;
2884
+ }
2885
+ rebuild = true;
2886
+ if (name) {
2887
+ dirtySet.add((0, import_node_path17.resolve)(srcDir, name));
2888
+ }
2889
+ const rel = name ? `src/${name.replace(/\\/g, "/")}` : "src";
2890
+ log2.change(outcome === "full" ? "sync" : "update", rel, bump(rel));
2891
+ if (outcome === "full") {
2892
+ fullSync = true;
2893
+ break;
2894
+ }
2895
+ }
2896
+ if (rebuild) {
2897
+ current = await buildGiriApp(cfg, {
2898
+ services,
2899
+ dirty: fullSync ? void 0 : dirtySet,
2900
+ lazy: true,
2901
+ loaderRegistered: true,
2902
+ aliasResolverRegistered: true
2903
+ });
2904
+ }
2057
2905
  }
2058
- current = await buildGiriApp(config, { services });
2906
+ } catch (error) {
2907
+ log2.error(formatError(error), "watch");
2908
+ } finally {
2909
+ syncing = false;
2059
2910
  }
2060
- } catch (error) {
2061
- log2.error(error instanceof Error ? error.message : String(error), "watch");
2062
- } finally {
2063
- syncing = false;
2064
- }
2065
- if (changed.size > 0) {
2066
- void flush();
2067
- }
2068
- };
2069
- watch(srcDir, { recursive: true }, (_event, filename) => {
2070
- changed.add(filename ? filename.toString() : "");
2071
- clearTimeout(timer);
2072
- timer = setTimeout(() => void flush(), 150);
2073
- });
2911
+ if (changed.size > 0) {
2912
+ void flush();
2913
+ }
2914
+ };
2915
+ const watcher = watch(srcDir, { persistent: true, ignoreInitial: true });
2916
+ const onFileChange = (filename) => {
2917
+ changed.add((0, import_node_path17.isAbsolute)(filename) ? (0, import_node_path17.relative)(srcDir, filename) : filename);
2918
+ clearTimeout(timer);
2919
+ timer = setTimeout(() => void flush(), 150);
2920
+ };
2921
+ watcher.on("change", onFileChange);
2922
+ watcher.on("add", onFileChange);
2923
+ watcher.on("unlink", onFileChange);
2924
+ closers.push(() => {
2925
+ clearTimeout(timer);
2926
+ watcher.close();
2927
+ });
2928
+ }
2074
2929
  }
2930
+ const handler = (request) => cfg.adapter.fetch(current.app, request);
2931
+ const server = cfg.adapter.serve(handler, { port, hostname }, (info) => {
2932
+ log2.ready(`http://${displayHost(info.address)}:${info.port}`);
2933
+ });
2934
+ stop = async () => {
2935
+ for (const close of closers) {
2936
+ await close();
2937
+ }
2938
+ try {
2939
+ await server.close();
2940
+ } catch {
2941
+ }
2942
+ if (lifecycle.teardown) {
2943
+ await lifecycle.teardown(services);
2944
+ }
2945
+ };
2946
+ };
2947
+ await boot(config);
2948
+ const configPath = flags.watch ? findConfigPath((0, import_node_path17.resolve)(process.cwd())) : void 0;
2949
+ if (configPath) {
2950
+ let timer;
2951
+ let restarting = false;
2952
+ const configName = (0, import_node_path17.basename)(configPath);
2953
+ const restart = async () => {
2954
+ if (restarting) {
2955
+ return;
2956
+ }
2957
+ restarting = true;
2958
+ try {
2959
+ log2.info(`${color.green("restart")} ${configName} changed`, "config");
2960
+ delete require.cache[configPath];
2961
+ const next = await load({ throwOnError: true });
2962
+ await stop?.();
2963
+ await boot(next);
2964
+ } catch (error) {
2965
+ log2.error(error instanceof Error ? error.message : String(error), "config");
2966
+ log2.info("kept the previous server running \u2014 fix the config and save again", "config");
2967
+ } finally {
2968
+ restarting = false;
2969
+ }
2970
+ };
2971
+ const configWatcher = watch(configPath, { persistent: true, ignoreInitial: true });
2972
+ configWatcher.on("all", () => {
2973
+ clearTimeout(timer);
2974
+ timer = setTimeout(() => void restart(), 150);
2975
+ });
2075
2976
  }
2076
- const handler = (request) => config.adapter.fetch(current.app, request);
2077
- const server = config.adapter.serve(handler, { port, hostname }, (info) => {
2078
- log2.ready(`http://${displayHost(info.address)}:${info.port}`);
2079
- });
2080
- registerShutdown(server, lifecycle, services);
2977
+ registerShutdown(() => stop?.());
2081
2978
  }
2082
- function registerShutdown(server, lifecycle, services) {
2979
+ function registerShutdown(cleanup) {
2083
2980
  let shuttingDown = false;
2084
2981
  const shutdown = async () => {
2085
2982
  if (shuttingDown) {
@@ -2087,10 +2984,7 @@ function registerShutdown(server, lifecycle, services) {
2087
2984
  }
2088
2985
  shuttingDown = true;
2089
2986
  try {
2090
- await server.close();
2091
- if (lifecycle.teardown) {
2092
- await lifecycle.teardown(services);
2093
- }
2987
+ await cleanup();
2094
2988
  } catch (error) {
2095
2989
  log2.error(error instanceof Error ? error.message : String(error));
2096
2990
  process.exitCode = 1;
@@ -2103,7 +2997,7 @@ function registerShutdown(server, lifecycle, services) {
2103
2997
  }
2104
2998
  async function main() {
2105
2999
  const [command = "help", ...args] = process.argv.slice(2);
2106
- const cwd = (0, import_node_path15.resolve)(process.cwd());
3000
+ const cwd = (0, import_node_path17.resolve)(process.cwd());
2107
3001
  if (command === "help" || command === "--help" || command === "-h") {
2108
3002
  help();
2109
3003
  return;