@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/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_typescript2.default.findConfigFile(paths.cwd, import_typescript2.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_typescript2.default.getParsedCommandLineOfConfigFile(configPath, {}, {
54
+ ...import_typescript2.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_typescript2.default.createProgram(routeFiles, options);
63
63
  }
64
- var import_typescript, DEFAULT_OPTIONS;
64
+ var import_typescript2, 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_typescript2 = __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_typescript2.default.ScriptTarget.ES2022,
71
+ module: import_typescript2.default.ModuleKind.NodeNext,
72
+ moduleResolution: import_typescript2.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_typescript3.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_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void | import_typescript3.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_typescript3.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript3.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_typescript3.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript3.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript3.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_typescript3.default.TypeFlags.Any | import_typescript3.default.TypeFlags.Unknown)) {
194
194
  return {};
195
195
  }
196
- if (flags & import_typescript2.default.TypeFlags.Null) {
196
+ if (flags & import_typescript3.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_typescript3.default.TypeFlags.Undefined | import_typescript3.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_typescript3.default.TypeFlags.BigInt | import_typescript3.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_typescript3.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_typescript3.default.TypeFlags.String) {
216
216
  return { type: "string" };
217
217
  }
218
- if (flags & import_typescript2.default.TypeFlags.Number) {
218
+ if (flags & import_typescript3.default.TypeFlags.Number) {
219
219
  return { type: "number" };
220
220
  }
221
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
221
+ if (flags & import_typescript3.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_typescript3.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_typescript3;
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_typescript3 = __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_typescript4.default.canHaveModifiers(node) && (import_typescript4.default.getModifiers(node)?.some((m) => m.kind === import_typescript4.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_typescript4.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_typescript4.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_typescript4.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript4.default.isArrowFunction(declaration.initializer) || import_typescript4.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_typescript4.default.isArrowFunction(fn) && !import_typescript4.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_typescript4.default.isFunctionDeclaration(node) || import_typescript4.default.isFunctionExpression(node) || import_typescript4.default.isArrowFunction(node)) {
268
268
  return;
269
269
  }
270
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
270
+ if (import_typescript4.default.isReturnStatement(node) && node.expression) {
271
271
  expressions.push(node.expression);
272
272
  }
273
- import_typescript3.default.forEachChild(node, visit);
273
+ import_typescript4.default.forEachChild(node, visit);
274
274
  };
275
- import_typescript3.default.forEachChild(fn.body, visit);
275
+ import_typescript4.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_typescript4.default.isIdentifier(first.name) ? first.name.text : void 0;
290
+ }
291
+ function readFromCall(checker, expression, contextName) {
292
+ if (!import_typescript4.default.isCallExpression(expression) || !import_typescript4.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_typescript4.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_typescript4;
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_typescript4 = __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
  };
@@ -510,13 +533,18 @@ function methodFromFile(fileName) {
510
533
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
511
534
  return METHOD_FROM_FILE.get(stem);
512
535
  }
513
- function sharedFileIn(dir) {
536
+ function sharedFileIn(dir, cache) {
537
+ if (cache?.has(dir)) {
538
+ return cache.get(dir);
539
+ }
514
540
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
515
541
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
516
542
  if ((0, import_node_fs2.existsSync)(file)) {
543
+ cache?.set(dir, file);
517
544
  return file;
518
545
  }
519
546
  }
547
+ cache?.set(dir, void 0);
520
548
  return void 0;
521
549
  }
522
550
  function physicalRouteSegments(routesDir, routeDir) {
@@ -585,7 +613,7 @@ async function scanRouteFolders(routesDir) {
585
613
  function routeParamsForDir(routesDir, dir) {
586
614
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
587
615
  }
588
- function sharedFilesForDir(routesDir, dir) {
616
+ function sharedFilesForDir(routesDir, dir, cache) {
589
617
  const segments = physicalRouteSegments(routesDir, dir);
590
618
  const dirs = [routesDir];
591
619
  let current = routesDir;
@@ -593,7 +621,7 @@ function sharedFilesForDir(routesDir, dir) {
593
621
  current = (0, import_node_path2.join)(current, segment);
594
622
  dirs.push(current);
595
623
  }
596
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
624
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
597
625
  }
598
626
  async function scanRoutes(routesDir) {
599
627
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -605,6 +633,7 @@ async function scanRoutes(routesDir) {
605
633
  onlyFiles: true
606
634
  });
607
635
  const routes = [];
636
+ const sharedCache = /* @__PURE__ */ new Map();
608
637
  for (const file of files) {
609
638
  const method = methodFromFile((0, import_node_path2.basename)(file));
610
639
  if (!method) {
@@ -620,7 +649,7 @@ async function scanRoutes(routesDir) {
620
649
  routeDir,
621
650
  routeSegments,
622
651
  params,
623
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
652
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
624
653
  });
625
654
  }
626
655
  return routes.sort((left, right) => {
@@ -649,9 +678,11 @@ function isGiriBodySchema(value) {
649
678
  }
650
679
 
651
680
  // src/app.ts
652
- function loadModule(file) {
681
+ function loadModule(file, force = true) {
653
682
  const resolved = require.resolve(file);
654
- delete require.cache[resolved];
683
+ if (force) {
684
+ delete require.cache[resolved];
685
+ }
655
686
  return require(resolved);
656
687
  }
657
688
  function interopDefault(value) {
@@ -713,20 +744,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
713
744
  }
714
745
  function matchAlias(request, key) {
715
746
  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);
747
+ const [prefix, suffix = ""] = key.split("*");
748
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
749
+ return request.slice(prefix.length, request.length - suffix.length);
719
750
  }
720
751
  return void 0;
721
752
  }
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;
753
+ return request === key ? "" : void 0;
730
754
  }
731
755
  function resolveAliasRequest(request, alias, cwd) {
732
756
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -790,38 +814,78 @@ async function buildGiriApp(config, options = {}) {
790
814
  const routes = await scanRoutes(paths.routesDir);
791
815
  const app = config.adapter.createApp();
792
816
  ensureGiriAliasResolver(paths.outDir);
793
- const { unregister } = await safeRegister();
794
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
817
+ if (options.lazy && (!options.loaderRegistered || !options.aliasResolverRegistered)) {
818
+ throw new Error("Lazy route loading requires persistent loader and alias registrations.");
819
+ }
820
+ const loader = options.loaderRegistered ? void 0 : await safeRegister();
821
+ const unregisterAliasResolver = options.aliasResolverRegistered ? void 0 : registerAliasResolver(config.alias, paths.cwd);
795
822
  try {
796
- for (const route of routes) {
797
- const routeModule = loadModule(route.file);
823
+ const dirty = options.dirty;
824
+ const forceReload = dirty === void 0;
825
+ const isDirty = (file) => forceReload || dirty.has(file);
826
+ const sharedCache = /* @__PURE__ */ new Map();
827
+ const loadShared = (file) => {
828
+ if (!sharedCache.has(file)) {
829
+ sharedCache.set(file, loadModule(file, isDirty(file)));
830
+ }
831
+ return sharedCache.get(file);
832
+ };
833
+ const runtimeFor = (route) => {
834
+ const routeModule = loadModule(route.file, isDirty(route.file));
798
835
  if (typeof routeModule.handle !== "function") {
799
836
  throw new Error(`${route.file} must export a named handle function.`);
800
837
  }
801
838
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
802
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
839
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
803
840
  );
804
841
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
805
- config.adapter.register(app, {
842
+ return {
806
843
  method: route.method,
807
844
  path: route.path,
808
845
  handle: routeModule.handle,
809
846
  middleware: [...folderMiddleware, ...verbMiddleware],
810
847
  input: routeInput(routeModule, route.file),
811
- services: options.services
848
+ services: options.services,
849
+ cookieSecret: config.cookieSecret
850
+ };
851
+ };
852
+ for (const route of routes) {
853
+ if (!options.lazy) {
854
+ config.adapter.register(app, runtimeFor(route));
855
+ continue;
856
+ }
857
+ let runtime;
858
+ const getRuntime = () => {
859
+ runtime ??= runtimeFor(route);
860
+ return runtime;
861
+ };
862
+ config.adapter.register(app, {
863
+ method: route.method,
864
+ path: route.path,
865
+ get handle() {
866
+ return getRuntime().handle;
867
+ },
868
+ get middleware() {
869
+ return getRuntime().middleware;
870
+ },
871
+ get input() {
872
+ return getRuntime().input;
873
+ },
874
+ services: options.services,
875
+ cookieSecret: config.cookieSecret
812
876
  });
813
877
  }
814
878
  } finally {
815
- unregisterAliasResolver();
816
- unregister();
879
+ unregisterAliasResolver?.();
880
+ loader?.unregister();
817
881
  }
818
882
  return { app, routes, paths };
819
883
  }
820
884
 
821
885
  // 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");
886
+ var import_node_fs8 = require("fs");
887
+ var import_promises4 = require("fs/promises");
888
+ var import_node_path12 = require("path");
825
889
 
826
890
  // src/generator/app-types.ts
827
891
  var import_node_fs4 = require("fs");
@@ -1073,6 +1137,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1073
1137
  const documentPaths = {};
1074
1138
  const schemas = {};
1075
1139
  const securitySchemes = {};
1140
+ const tagOrder = [];
1076
1141
  for (const route of routes) {
1077
1142
  if (data.hiddenFiles?.has(route.file)) {
1078
1143
  continue;
@@ -1080,10 +1145,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1080
1145
  const responses = data.responsesByFile?.get(route.file);
1081
1146
  const input = data.inputsByFile?.get(route.file);
1082
1147
  const security = data.securityByFile?.get(route.file);
1148
+ const meta = data.openapiByFile?.get(route.file);
1083
1149
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1084
1150
  schemas[name] = rewriteRefs(schema);
1085
1151
  }
1086
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1152
+ const operation = {};
1153
+ if (meta?.tags && meta.tags.length > 0) {
1154
+ operation.tags = meta.tags;
1155
+ for (const tag2 of meta.tags) {
1156
+ if (!tagOrder.includes(tag2)) {
1157
+ tagOrder.push(tag2);
1158
+ }
1159
+ }
1160
+ }
1161
+ if (meta?.summary) {
1162
+ operation.summary = meta.summary;
1163
+ }
1164
+ if (meta?.description) {
1165
+ operation.description = meta.description;
1166
+ }
1167
+ if (meta?.operationId) {
1168
+ operation.operationId = meta.operationId;
1169
+ }
1170
+ if (meta?.deprecated) {
1171
+ operation.deprecated = true;
1172
+ }
1173
+ operation.responses = buildResponses(responses?.responses ?? []);
1087
1174
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1088
1175
  if (parameters.length > 0) {
1089
1176
  operation.parameters = parameters;
@@ -1115,6 +1202,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1115
1202
  info: readProjectInfo(paths.cwd),
1116
1203
  paths: documentPaths
1117
1204
  };
1205
+ if (tagOrder.length > 0) {
1206
+ document.tags = tagOrder.map((name) => ({ name }));
1207
+ }
1118
1208
  const components = {};
1119
1209
  if (Object.keys(schemas).length > 0) {
1120
1210
  components.schemas = schemas;
@@ -1146,14 +1236,29 @@ function paramsType(params) {
1146
1236
  ${fields}
1147
1237
  }`;
1148
1238
  }
1149
- function varsType(typesDir, sharedFiles) {
1150
- if (sharedFiles.length === 0) {
1151
- return "{}";
1239
+ function middlewareVarsType(typesDir, sharedFile) {
1240
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1241
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1242
+ }
1243
+ function ownSharedFile(dir, sharedFiles) {
1244
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1245
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1246
+ return sharedFiles[index];
1247
+ }
1152
1248
  }
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 & ");
1249
+ return void 0;
1250
+ }
1251
+ function varsType(paths, file, dir, sharedFiles) {
1252
+ const typesDir = (0, import_node_path8.dirname)(file);
1253
+ const parts = [];
1254
+ if (dir !== paths.routesDir) {
1255
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1256
+ }
1257
+ const ownShared = ownSharedFile(dir, sharedFiles);
1258
+ if (ownShared) {
1259
+ parts.push(middlewareVarsType(typesDir, ownShared));
1260
+ }
1261
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1157
1262
  }
1158
1263
  function methodExports(typesDir, verbs) {
1159
1264
  return verbs.map(({ method, file }) => {
@@ -1164,14 +1269,14 @@ function methodExports(typesDir, verbs) {
1164
1269
  });
1165
1270
  }
1166
1271
  async function writeParamTypes(paths, folders) {
1167
- for (const { dir, params, sharedFiles, verbs } of folders) {
1272
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1168
1273
  const file = typeFilePath(paths, dir);
1169
1274
  const typesDir = (0, import_node_path8.dirname)(file);
1170
1275
  const lines = [
1171
1276
  GENERATED_HEADER,
1172
1277
  `export type Params = ${paramsType(params)};`,
1173
1278
  "export type RouteParams = Params;",
1174
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1279
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1175
1280
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1176
1281
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1177
1282
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1181,10 +1286,14 @@ async function writeParamTypes(paths, folders) {
1181
1286
  lines.push(...methodExports(typesDir, verbs));
1182
1287
  }
1183
1288
  lines.push("");
1184
- await writeGenerated(file, lines.join("\n"));
1185
- }
1289
+ return writeGenerated(file, lines.join("\n"));
1290
+ }));
1186
1291
  }
1187
1292
 
1293
+ // src/generator/route-meta.ts
1294
+ var import_node_fs6 = require("fs");
1295
+ var import_typescript = __toESM(require("typescript"));
1296
+
1188
1297
  // src/generator/inputs.ts
1189
1298
  function sanitize(schema) {
1190
1299
  const { $schema, ...rest } = schema;
@@ -1245,28 +1354,315 @@ function readInput(routeModule) {
1245
1354
  }
1246
1355
  return input.body || input.query ? input : void 0;
1247
1356
  }
1248
- function hiddenFrom(value) {
1249
- if (value === false) {
1357
+ function hasExportModifier(node) {
1358
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
1359
+ }
1360
+ function unwrapExpression(expression) {
1361
+ let current = expression;
1362
+ while (import_typescript.default.isParenthesizedExpression(current) || import_typescript.default.isAsExpression(current) || import_typescript.default.isSatisfiesExpression(current)) {
1363
+ current = current.expression;
1364
+ }
1365
+ return current;
1366
+ }
1367
+ function staticBoolean(expression) {
1368
+ const value = unwrapExpression(expression);
1369
+ if (value.kind === import_typescript.default.SyntaxKind.TrueKeyword) {
1250
1370
  return true;
1251
1371
  }
1252
- if (value === true) {
1372
+ if (value.kind === import_typescript.default.SyntaxKind.FalseKeyword) {
1253
1373
  return false;
1254
1374
  }
1255
- if (value && typeof value === "object" && "hidden" in value) {
1256
- return Boolean(value.hidden);
1375
+ return void 0;
1376
+ }
1377
+ function staticString(expression) {
1378
+ const value = unwrapExpression(expression);
1379
+ return import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
1380
+ }
1381
+ function staticStringArray(expression) {
1382
+ const value = unwrapExpression(expression);
1383
+ if (!import_typescript.default.isArrayLiteralExpression(value)) {
1384
+ return void 0;
1385
+ }
1386
+ const strings = [];
1387
+ for (const element of value.elements) {
1388
+ const string = staticString(element);
1389
+ if (string === void 0) {
1390
+ return void 0;
1391
+ }
1392
+ strings.push(string);
1393
+ }
1394
+ return strings;
1395
+ }
1396
+ function propertyName(name) {
1397
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
1398
+ return name.text;
1257
1399
  }
1258
1400
  return void 0;
1259
1401
  }
1260
- function collectHidden(route, routeModule, loadShared) {
1402
+ function collectImportedNames(source) {
1403
+ const names = /* @__PURE__ */ new Set();
1404
+ for (const statement of source.statements) {
1405
+ if (!import_typescript.default.isImportDeclaration(statement)) {
1406
+ continue;
1407
+ }
1408
+ const clause = statement.importClause;
1409
+ if (!clause) {
1410
+ continue;
1411
+ }
1412
+ if (clause.name) {
1413
+ names.add(clause.name.text);
1414
+ }
1415
+ const bindings = clause.namedBindings;
1416
+ if (bindings && import_typescript.default.isNamespaceImport(bindings)) {
1417
+ names.add(bindings.name.text);
1418
+ }
1419
+ if (bindings && import_typescript.default.isNamedImports(bindings)) {
1420
+ for (const element of bindings.elements) {
1421
+ names.add(element.name.text);
1422
+ }
1423
+ }
1424
+ }
1425
+ return names;
1426
+ }
1427
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1428
+ let found = false;
1429
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1430
+ const visit = (node) => {
1431
+ if (found) {
1432
+ return;
1433
+ }
1434
+ if (import_typescript.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1435
+ found = true;
1436
+ return;
1437
+ }
1438
+ import_typescript.default.forEachChild(node, visit);
1439
+ };
1440
+ visit(expression);
1441
+ return found;
1442
+ }
1443
+ function parseStaticOpenApi(expression) {
1444
+ const value = unwrapExpression(expression);
1445
+ const boolean = staticBoolean(value);
1446
+ if (boolean !== void 0) {
1447
+ return boolean;
1448
+ }
1449
+ if (!import_typescript.default.isObjectLiteralExpression(value)) {
1450
+ return void 0;
1451
+ }
1452
+ const openapi = {};
1453
+ for (const property of value.properties) {
1454
+ if (!import_typescript.default.isPropertyAssignment(property)) {
1455
+ return void 0;
1456
+ }
1457
+ const name = propertyName(property.name);
1458
+ if (!name) {
1459
+ return void 0;
1460
+ }
1461
+ if (name === "hidden") {
1462
+ const hidden = staticBoolean(property.initializer);
1463
+ if (hidden === void 0) {
1464
+ return void 0;
1465
+ }
1466
+ openapi.hidden = hidden;
1467
+ } else if (name === "tags") {
1468
+ const tags = staticStringArray(property.initializer);
1469
+ if (!tags) {
1470
+ return void 0;
1471
+ }
1472
+ openapi.tags = tags;
1473
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1474
+ const string = staticString(property.initializer);
1475
+ if (string === void 0) {
1476
+ return void 0;
1477
+ }
1478
+ openapi[name] = string;
1479
+ } else if (name === "deprecated") {
1480
+ const deprecated = staticBoolean(property.initializer);
1481
+ if (deprecated === void 0) {
1482
+ return void 0;
1483
+ }
1484
+ openapi.deprecated = deprecated;
1485
+ } else {
1486
+ return void 0;
1487
+ }
1488
+ }
1489
+ return openapi;
1490
+ }
1491
+ function readStaticModuleMeta(file) {
1492
+ let source;
1493
+ try {
1494
+ source = import_typescript.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript.default.ScriptTarget.Latest, true);
1495
+ } catch {
1496
+ return void 0;
1497
+ }
1498
+ const importedNames = collectImportedNames(source);
1499
+ const sourceText = source.getFullText();
1500
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1501
+ const meta = { middlewareSecurity: false };
1502
+ for (const statement of source.statements) {
1503
+ if (import_typescript.default.isImportDeclaration(statement) || import_typescript.default.isInterfaceDeclaration(statement) || import_typescript.default.isTypeAliasDeclaration(statement) || import_typescript.default.isEmptyStatement(statement) || !hasExportModifier(statement)) {
1504
+ continue;
1505
+ }
1506
+ if (import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1507
+ continue;
1508
+ }
1509
+ if (import_typescript.default.isVariableStatement(statement)) {
1510
+ for (const declaration of statement.declarationList.declarations) {
1511
+ if (!import_typescript.default.isIdentifier(declaration.name)) {
1512
+ return void 0;
1513
+ }
1514
+ const name = declaration.name.text;
1515
+ if (name === "openapi") {
1516
+ if (!declaration.initializer) {
1517
+ return void 0;
1518
+ }
1519
+ const openapi = parseStaticOpenApi(declaration.initializer);
1520
+ if (openapi === void 0) {
1521
+ return void 0;
1522
+ }
1523
+ meta.openapi = openapi;
1524
+ } else if (name === "handle") {
1525
+ if (!declaration.initializer || !import_typescript.default.isArrowFunction(declaration.initializer) && !import_typescript.default.isFunctionExpression(declaration.initializer)) {
1526
+ return void 0;
1527
+ }
1528
+ continue;
1529
+ } else if (name === "middleware") {
1530
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1531
+ return void 0;
1532
+ }
1533
+ meta.middlewareSecurity = false;
1534
+ continue;
1535
+ } else {
1536
+ return void 0;
1537
+ }
1538
+ }
1539
+ continue;
1540
+ }
1541
+ return void 0;
1542
+ }
1543
+ return meta;
1544
+ }
1545
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1261
1546
  let hidden = false;
1547
+ const tags = [];
1548
+ const meta = {};
1549
+ const apply = (value, isVerb) => {
1550
+ if (value === false) {
1551
+ hidden = true;
1552
+ return;
1553
+ }
1554
+ if (value === true) {
1555
+ hidden = false;
1556
+ return;
1557
+ }
1558
+ if (!value) {
1559
+ return;
1560
+ }
1561
+ if (typeof value.hidden === "boolean") {
1562
+ hidden = value.hidden;
1563
+ }
1564
+ if (value.tags) {
1565
+ tags.push(...value.tags);
1566
+ }
1567
+ if (typeof value.summary === "string") {
1568
+ meta.summary = value.summary;
1569
+ }
1570
+ if (typeof value.description === "string") {
1571
+ meta.description = value.description;
1572
+ }
1573
+ if (typeof value.deprecated === "boolean") {
1574
+ meta.deprecated = value.deprecated;
1575
+ }
1576
+ if (isVerb && typeof value.operationId === "string") {
1577
+ meta.operationId = value.operationId;
1578
+ }
1579
+ };
1262
1580
  for (const file of route.sharedFiles) {
1263
- const opinion = hiddenFrom(loadShared(file).openapi);
1264
- if (opinion !== void 0) {
1265
- hidden = opinion;
1581
+ const shared = loadShared(file);
1582
+ if (!shared) {
1583
+ return void 0;
1584
+ }
1585
+ apply(shared.openapi, false);
1586
+ }
1587
+ apply(routeModule.openapi, true);
1588
+ if (tags.length > 0) {
1589
+ meta.tags = [...new Set(tags)];
1590
+ }
1591
+ return { hidden, meta };
1592
+ }
1593
+ function extractStaticMeta(route, routeModule, loadShared) {
1594
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1595
+ if (!openapi) {
1596
+ return void 0;
1597
+ }
1598
+ const meta = {};
1599
+ if (openapi.hidden) {
1600
+ meta.hidden = true;
1601
+ }
1602
+ if (Object.keys(openapi.meta).length > 0) {
1603
+ meta.openapi = openapi.meta;
1604
+ }
1605
+ return meta;
1606
+ }
1607
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
1608
+ const meta = {};
1609
+ const security = collectSecurity(route, {}, loadShared);
1610
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
1611
+ if (security) {
1612
+ meta.security = security;
1613
+ }
1614
+ if (hidden) {
1615
+ meta.hidden = true;
1616
+ }
1617
+ if (Object.keys(openapi).length > 0) {
1618
+ meta.openapi = openapi;
1619
+ }
1620
+ return meta;
1621
+ }
1622
+ function resolveOpenApi(route, routeModule, loadShared) {
1623
+ let hidden = false;
1624
+ const tags = [];
1625
+ const meta = {};
1626
+ const apply = (value, isVerb) => {
1627
+ if (value === false) {
1628
+ hidden = true;
1629
+ return;
1266
1630
  }
1631
+ if (value === true) {
1632
+ hidden = false;
1633
+ return;
1634
+ }
1635
+ if (!value || typeof value !== "object") {
1636
+ return;
1637
+ }
1638
+ const o = value;
1639
+ if ("hidden" in o) {
1640
+ hidden = Boolean(o.hidden);
1641
+ }
1642
+ if (Array.isArray(o.tags)) {
1643
+ tags.push(...o.tags.filter((tag2) => typeof tag2 === "string"));
1644
+ }
1645
+ if (typeof o.summary === "string") {
1646
+ meta.summary = o.summary;
1647
+ }
1648
+ if (typeof o.description === "string") {
1649
+ meta.description = o.description;
1650
+ }
1651
+ if (typeof o.deprecated === "boolean") {
1652
+ meta.deprecated = o.deprecated;
1653
+ }
1654
+ if (isVerb && typeof o.operationId === "string") {
1655
+ meta.operationId = o.operationId;
1656
+ }
1657
+ };
1658
+ for (const file of route.sharedFiles) {
1659
+ apply(loadShared(file).openapi, false);
1660
+ }
1661
+ apply(routeModule.openapi, true);
1662
+ if (tags.length > 0) {
1663
+ meta.tags = [...new Set(tags)];
1267
1664
  }
1268
- const verb = hiddenFrom(routeModule.openapi);
1269
- return verb ?? hidden;
1665
+ return { hidden, meta };
1270
1666
  }
1271
1667
  function collectSecurity(route, routeModule, loadShared) {
1272
1668
  const skipInherited = Boolean(
@@ -1298,6 +1694,33 @@ function collectSecurity(route, routeModule, loadShared) {
1298
1694
  }
1299
1695
  async function extractRouteMeta(config, paths, routes) {
1300
1696
  const byFile = /* @__PURE__ */ new Map();
1697
+ const remainingRoutes = [];
1698
+ const runtimeSharedRoutes = [];
1699
+ const staticCache = /* @__PURE__ */ new Map();
1700
+ const loadStatic = (file) => {
1701
+ if (!staticCache.has(file)) {
1702
+ staticCache.set(file, readStaticModuleMeta(file));
1703
+ }
1704
+ return staticCache.get(file);
1705
+ };
1706
+ for (const route of routes) {
1707
+ const routeModule = loadStatic(route.file);
1708
+ if (!routeModule) {
1709
+ remainingRoutes.push(route);
1710
+ continue;
1711
+ }
1712
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
1713
+ if (meta) {
1714
+ if (meta.hidden || meta.openapi) {
1715
+ byFile.set(route.file, meta);
1716
+ }
1717
+ continue;
1718
+ }
1719
+ runtimeSharedRoutes.push({ route, routeModule });
1720
+ }
1721
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
1722
+ return byFile;
1723
+ }
1301
1724
  const { unregister } = await safeRegister();
1302
1725
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1303
1726
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1312,13 +1735,19 @@ async function extractRouteMeta(config, paths, routes) {
1312
1735
  return sharedCache.get(file);
1313
1736
  };
1314
1737
  try {
1315
- for (const route of routes) {
1738
+ for (const { route, routeModule } of runtimeSharedRoutes) {
1739
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
1740
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1741
+ byFile.set(route.file, meta);
1742
+ }
1743
+ }
1744
+ for (const route of remainingRoutes) {
1316
1745
  try {
1317
1746
  const routeModule = loadModule2(route.file);
1318
1747
  const meta = {};
1319
1748
  const input = readInput(routeModule);
1320
1749
  const security = collectSecurity(route, routeModule, loadShared);
1321
- const hidden = collectHidden(route, routeModule, loadShared);
1750
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1322
1751
  if (input) {
1323
1752
  meta.input = input;
1324
1753
  }
@@ -1328,7 +1757,10 @@ async function extractRouteMeta(config, paths, routes) {
1328
1757
  if (hidden) {
1329
1758
  meta.hidden = true;
1330
1759
  }
1331
- if (meta.input || meta.security || meta.hidden) {
1760
+ if (Object.keys(openapi).length > 0) {
1761
+ meta.openapi = openapi;
1762
+ }
1763
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1332
1764
  byFile.set(route.file, meta);
1333
1765
  }
1334
1766
  } catch {
@@ -1400,6 +1832,103 @@ async function writeTsConfig(paths, config) {
1400
1832
  });
1401
1833
  }
1402
1834
 
1835
+ // src/generator/cache.ts
1836
+ var import_node_crypto = require("crypto");
1837
+ var import_node_fs7 = require("fs");
1838
+ var import_promises3 = require("fs/promises");
1839
+ var import_node_path11 = require("path");
1840
+ var import_tinyglobby2 = require("tinyglobby");
1841
+ var CACHE_VERSION = 1;
1842
+ var SYNC_CACHE_NAME = ".sync-cache.json";
1843
+ function stableConfig(config) {
1844
+ const alias = Object.entries(config.alias ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]);
1845
+ return { alias, outDir: config.outDir ?? ".giri" };
1846
+ }
1847
+ async function syncFingerprint(config, paths) {
1848
+ const outRelative = slash((0, import_node_path11.relative)(paths.cwd, paths.outDir));
1849
+ const ignore = ["**/node_modules/**", "**/.git/**"];
1850
+ if (outRelative && !outRelative.startsWith("..")) {
1851
+ ignore.push(`${outRelative}/**`);
1852
+ }
1853
+ const files = await (0, import_tinyglobby2.glob)([
1854
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}",
1855
+ "giri.config.{ts,js,mts,cts,mjs,cjs}",
1856
+ "tsconfig*.json",
1857
+ "package.json",
1858
+ "package-lock.json",
1859
+ "npm-shrinkwrap.json",
1860
+ "yarn.lock",
1861
+ "pnpm-lock.yaml",
1862
+ "bun.lock",
1863
+ "bun.lockb"
1864
+ ], {
1865
+ cwd: paths.cwd,
1866
+ absolute: false,
1867
+ onlyFiles: true,
1868
+ dot: true,
1869
+ ignore
1870
+ });
1871
+ const hash = (0, import_node_crypto.createHash)("sha256");
1872
+ hash.update(JSON.stringify(stableConfig(config)));
1873
+ for (const file of files.sort()) {
1874
+ hash.update("\0");
1875
+ hash.update(slash(file));
1876
+ hash.update("\0");
1877
+ hash.update(await (0, import_promises3.readFile)((0, import_node_path11.resolve)(paths.cwd, file)));
1878
+ }
1879
+ return hash.digest("hex");
1880
+ }
1881
+ function cachePath(paths) {
1882
+ return (0, import_node_path11.join)(paths.outDir, SYNC_CACHE_NAME);
1883
+ }
1884
+ function serializePath(paths, file) {
1885
+ return slash((0, import_node_path11.relative)(paths.cwd, file));
1886
+ }
1887
+ function deserializePath(paths, file) {
1888
+ return slash((0, import_node_path11.resolve)(paths.cwd, file.split("/").join(import_node_path11.sep)));
1889
+ }
1890
+ function serializeMap(paths, values) {
1891
+ return [...values].map(([file, value]) => [serializePath(paths, file), value]);
1892
+ }
1893
+ function deserializeMap(paths, values) {
1894
+ return new Map(values.map(([file, value]) => [deserializePath(paths, file), value]));
1895
+ }
1896
+ async function readSyncCache(paths, fingerprint) {
1897
+ const file = cachePath(paths);
1898
+ if (!(0, import_node_fs7.existsSync)(file)) {
1899
+ return void 0;
1900
+ }
1901
+ try {
1902
+ const cache = JSON.parse(await (0, import_promises3.readFile)(file, "utf8"));
1903
+ if (cache.version !== CACHE_VERSION || cache.fingerprint !== fingerprint) {
1904
+ return void 0;
1905
+ }
1906
+ return {
1907
+ responsesByFile: deserializeMap(paths, cache.data.responsesByFile),
1908
+ inputsByFile: deserializeMap(paths, cache.data.inputsByFile),
1909
+ securityByFile: deserializeMap(paths, cache.data.securityByFile),
1910
+ hiddenFiles: new Set(cache.data.hiddenFiles.map((entry) => deserializePath(paths, entry))),
1911
+ openapiByFile: deserializeMap(paths, cache.data.openapiByFile)
1912
+ };
1913
+ } catch {
1914
+ return void 0;
1915
+ }
1916
+ }
1917
+ async function writeSyncCache(paths, fingerprint, data) {
1918
+ const cache = {
1919
+ version: CACHE_VERSION,
1920
+ fingerprint,
1921
+ data: {
1922
+ responsesByFile: serializeMap(paths, data.responsesByFile),
1923
+ inputsByFile: serializeMap(paths, data.inputsByFile),
1924
+ securityByFile: serializeMap(paths, data.securityByFile),
1925
+ hiddenFiles: [...data.hiddenFiles].map((file) => serializePath(paths, file)),
1926
+ openapiByFile: serializeMap(paths, data.openapiByFile)
1927
+ }
1928
+ };
1929
+ await writeJson(cachePath(paths), cache);
1930
+ }
1931
+
1403
1932
  // src/generator/sync.ts
1404
1933
  async function typeFolders(paths, routes) {
1405
1934
  const verbsByDir = /* @__PURE__ */ new Map();
@@ -1410,10 +1939,11 @@ async function typeFolders(paths, routes) {
1410
1939
  verbsByDir.set(key, list);
1411
1940
  }
1412
1941
  const dirs = await scanRouteFolders(paths.routesDir);
1942
+ const sharedCache = /* @__PURE__ */ new Map();
1413
1943
  return dirs.map((dir) => ({
1414
1944
  dir,
1415
1945
  params: routeParamsForDir(paths.routesDir, dir),
1416
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
1946
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1417
1947
  verbs: verbsByDir.get(slash(dir)) ?? []
1418
1948
  }));
1419
1949
  }
@@ -1425,11 +1955,9 @@ async function extractResponses(paths, routes) {
1425
1955
  try {
1426
1956
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1427
1957
  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
- );
1958
+ const appTypes = (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts");
1959
+ const roots = (0, import_node_fs8.existsSync)(appTypes) ? [...files, appTypes] : files;
1960
+ const program = createSchemaProgram2(paths, roots);
1433
1961
  for (const file of files) {
1434
1962
  byFile.set(file, extractRouteResponses2(program, file));
1435
1963
  }
@@ -1442,8 +1970,9 @@ async function extractMeta(config, paths, routes) {
1442
1970
  const inputsByFile = /* @__PURE__ */ new Map();
1443
1971
  const securityByFile = /* @__PURE__ */ new Map();
1444
1972
  const hiddenFiles = /* @__PURE__ */ new Set();
1973
+ const openapiByFile = /* @__PURE__ */ new Map();
1445
1974
  if (routes.length === 0) {
1446
- return { inputsByFile, securityByFile, hiddenFiles };
1975
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1447
1976
  }
1448
1977
  try {
1449
1978
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1457,79 +1986,180 @@ async function extractMeta(config, paths, routes) {
1457
1986
  if (entry.hidden) {
1458
1987
  hiddenFiles.add(file);
1459
1988
  }
1989
+ if (entry.openapi) {
1990
+ openapiByFile.set(file, entry.openapi);
1991
+ }
1460
1992
  }
1461
1993
  } catch (error) {
1462
1994
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1463
1995
  }
1464
- return { inputsByFile, securityByFile, hiddenFiles };
1996
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1465
1997
  }
1466
1998
  async function syncProject(config, options = {}) {
1467
1999
  const paths = resolveGiriPaths(config, options.cwd);
1468
2000
  assertSafeOutDir(paths);
2001
+ const hadOutDir = (0, import_node_fs8.existsSync)(paths.outDir);
1469
2002
  const routes = await scanRoutes(paths.routesDir);
1470
2003
  const folders = await typeFolders(paths, routes);
1471
- await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
2004
+ const fingerprint = await syncFingerprint(config, paths);
2005
+ const cached = await readSyncCache(paths, fingerprint);
2006
+ const generatedFiles = [
2007
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2008
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2009
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2010
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2011
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2012
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2013
+ ];
2014
+ if (cached && generatedFiles.every(import_node_fs8.existsSync)) {
2015
+ return { paths, routes, folders, data: cached };
2016
+ }
2017
+ await (0, import_promises4.mkdir)(paths.outDir, { recursive: true });
1472
2018
  await writeParamTypes(paths, folders);
1473
2019
  await writeRouteTypes(paths, routes);
1474
2020
  await writeAppTypes(paths);
1475
2021
  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 };
2022
+ const data = cached ?? {
2023
+ responsesByFile: await extractResponses(paths, routes),
2024
+ ...await extractMeta(config, paths, routes)
2025
+ };
1479
2026
  await writeManifest(paths, routes, data);
1480
2027
  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
- );
2028
+ await writeSyncCache(paths, fingerprint, data);
2029
+ if (hadOutDir) {
2030
+ await pruneDir(
2031
+ paths.outDir,
2032
+ /* @__PURE__ */ new Set([
2033
+ (0, import_node_path12.join)(paths.outDir, "tsconfig.json"),
2034
+ (0, import_node_path12.join)(paths.outDir, "manifest.json"),
2035
+ (0, import_node_path12.join)(paths.outDir, "openapi.json"),
2036
+ (0, import_node_path12.join)(paths.outDir, "routes.d.ts"),
2037
+ (0, import_node_path12.join)(paths.outDir, SYNC_CACHE_NAME),
2038
+ (0, import_node_path12.join)(paths.outDir, "types", "app.d.ts"),
2039
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
2040
+ ])
2041
+ );
2042
+ }
1492
2043
  return { paths, routes, folders, data };
1493
2044
  }
1494
2045
 
1495
2046
  // src/generator/watch.ts
1496
- var import_node_fs7 = require("fs");
1497
- var import_node_path13 = require("path");
2047
+ var import_node_fs10 = require("fs");
2048
+ var import_node_path15 = require("path");
1498
2049
 
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;
2050
+ // src/loader/import-graph.ts
2051
+ var import_node_fs9 = require("fs");
2052
+ var import_node_path13 = require("path");
2053
+ var import_typescript5 = __toESM(require("typescript"));
2054
+ var import_tinyglobby3 = require("tinyglobby");
2055
+ var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
2056
+ var JS_EXT = /\.(?:c|m)?jsx?$/;
2057
+ var toSlash = (path) => path.split(import_node_path13.sep).join("/");
2058
+ function probeFile(base) {
2059
+ if ((0, import_node_fs9.existsSync)(base) && (0, import_node_fs9.statSync)(base).isFile()) {
2060
+ return base;
2061
+ }
2062
+ for (const ext of RESOLVE_EXTS) {
2063
+ const candidate = base + ext;
2064
+ if ((0, import_node_fs9.existsSync)(candidate)) {
2065
+ return candidate;
2066
+ }
2067
+ }
2068
+ for (const ext of RESOLVE_EXTS) {
2069
+ const candidate = (0, import_node_path13.join)(base, `index${ext}`);
2070
+ if ((0, import_node_fs9.existsSync)(candidate)) {
2071
+ return candidate;
2072
+ }
2073
+ }
2074
+ return void 0;
2075
+ }
2076
+ function resolveSpecifier(specifier, fromFile, alias, cwd) {
2077
+ let target;
2078
+ if (specifier.startsWith(".")) {
2079
+ target = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(fromFile), specifier);
2080
+ } else {
2081
+ const aliased = resolveAliasRequest(specifier, alias, cwd);
2082
+ if (aliased === void 0) {
2083
+ return void 0;
2084
+ }
2085
+ target = aliased;
2086
+ }
2087
+ const resolved = probeFile(target);
2088
+ if (resolved) {
2089
+ return resolved;
2090
+ }
2091
+ if (JS_EXT.test(target)) {
2092
+ return probeFile(target.replace(JS_EXT, ""));
2093
+ }
2094
+ return void 0;
2095
+ }
2096
+ function importSpecifiers(file) {
2097
+ let source;
2098
+ try {
2099
+ source = import_typescript5.default.createSourceFile(file, (0, import_node_fs9.readFileSync)(file, "utf8"), import_typescript5.default.ScriptTarget.Latest, false);
2100
+ } catch {
2101
+ return [];
2102
+ }
2103
+ const specifiers = [];
2104
+ const visit = (node) => {
2105
+ if ((import_typescript5.default.isImportDeclaration(node) || import_typescript5.default.isExportDeclaration(node)) && node.moduleSpecifier && import_typescript5.default.isStringLiteral(node.moduleSpecifier)) {
2106
+ specifiers.push(node.moduleSpecifier.text);
2107
+ } else if (import_typescript5.default.isImportEqualsDeclaration(node) && import_typescript5.default.isExternalModuleReference(node.moduleReference) && import_typescript5.default.isStringLiteralLike(node.moduleReference.expression)) {
2108
+ specifiers.push(node.moduleReference.expression.text);
2109
+ } else if (import_typescript5.default.isCallExpression(node)) {
2110
+ const isRequire = import_typescript5.default.isIdentifier(node.expression) && node.expression.text === "require";
2111
+ const isDynamicImport = node.expression.kind === import_typescript5.default.SyntaxKind.ImportKeyword;
2112
+ const [first] = node.arguments;
2113
+ if ((isRequire || isDynamicImport) && first && import_typescript5.default.isStringLiteralLike(first)) {
2114
+ specifiers.push(first.text);
2115
+ }
2116
+ }
2117
+ import_typescript5.default.forEachChild(node, visit);
2118
+ };
2119
+ visit(source);
2120
+ return specifiers;
2121
+ }
2122
+ async function buildImportGraph(config, cwd) {
2123
+ const root = (0, import_node_path13.resolve)(cwd);
2124
+ const outDir = (0, import_node_path13.resolve)(root, config.outDir ?? ".giri") + import_node_path13.sep;
2125
+ const outRel = (0, import_node_path13.relative)(root, outDir).split(import_node_path13.sep).join("/").replace(/\/$/, "");
2126
+ const files = await (0, import_tinyglobby3.glob)("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", {
2127
+ cwd: root,
2128
+ absolute: true,
2129
+ onlyFiles: true,
2130
+ ignore: ["**/node_modules/**", "**/.git/**", outRel ? `${outRel}/**` : ".giri/**"]
2131
+ });
1507
2132
  const importers = /* @__PURE__ */ new Map();
1508
2133
  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) {
2134
+ for (const file of files) {
2135
+ if (file.startsWith(outDir)) {
1515
2136
  continue;
1516
2137
  }
1517
- nodes.add(toSlash(id));
1518
- for (const child of mod.children) {
1519
- if (!isProjectModule(child.id, root)) {
2138
+ for (const specifier of importSpecifiers(file)) {
2139
+ const dep = resolveSpecifier(specifier, file, config.alias, root);
2140
+ if (!dep || dep.startsWith(outDir)) {
1520
2141
  continue;
1521
2142
  }
1522
- nodes.add(toSlash(child.id));
1523
- const dep = toSlash(child.id);
1524
- let set = importers.get(dep);
2143
+ const from = toSlash(file);
2144
+ const to = toSlash(dep);
2145
+ nodes.add(from);
2146
+ nodes.add(to);
2147
+ let set = importers.get(to);
1525
2148
  if (!set) {
1526
2149
  set = /* @__PURE__ */ new Set();
1527
- importers.set(dep, set);
2150
+ importers.set(to, set);
1528
2151
  }
1529
- set.add(toSlash(id));
2152
+ set.add(from);
1530
2153
  }
1531
2154
  }
1532
2155
  return { importers, nodes };
2156
+ }
2157
+
2158
+ // src/loader/module-loader.ts
2159
+ var import_node_path14 = require("path");
2160
+ var toSlash2 = (path) => path.split(import_node_path14.sep).join("/");
2161
+ var isProjectModule = (id, root) => {
2162
+ 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
2163
  };
1534
2164
  var collectDependents = (graph, start) => {
1535
2165
  const out = /* @__PURE__ */ new Set([start]);
@@ -1547,19 +2177,27 @@ var collectDependents = (graph, start) => {
1547
2177
  };
1548
2178
  var purgeModules = (files) => {
1549
2179
  for (const id of Object.keys(require.cache)) {
1550
- if (files.has(toSlash(id))) {
2180
+ if (files.has(toSlash2(id))) {
1551
2181
  delete require.cache[id];
1552
2182
  }
1553
2183
  }
1554
2184
  };
1555
2185
  var purgeProjectModules = (cwd) => {
1556
- const root = (0, import_node_path12.resolve)(cwd) + import_node_path12.sep;
2186
+ const root = (0, import_node_path14.resolve)(cwd) + import_node_path14.sep;
1557
2187
  for (const id of Object.keys(require.cache)) {
1558
2188
  if (isProjectModule(id, root)) {
1559
2189
  delete require.cache[id];
1560
2190
  }
1561
2191
  }
1562
2192
  };
2193
+ var purgeGeneratedModules = (outDir) => {
2194
+ const root = (0, import_node_path14.resolve)(outDir) + import_node_path14.sep;
2195
+ for (const id of Object.keys(require.cache)) {
2196
+ if ((0, import_node_path14.resolve)(id).startsWith(root)) {
2197
+ delete require.cache[id];
2198
+ }
2199
+ }
2200
+ };
1563
2201
 
1564
2202
  // src/generator/watch.ts
1565
2203
  function createWatchUpdater(config, initial) {
@@ -1574,14 +2212,16 @@ function createWatchUpdater(config, initial) {
1574
2212
  data.inputsByFile = result.data.inputsByFile;
1575
2213
  data.securityByFile = result.data.securityByFile;
1576
2214
  data.hiddenFiles = result.data.hiddenFiles;
2215
+ data.openapiByFile = result.data.openapiByFile;
2216
+ purgeGeneratedModules(paths.outDir);
1577
2217
  return "full";
1578
2218
  };
1579
2219
  const reextractRoute = async (route) => {
1580
2220
  const key = route.file;
1581
2221
  try {
1582
2222
  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]);
2223
+ const appTypes = (0, import_node_path15.join)(paths.outDir, "types", "app.d.ts");
2224
+ const program = createSchemaProgram2(paths, (0, import_node_fs10.existsSync)(appTypes) ? [key, appTypes] : [key]);
1585
2225
  data.responsesByFile.set(key, extractRouteResponses2(program, key));
1586
2226
  } catch {
1587
2227
  }
@@ -1591,6 +2231,7 @@ function createWatchUpdater(config, initial) {
1591
2231
  data.inputsByFile.delete(key);
1592
2232
  data.securityByFile.delete(key);
1593
2233
  data.hiddenFiles.delete(key);
2234
+ data.openapiByFile.delete(key);
1594
2235
  if (entry?.input) {
1595
2236
  data.inputsByFile.set(key, entry.input);
1596
2237
  }
@@ -1600,6 +2241,9 @@ function createWatchUpdater(config, initial) {
1600
2241
  if (entry?.hidden) {
1601
2242
  data.hiddenFiles.add(key);
1602
2243
  }
2244
+ if (entry?.openapi) {
2245
+ data.openapiByFile.set(key, entry.openapi);
2246
+ }
1603
2247
  } catch {
1604
2248
  }
1605
2249
  };
@@ -1608,12 +2252,15 @@ function createWatchUpdater(config, initial) {
1608
2252
  if (!filename) {
1609
2253
  return fullResync();
1610
2254
  }
1611
- const abs = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(paths.routesDir), filename);
2255
+ const abs = (0, import_node_path15.resolve)((0, import_node_path15.dirname)(paths.routesDir), filename);
1612
2256
  const file = slash(abs);
1613
- if (!(0, import_node_fs7.existsSync)(abs)) {
2257
+ if (!(0, import_node_fs10.existsSync)(abs)) {
1614
2258
  return fullResync();
1615
2259
  }
1616
- const graph = buildModuleGraph(paths.cwd);
2260
+ if ((0, import_node_fs10.statSync)(abs).isDirectory()) {
2261
+ return "skip";
2262
+ }
2263
+ const graph = await buildImportGraph(config, paths.cwd);
1617
2264
  const isRoute = routes.some((candidate) => slash(candidate.file) === file);
1618
2265
  if (!graph.nodes.has(file) && !isRoute) {
1619
2266
  return fullResync();
@@ -1628,26 +2275,28 @@ function createWatchUpdater(config, initial) {
1628
2275
  }
1629
2276
  await writeManifest(paths, routes, data);
1630
2277
  await writeOpenApi(paths, routes, data);
2278
+ await writeSyncCache(paths, await syncFingerprint(config, paths), data);
2279
+ purgeGeneratedModules(paths.outDir);
1631
2280
  return "incremental";
1632
2281
  }
1633
2282
  };
1634
2283
  }
1635
2284
 
1636
2285
  // src/lifecycle.ts
1637
- var import_node_fs8 = require("fs");
1638
- var import_node_path14 = require("path");
2286
+ var import_node_fs11 = require("fs");
2287
+ var import_node_path16 = require("path");
1639
2288
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1640
2289
  function resolveMainFile(cwd) {
1641
2290
  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)) {
2291
+ const file = (0, import_node_path16.join)(cwd, "src", `main.${ext}`);
2292
+ if ((0, import_node_fs11.existsSync)(file)) {
1644
2293
  return file;
1645
2294
  }
1646
2295
  }
1647
2296
  return void 0;
1648
2297
  }
1649
2298
  async function loadLifecycle(cwd = process.cwd()) {
1650
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2299
+ const file = resolveMainFile((0, import_node_path16.resolve)(cwd));
1651
2300
  if (!file) {
1652
2301
  return {};
1653
2302
  }
@@ -1805,25 +2454,25 @@ function parseFlags(args) {
1805
2454
  return flags;
1806
2455
  }
1807
2456
  async function ensureGitignore(cwd) {
1808
- const file = (0, import_node_path15.join)(cwd, ".gitignore");
2457
+ const file = (0, import_node_path17.join)(cwd, ".gitignore");
1809
2458
  const entry = ".giri";
1810
- if (!(0, import_node_fs9.existsSync)(file)) {
1811
- await (0, import_promises4.writeFile)(file, `${entry}
2459
+ if (!(0, import_node_fs12.existsSync)(file)) {
2460
+ await (0, import_promises5.writeFile)(file, `${entry}
1812
2461
  `);
1813
2462
  return;
1814
2463
  }
1815
- const content = await (0, import_promises4.readFile)(file, "utf8");
2464
+ const content = await (0, import_promises5.readFile)(file, "utf8");
1816
2465
  if (!content.split(/\r?\n/).includes(entry)) {
1817
- await (0, import_promises4.appendFile)(file, `${content.endsWith("\n") ? "" : "\n"}${entry}
2466
+ await (0, import_promises5.appendFile)(file, `${content.endsWith("\n") ? "" : "\n"}${entry}
1818
2467
  `);
1819
2468
  }
1820
2469
  }
1821
2470
  async function ensureTsConfig(cwd) {
1822
- const file = (0, import_node_path15.join)(cwd, "tsconfig.json");
1823
- if ((0, import_node_fs9.existsSync)(file)) {
2471
+ const file = (0, import_node_path17.join)(cwd, "tsconfig.json");
2472
+ if ((0, import_node_fs12.existsSync)(file)) {
1824
2473
  return;
1825
2474
  }
1826
- await (0, import_promises4.writeFile)(
2475
+ await (0, import_promises5.writeFile)(
1827
2476
  file,
1828
2477
  `${JSON.stringify(
1829
2478
  {
@@ -1849,7 +2498,7 @@ async function ensureTsConfig(cwd) {
1849
2498
  async function missingDeps(cwd, candidates) {
1850
2499
  let pkg = {};
1851
2500
  try {
1852
- pkg = JSON.parse(await (0, import_promises4.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8"));
2501
+ pkg = JSON.parse(await (0, import_promises5.readFile)((0, import_node_path17.join)(cwd, "package.json"), "utf8"));
1853
2502
  } catch {
1854
2503
  }
1855
2504
  const present = /* @__PURE__ */ new Set();
@@ -1918,7 +2567,7 @@ async function selectAdapter(interactive) {
1918
2567
  return ADAPTERS.find((adapter) => adapter.value === picked) ?? null;
1919
2568
  }
1920
2569
  async function initProject(cwd, flags) {
1921
- if (!(0, import_node_fs9.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
2570
+ if (!(0, import_node_fs12.existsSync)((0, import_node_path17.join)(cwd, "package.json"))) {
1922
2571
  throw new Error(
1923
2572
  "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
2573
  );
@@ -1943,14 +2592,14 @@ async function initProject(cwd, flags) {
1943
2592
  prompts.cancel(`The ${adapter.label} adapter isn't available yet - only Hono ships today.`);
1944
2593
  return;
1945
2594
  }
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));
2595
+ const configPath = (0, import_node_path17.join)(cwd, "giri.config.ts");
2596
+ if (!(0, import_node_fs12.existsSync)(configPath)) {
2597
+ await (0, import_promises5.writeFile)(configPath, configSource(adapter));
1949
2598
  }
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)(
2599
+ const routePath = (0, import_node_path17.join)(cwd, "src", "routes", "+get.ts");
2600
+ if (!(0, import_node_fs12.existsSync)(routePath)) {
2601
+ await (0, import_promises5.mkdir)((0, import_node_path17.join)(cwd, "src", "routes"), { recursive: true });
2602
+ await (0, import_promises5.writeFile)(
1954
2603
  routePath,
1955
2604
  [
1956
2605
  'import type { Handle } from "@boon4681/giri";',
@@ -2017,69 +2666,156 @@ function displayHost(address) {
2017
2666
  return address.includes(":") ? `[${address}]` : address;
2018
2667
  }
2019
2668
  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));
2669
+ Error.stackTraceLimit = 30;
2670
+ const { watch } = await import("chokidar");
2671
+ let stop;
2672
+ const boot = async (cfg) => {
2673
+ const closers = [];
2674
+ closers.push(registerAliasResolver(cfg.alias, (0, import_node_path17.resolve)(process.cwd())));
2675
+ const lifecycle = await loadLifecycle();
2676
+ const sync = syncProject(cfg).then((initial2) => {
2677
+ log2.success(
2678
+ `synced ${initial2.routes.length} route${initial2.routes.length === 1 ? "" : "s"} ${muted(`at ${initial2.paths.outDir}`)}`,
2679
+ "sync"
2680
+ );
2681
+ return initial2;
2682
+ });
2683
+ const [initial, services] = await Promise.all([sync, runInit(lifecycle)]);
2684
+ const loader = await safeRegister();
2685
+ closers.push(loader.unregister);
2686
+ let current = await buildGiriApp(cfg, {
2687
+ services,
2688
+ lazy: true,
2689
+ loaderRegistered: true,
2690
+ aliasResolverRegistered: true
2691
+ });
2692
+ const port = flags.port ?? cfg.server?.port ?? 3e3;
2693
+ const hostname = flags.hostname ?? cfg.server?.hostname;
2694
+ if (flags.watch) {
2695
+ const srcDir = (0, import_node_path17.resolve)(current.paths.routesDir, "..");
2696
+ if ((0, import_node_fs12.existsSync)(srcDir)) {
2697
+ let timer;
2698
+ let syncing = false;
2699
+ const changed = /* @__PURE__ */ new Set();
2700
+ const updater = createWatchUpdater(cfg, initial);
2701
+ const hmrCount = /* @__PURE__ */ new Map();
2702
+ const bump = (key) => {
2703
+ const next = (hmrCount.get(key) ?? 0) + 1;
2704
+ hmrCount.set(key, next);
2705
+ return next;
2706
+ };
2707
+ const flush = async () => {
2708
+ if (syncing) {
2709
+ return;
2710
+ }
2711
+ syncing = true;
2712
+ try {
2713
+ while (changed.size > 0) {
2714
+ const batch = [...changed];
2715
+ changed.clear();
2716
+ let rebuild = false;
2717
+ let fullSync = false;
2718
+ const dirtySet = /* @__PURE__ */ new Set();
2719
+ for (const name of batch) {
2720
+ const outcome = await updater.apply(name || null);
2721
+ if (outcome === "skip") {
2722
+ continue;
2723
+ }
2724
+ rebuild = true;
2725
+ if (name) {
2726
+ dirtySet.add((0, import_node_path17.resolve)(srcDir, name));
2727
+ }
2728
+ const rel = name ? `src/${name.replace(/\\/g, "/")}` : "src";
2729
+ log2.change(outcome === "full" ? "sync" : "update", rel, bump(rel));
2730
+ if (outcome === "full") {
2731
+ fullSync = true;
2732
+ break;
2733
+ }
2734
+ }
2735
+ if (rebuild) {
2736
+ current = await buildGiriApp(cfg, {
2737
+ services,
2738
+ dirty: fullSync ? void 0 : dirtySet,
2739
+ lazy: true,
2740
+ loaderRegistered: true,
2741
+ aliasResolverRegistered: true
2742
+ });
2743
+ }
2057
2744
  }
2058
- current = await buildGiriApp(config, { services });
2745
+ } catch (error) {
2746
+ log2.error(error instanceof Error ? error.message : String(error), "watch");
2747
+ } finally {
2748
+ syncing = false;
2059
2749
  }
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
- });
2750
+ if (changed.size > 0) {
2751
+ void flush();
2752
+ }
2753
+ };
2754
+ const watcher = watch(srcDir, { persistent: true, ignoreInitial: true });
2755
+ const onFileChange = (filename) => {
2756
+ changed.add((0, import_node_path17.isAbsolute)(filename) ? (0, import_node_path17.relative)(srcDir, filename) : filename);
2757
+ clearTimeout(timer);
2758
+ timer = setTimeout(() => void flush(), 150);
2759
+ };
2760
+ watcher.on("change", onFileChange);
2761
+ watcher.on("add", onFileChange);
2762
+ watcher.on("unlink", onFileChange);
2763
+ closers.push(() => {
2764
+ clearTimeout(timer);
2765
+ watcher.close();
2766
+ });
2767
+ }
2074
2768
  }
2769
+ const handler = (request) => cfg.adapter.fetch(current.app, request);
2770
+ const server = cfg.adapter.serve(handler, { port, hostname }, (info) => {
2771
+ log2.ready(`http://${displayHost(info.address)}:${info.port}`);
2772
+ });
2773
+ stop = async () => {
2774
+ for (const close of closers) {
2775
+ await close();
2776
+ }
2777
+ try {
2778
+ await server.close();
2779
+ } catch {
2780
+ }
2781
+ if (lifecycle.teardown) {
2782
+ await lifecycle.teardown(services);
2783
+ }
2784
+ };
2785
+ };
2786
+ await boot(config);
2787
+ const configPath = flags.watch ? findConfigPath((0, import_node_path17.resolve)(process.cwd())) : void 0;
2788
+ if (configPath) {
2789
+ let timer;
2790
+ let restarting = false;
2791
+ const configName = (0, import_node_path17.basename)(configPath);
2792
+ const restart = async () => {
2793
+ if (restarting) {
2794
+ return;
2795
+ }
2796
+ restarting = true;
2797
+ try {
2798
+ log2.info(`${color.green("restart")} ${configName} changed`, "config");
2799
+ delete require.cache[configPath];
2800
+ const next = await load({ throwOnError: true });
2801
+ await stop?.();
2802
+ await boot(next);
2803
+ } catch (error) {
2804
+ log2.error(error instanceof Error ? error.message : String(error), "config");
2805
+ log2.info("kept the previous server running \u2014 fix the config and save again", "config");
2806
+ } finally {
2807
+ restarting = false;
2808
+ }
2809
+ };
2810
+ const configWatcher = watch(configPath, { persistent: true, ignoreInitial: true });
2811
+ configWatcher.on("all", () => {
2812
+ clearTimeout(timer);
2813
+ timer = setTimeout(() => void restart(), 150);
2814
+ });
2075
2815
  }
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);
2816
+ registerShutdown(() => stop?.());
2081
2817
  }
2082
- function registerShutdown(server, lifecycle, services) {
2818
+ function registerShutdown(cleanup) {
2083
2819
  let shuttingDown = false;
2084
2820
  const shutdown = async () => {
2085
2821
  if (shuttingDown) {
@@ -2087,10 +2823,7 @@ function registerShutdown(server, lifecycle, services) {
2087
2823
  }
2088
2824
  shuttingDown = true;
2089
2825
  try {
2090
- await server.close();
2091
- if (lifecycle.teardown) {
2092
- await lifecycle.teardown(services);
2093
- }
2826
+ await cleanup();
2094
2827
  } catch (error) {
2095
2828
  log2.error(error instanceof Error ? error.message : String(error));
2096
2829
  process.exitCode = 1;
@@ -2103,7 +2836,7 @@ function registerShutdown(server, lifecycle, services) {
2103
2836
  }
2104
2837
  async function main() {
2105
2838
  const [command = "help", ...args] = process.argv.slice(2);
2106
- const cwd = (0, import_node_path15.resolve)(process.cwd());
2839
+ const cwd = (0, import_node_path17.resolve)(process.cwd());
2107
2840
  if (command === "help" || command === "--help" || command === "-h") {
2108
2841
  help();
2109
2842
  return;