@boon4681/giri 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -46,12 +46,18 @@ var init_es5 = __esm({
46
46
  });
47
47
 
48
48
  // src/generator/schema/program.ts
49
- function createSchemaProgram(paths, routeFiles) {
49
+ function leanOptions(options) {
50
+ return {
51
+ ...options,
52
+ types: []
53
+ };
54
+ }
55
+ function createSchemaProgram(paths, routeFiles, programOptions = {}) {
50
56
  let options = { ...DEFAULT_OPTIONS };
51
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
57
+ const configPath = import_typescript2.default.findConfigFile(paths.cwd, import_typescript2.default.sys.fileExists, "tsconfig.json");
52
58
  if (configPath) {
53
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
54
- ...import_typescript.default.sys,
59
+ const parsed = import_typescript2.default.getParsedCommandLineOfConfigFile(configPath, {}, {
60
+ ...import_typescript2.default.sys,
55
61
  onUnRecoverableConfigFileDiagnostic: () => {
56
62
  }
57
63
  });
@@ -59,17 +65,20 @@ function createSchemaProgram(paths, routeFiles) {
59
65
  options = { ...parsed.options, noEmit: true };
60
66
  }
61
67
  }
62
- return import_typescript.default.createProgram(routeFiles, options);
68
+ if (programOptions.lean) {
69
+ options = leanOptions(options);
70
+ }
71
+ return import_typescript2.default.createProgram(routeFiles, options);
63
72
  }
64
- var import_typescript, DEFAULT_OPTIONS;
73
+ var import_typescript2, DEFAULT_OPTIONS;
65
74
  var init_program = __esm({
66
75
  "src/generator/schema/program.ts"() {
67
76
  "use strict";
68
- import_typescript = __toESM(require("typescript"));
77
+ import_typescript2 = __toESM(require("typescript"));
69
78
  DEFAULT_OPTIONS = {
70
- target: import_typescript.default.ScriptTarget.ES2022,
71
- module: import_typescript.default.ModuleKind.NodeNext,
72
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
79
+ target: import_typescript2.default.ScriptTarget.ES2022,
80
+ module: import_typescript2.default.ModuleKind.NodeNext,
81
+ moduleResolution: import_typescript2.default.ModuleResolutionKind.NodeNext,
73
82
  strict: true,
74
83
  skipLibCheck: true,
75
84
  noEmit: true
@@ -103,7 +112,7 @@ function literalValuesOf(types) {
103
112
  for (const member of types) {
104
113
  if (member.isStringLiteral() || member.isNumberLiteral()) {
105
114
  values.push(member.value);
106
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
115
+ } else if (member.flags & import_typescript3.default.TypeFlags.BooleanLiteral) {
107
116
  values.push(intrinsicName(member) === "true");
108
117
  } else {
109
118
  return void 0;
@@ -112,7 +121,7 @@ function literalValuesOf(types) {
112
121
  return values;
113
122
  }
114
123
  function walkUnion(type, ctx) {
115
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
124
+ const flag = import_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void | import_typescript3.default.TypeFlags.Never;
116
125
  const members = type.types.filter((member) => !(member.flags & flag));
117
126
  if (members.length === 1) {
118
127
  return walkType(members[0], ctx);
@@ -125,13 +134,13 @@ function walkUnion(type, ctx) {
125
134
  }
126
135
  function buildObjectSchema(type, ctx) {
127
136
  const { checker } = ctx;
128
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
137
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript3.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript3.default.IndexKind.Number);
129
138
  const properties = {};
130
139
  const required = [];
131
140
  for (const symbol of checker.getPropertiesOfType(type)) {
132
141
  const name = symbol.getName();
133
142
  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));
143
+ 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
144
  properties[name] = walkType(propType, ctx);
136
145
  if (!optional) {
137
146
  required.push(name);
@@ -190,16 +199,16 @@ function walkObject(type, ctx) {
190
199
  }
191
200
  function walkType(type, ctx) {
192
201
  const flags = type.flags;
193
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
202
+ if (flags & (import_typescript3.default.TypeFlags.Any | import_typescript3.default.TypeFlags.Unknown)) {
194
203
  return {};
195
204
  }
196
- if (flags & import_typescript2.default.TypeFlags.Null) {
205
+ if (flags & import_typescript3.default.TypeFlags.Null) {
197
206
  return { type: "null" };
198
207
  }
199
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
208
+ if (flags & (import_typescript3.default.TypeFlags.Undefined | import_typescript3.default.TypeFlags.Void)) {
200
209
  return {};
201
210
  }
202
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
211
+ if (flags & (import_typescript3.default.TypeFlags.BigInt | import_typescript3.default.TypeFlags.BigIntLiteral)) {
203
212
  ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
204
213
  return { type: "string" };
205
214
  }
@@ -209,45 +218,45 @@ function walkType(type, ctx) {
209
218
  if (type.isNumberLiteral()) {
210
219
  return { type: "number", const: type.value };
211
220
  }
212
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
221
+ if (flags & import_typescript3.default.TypeFlags.BooleanLiteral) {
213
222
  return { type: "boolean", const: intrinsicName(type) === "true" };
214
223
  }
215
- if (flags & import_typescript2.default.TypeFlags.String) {
224
+ if (flags & import_typescript3.default.TypeFlags.String) {
216
225
  return { type: "string" };
217
226
  }
218
- if (flags & import_typescript2.default.TypeFlags.Number) {
227
+ if (flags & import_typescript3.default.TypeFlags.Number) {
219
228
  return { type: "number" };
220
229
  }
221
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
230
+ if (flags & import_typescript3.default.TypeFlags.Boolean) {
222
231
  return { type: "boolean" };
223
232
  }
224
233
  if (type.isUnion()) {
225
234
  return walkUnion(type, ctx);
226
235
  }
227
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
236
+ if (flags & import_typescript3.default.TypeFlags.Object || type.isIntersection()) {
228
237
  return walkObject(type, ctx);
229
238
  }
230
239
  return {};
231
240
  }
232
- var import_typescript2;
241
+ var import_typescript3;
233
242
  var init_json_schema = __esm({
234
243
  "src/generator/schema/json-schema.ts"() {
235
244
  "use strict";
236
- import_typescript2 = __toESM(require("typescript"));
245
+ import_typescript3 = __toESM(require("typescript"));
237
246
  }
238
247
  });
239
248
 
240
249
  // src/generator/schema/responses.ts
241
250
  function findHandleFunction(source) {
242
251
  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);
252
+ const isExported = (node) => import_typescript4.default.canHaveModifiers(node) && (import_typescript4.default.getModifiers(node)?.some((m) => m.kind === import_typescript4.default.SyntaxKind.ExportKeyword) ?? false);
244
253
  for (const statement of source.statements) {
245
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
254
+ if (import_typescript4.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
246
255
  found = statement;
247
256
  }
248
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
257
+ if (import_typescript4.default.isVariableStatement(statement) && isExported(statement)) {
249
258
  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))) {
259
+ 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
260
  found = declaration.initializer;
252
261
  }
253
262
  }
@@ -256,7 +265,7 @@ function findHandleFunction(source) {
256
265
  return found;
257
266
  }
258
267
  function collectReturnExpressions(fn) {
259
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
268
+ if (import_typescript4.default.isArrowFunction(fn) && !import_typescript4.default.isBlock(fn.body)) {
260
269
  return [fn.body];
261
270
  }
262
271
  if (!fn.body) {
@@ -264,15 +273,15 @@ function collectReturnExpressions(fn) {
264
273
  }
265
274
  const expressions = [];
266
275
  const visit = (node) => {
267
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
276
+ if (import_typescript4.default.isFunctionDeclaration(node) || import_typescript4.default.isFunctionExpression(node) || import_typescript4.default.isArrowFunction(node)) {
268
277
  return;
269
278
  }
270
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
279
+ if (import_typescript4.default.isReturnStatement(node) && node.expression) {
271
280
  expressions.push(node.expression);
272
281
  }
273
- import_typescript3.default.forEachChild(node, visit);
282
+ import_typescript4.default.forEachChild(node, visit);
274
283
  };
275
- import_typescript3.default.forEachChild(fn.body, visit);
284
+ import_typescript4.default.forEachChild(fn.body, visit);
276
285
  return expressions;
277
286
  }
278
287
  function propertyType(checker, type, name, location) {
@@ -284,15 +293,21 @@ function isTypedResponse(checker, type) {
284
293
  checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
285
294
  );
286
295
  }
287
- function readFromCall(checker, expression) {
288
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
296
+ function firstParameterName(fn) {
297
+ const [first] = fn.parameters;
298
+ return first && import_typescript4.default.isIdentifier(first.name) ? first.name.text : void 0;
299
+ }
300
+ function readFromCall(checker, expression, contextName) {
301
+ if (!import_typescript4.default.isCallExpression(expression) || !import_typescript4.default.isPropertyAccessExpression(expression.expression)) {
289
302
  return void 0;
290
303
  }
291
304
  const method = expression.expression.name.text;
292
305
  if (method !== "json" && method !== "text") {
293
306
  return void 0;
294
307
  }
295
- if (!isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
308
+ const target = expression.expression.expression;
309
+ const directContextCall = contextName && import_typescript4.default.isIdentifier(target) && target.text === contextName;
310
+ if (!directContextCall && !isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
296
311
  return void 0;
297
312
  }
298
313
  const [dataArg, statusArg] = expression.arguments;
@@ -332,6 +347,7 @@ function extractRouteResponses(program, file) {
332
347
  return result;
333
348
  }
334
349
  const ctx = createWalkContext(checker, fn);
350
+ const contextName = firstParameterName(fn);
335
351
  const byStatus = /* @__PURE__ */ new Map();
336
352
  const record = (hit) => {
337
353
  const schema = walkType(hit.data, ctx);
@@ -340,7 +356,7 @@ function extractRouteResponses(program, file) {
340
356
  byStatus.set(hit.status, bucket);
341
357
  };
342
358
  for (const expression of collectReturnExpressions(fn)) {
343
- const fromCall = readFromCall(checker, expression);
359
+ const fromCall = readFromCall(checker, expression, contextName);
344
360
  if (fromCall) {
345
361
  record(fromCall);
346
362
  continue;
@@ -366,11 +382,11 @@ function extractRouteResponses(program, file) {
366
382
  result.$defs = ctx.defs;
367
383
  return result;
368
384
  }
369
- var import_typescript3;
385
+ var import_typescript4;
370
386
  var init_responses = __esm({
371
387
  "src/generator/schema/responses.ts"() {
372
388
  "use strict";
373
- import_typescript3 = __toESM(require("typescript"));
389
+ import_typescript4 = __toESM(require("typescript"));
374
390
  init_json_schema();
375
391
  }
376
392
  });
@@ -394,9 +410,9 @@ var init_schema = __esm({
394
410
 
395
411
  // src/cli.ts
396
412
  var import_node_child_process = require("child_process");
397
- var import_node_fs9 = require("fs");
413
+ var import_node_fs11 = require("fs");
398
414
  var import_promises4 = require("fs/promises");
399
- var import_node_path15 = require("path");
415
+ var import_node_path16 = require("path");
400
416
  var prompts = __toESM(require("@clack/prompts"));
401
417
 
402
418
  // src/app.ts
@@ -422,7 +438,8 @@ var configSchema = import_typebox.Type.Object({
422
438
  port: import_typebox.Type.Optional(import_typebox.Type.Number()),
423
439
  hostname: import_typebox.Type.Optional(import_typebox.Type.String())
424
440
  }, { additionalProperties: false })),
425
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
441
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any()),
442
+ cookieSecret: import_typebox.Type.Optional(import_typebox.Type.String())
426
443
  }, { additionalProperties: false });
427
444
 
428
445
  // src/loader/loader.ts
@@ -461,29 +478,44 @@ var safeRegister = async () => {
461
478
  await assertES5(res.unregister);
462
479
  return res;
463
480
  };
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);
481
+ var findConfigPath = (cwd = (0, import_node_path.resolve)()) => {
482
+ for (const name of ["giri.config.ts", "giri.config.js"]) {
483
+ const path = (0, import_node_path.resolve)(cwd, name);
484
+ if ((0, import_node_fs.existsSync)(path)) {
485
+ return path;
486
+ }
471
487
  }
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`);
488
+ return void 0;
489
+ };
490
+ var load = async (opts = {}) => {
491
+ const fail = (message) => {
492
+ if (opts.throwOnError) {
493
+ throw new Error(message);
494
+ }
495
+ import_prompts.log.error(message);
475
496
  (0, import_node_process.exit)(1);
497
+ };
498
+ const path = findConfigPath();
499
+ if (!path) {
500
+ fail("Config file not found.");
476
501
  }
477
502
  const { unregister } = await safeRegister();
478
- const required = require(`${path}`);
479
- const content = required.default ?? required;
503
+ let content;
504
+ try {
505
+ const required = require(`${path}`);
506
+ content = required.default ?? required;
507
+ } finally {
508
+ }
480
509
  unregister();
481
510
  const res = import_value.Value.Check(configSchema, content);
482
511
  if (!res) {
483
- for (const error of [...import_value.Value.Errors(configSchema, content)]) {
484
- import_prompts.log.error(error.message);
512
+ const messages = [...import_value.Value.Errors(configSchema, content)].map((error) => error.message);
513
+ if (!opts.throwOnError) {
514
+ for (const message of messages) {
515
+ import_prompts.log.error(message);
516
+ }
485
517
  }
486
- (0, import_node_process.exit)(1);
518
+ fail(messages.join("\n"));
487
519
  }
488
520
  return content;
489
521
  };
@@ -510,13 +542,18 @@ function methodFromFile(fileName) {
510
542
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
511
543
  return METHOD_FROM_FILE.get(stem);
512
544
  }
513
- function sharedFileIn(dir) {
545
+ function sharedFileIn(dir, cache) {
546
+ if (cache?.has(dir)) {
547
+ return cache.get(dir);
548
+ }
514
549
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
515
550
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
516
551
  if ((0, import_node_fs2.existsSync)(file)) {
552
+ cache?.set(dir, file);
517
553
  return file;
518
554
  }
519
555
  }
556
+ cache?.set(dir, void 0);
520
557
  return void 0;
521
558
  }
522
559
  function physicalRouteSegments(routesDir, routeDir) {
@@ -585,7 +622,7 @@ async function scanRouteFolders(routesDir) {
585
622
  function routeParamsForDir(routesDir, dir) {
586
623
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
587
624
  }
588
- function sharedFilesForDir(routesDir, dir) {
625
+ function sharedFilesForDir(routesDir, dir, cache) {
589
626
  const segments = physicalRouteSegments(routesDir, dir);
590
627
  const dirs = [routesDir];
591
628
  let current = routesDir;
@@ -593,7 +630,7 @@ function sharedFilesForDir(routesDir, dir) {
593
630
  current = (0, import_node_path2.join)(current, segment);
594
631
  dirs.push(current);
595
632
  }
596
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
633
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
597
634
  }
598
635
  async function scanRoutes(routesDir) {
599
636
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -605,6 +642,7 @@ async function scanRoutes(routesDir) {
605
642
  onlyFiles: true
606
643
  });
607
644
  const routes = [];
645
+ const sharedCache = /* @__PURE__ */ new Map();
608
646
  for (const file of files) {
609
647
  const method = methodFromFile((0, import_node_path2.basename)(file));
610
648
  if (!method) {
@@ -620,7 +658,7 @@ async function scanRoutes(routesDir) {
620
658
  routeDir,
621
659
  routeSegments,
622
660
  params,
623
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
661
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
624
662
  });
625
663
  }
626
664
  return routes.sort((left, right) => {
@@ -649,9 +687,11 @@ function isGiriBodySchema(value) {
649
687
  }
650
688
 
651
689
  // src/app.ts
652
- function loadModule(file) {
690
+ function loadModule(file, force = true) {
653
691
  const resolved = require.resolve(file);
654
- delete require.cache[resolved];
692
+ if (force) {
693
+ delete require.cache[resolved];
694
+ }
655
695
  return require(resolved);
656
696
  }
657
697
  function interopDefault(value) {
@@ -713,20 +753,13 @@ function resolveAliasTarget(cwd, target, capture = "") {
713
753
  }
714
754
  function matchAlias(request, key) {
715
755
  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);
756
+ const [prefix, suffix = ""] = key.split("*");
757
+ if (request.startsWith(prefix) && request.endsWith(suffix)) {
758
+ return request.slice(prefix.length, request.length - suffix.length);
719
759
  }
720
760
  return void 0;
721
761
  }
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;
762
+ return request === key ? "" : void 0;
730
763
  }
731
764
  function resolveAliasRequest(request, alias, cwd) {
732
765
  for (const [key, value] of Object.entries(alias ?? {})) {
@@ -793,13 +826,23 @@ async function buildGiriApp(config, options = {}) {
793
826
  const { unregister } = await safeRegister();
794
827
  const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
795
828
  try {
829
+ const dirty = options.dirty;
830
+ const forceReload = dirty === void 0;
831
+ const isDirty = (file) => forceReload || dirty.has(file);
832
+ const sharedCache = /* @__PURE__ */ new Map();
833
+ const loadShared = (file) => {
834
+ if (!sharedCache.has(file)) {
835
+ sharedCache.set(file, loadModule(file, isDirty(file)));
836
+ }
837
+ return sharedCache.get(file);
838
+ };
796
839
  for (const route of routes) {
797
- const routeModule = loadModule(route.file);
840
+ const routeModule = loadModule(route.file, isDirty(route.file));
798
841
  if (typeof routeModule.handle !== "function") {
799
842
  throw new Error(`${route.file} must export a named handle function.`);
800
843
  }
801
844
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
802
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
845
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
803
846
  );
804
847
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
805
848
  config.adapter.register(app, {
@@ -808,7 +851,8 @@ async function buildGiriApp(config, options = {}) {
808
851
  handle: routeModule.handle,
809
852
  middleware: [...folderMiddleware, ...verbMiddleware],
810
853
  input: routeInput(routeModule, route.file),
811
- services: options.services
854
+ services: options.services,
855
+ cookieSecret: config.cookieSecret
812
856
  });
813
857
  }
814
858
  } finally {
@@ -819,7 +863,7 @@ async function buildGiriApp(config, options = {}) {
819
863
  }
820
864
 
821
865
  // src/generator/sync.ts
822
- var import_node_fs6 = require("fs");
866
+ var import_node_fs7 = require("fs");
823
867
  var import_promises3 = require("fs/promises");
824
868
  var import_node_path11 = require("path");
825
869
 
@@ -1073,6 +1117,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1073
1117
  const documentPaths = {};
1074
1118
  const schemas = {};
1075
1119
  const securitySchemes = {};
1120
+ const tagOrder = [];
1076
1121
  for (const route of routes) {
1077
1122
  if (data.hiddenFiles?.has(route.file)) {
1078
1123
  continue;
@@ -1080,10 +1125,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1080
1125
  const responses = data.responsesByFile?.get(route.file);
1081
1126
  const input = data.inputsByFile?.get(route.file);
1082
1127
  const security = data.securityByFile?.get(route.file);
1128
+ const meta = data.openapiByFile?.get(route.file);
1083
1129
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1084
1130
  schemas[name] = rewriteRefs(schema);
1085
1131
  }
1086
- const operation = { responses: buildResponses(responses?.responses ?? []) };
1132
+ const operation = {};
1133
+ if (meta?.tags && meta.tags.length > 0) {
1134
+ operation.tags = meta.tags;
1135
+ for (const tag2 of meta.tags) {
1136
+ if (!tagOrder.includes(tag2)) {
1137
+ tagOrder.push(tag2);
1138
+ }
1139
+ }
1140
+ }
1141
+ if (meta?.summary) {
1142
+ operation.summary = meta.summary;
1143
+ }
1144
+ if (meta?.description) {
1145
+ operation.description = meta.description;
1146
+ }
1147
+ if (meta?.operationId) {
1148
+ operation.operationId = meta.operationId;
1149
+ }
1150
+ if (meta?.deprecated) {
1151
+ operation.deprecated = true;
1152
+ }
1153
+ operation.responses = buildResponses(responses?.responses ?? []);
1087
1154
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1088
1155
  if (parameters.length > 0) {
1089
1156
  operation.parameters = parameters;
@@ -1115,6 +1182,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1115
1182
  info: readProjectInfo(paths.cwd),
1116
1183
  paths: documentPaths
1117
1184
  };
1185
+ if (tagOrder.length > 0) {
1186
+ document.tags = tagOrder.map((name) => ({ name }));
1187
+ }
1118
1188
  const components = {};
1119
1189
  if (Object.keys(schemas).length > 0) {
1120
1190
  components.schemas = schemas;
@@ -1146,14 +1216,29 @@ function paramsType(params) {
1146
1216
  ${fields}
1147
1217
  }`;
1148
1218
  }
1149
- function varsType(typesDir, sharedFiles) {
1150
- if (sharedFiles.length === 0) {
1151
- return "{}";
1219
+ function middlewareVarsType(typesDir, sharedFile) {
1220
+ const spec = JSON.stringify(moduleSpecifier(typesDir, sharedFile));
1221
+ return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1222
+ }
1223
+ function ownSharedFile(dir, sharedFiles) {
1224
+ for (let index = sharedFiles.length - 1; index >= 0; index -= 1) {
1225
+ if ((0, import_node_path8.dirname)(sharedFiles[index]) === dir) {
1226
+ return sharedFiles[index];
1227
+ }
1152
1228
  }
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 & ");
1229
+ return void 0;
1230
+ }
1231
+ function varsType(paths, file, dir, sharedFiles) {
1232
+ const typesDir = (0, import_node_path8.dirname)(file);
1233
+ const parts = [];
1234
+ if (dir !== paths.routesDir) {
1235
+ parts.push(`import(${JSON.stringify(importPath(file, typeFilePath(paths, (0, import_node_path8.dirname)(dir))))}).Vars`);
1236
+ }
1237
+ const ownShared = ownSharedFile(dir, sharedFiles);
1238
+ if (ownShared) {
1239
+ parts.push(middlewareVarsType(typesDir, ownShared));
1240
+ }
1241
+ return parts.length > 0 ? parts.join("\n & ") : "{}";
1157
1242
  }
1158
1243
  function methodExports(typesDir, verbs) {
1159
1244
  return verbs.map(({ method, file }) => {
@@ -1164,14 +1249,14 @@ function methodExports(typesDir, verbs) {
1164
1249
  });
1165
1250
  }
1166
1251
  async function writeParamTypes(paths, folders) {
1167
- for (const { dir, params, sharedFiles, verbs } of folders) {
1252
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1168
1253
  const file = typeFilePath(paths, dir);
1169
1254
  const typesDir = (0, import_node_path8.dirname)(file);
1170
1255
  const lines = [
1171
1256
  GENERATED_HEADER,
1172
1257
  `export type Params = ${paramsType(params)};`,
1173
1258
  "export type RouteParams = Params;",
1174
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1259
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1175
1260
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1176
1261
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1177
1262
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1181,10 +1266,14 @@ async function writeParamTypes(paths, folders) {
1181
1266
  lines.push(...methodExports(typesDir, verbs));
1182
1267
  }
1183
1268
  lines.push("");
1184
- await writeGenerated(file, lines.join("\n"));
1185
- }
1269
+ return writeGenerated(file, lines.join("\n"));
1270
+ }));
1186
1271
  }
1187
1272
 
1273
+ // src/generator/route-meta.ts
1274
+ var import_node_fs6 = require("fs");
1275
+ var import_typescript = __toESM(require("typescript"));
1276
+
1188
1277
  // src/generator/inputs.ts
1189
1278
  function sanitize(schema) {
1190
1279
  const { $schema, ...rest } = schema;
@@ -1245,28 +1334,315 @@ function readInput(routeModule) {
1245
1334
  }
1246
1335
  return input.body || input.query ? input : void 0;
1247
1336
  }
1248
- function hiddenFrom(value) {
1249
- if (value === false) {
1337
+ function hasExportModifier(node) {
1338
+ return import_typescript.default.canHaveModifiers(node) && (import_typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword) ?? false);
1339
+ }
1340
+ function unwrapExpression(expression) {
1341
+ let current = expression;
1342
+ while (import_typescript.default.isParenthesizedExpression(current) || import_typescript.default.isAsExpression(current) || import_typescript.default.isSatisfiesExpression(current)) {
1343
+ current = current.expression;
1344
+ }
1345
+ return current;
1346
+ }
1347
+ function staticBoolean(expression) {
1348
+ const value = unwrapExpression(expression);
1349
+ if (value.kind === import_typescript.default.SyntaxKind.TrueKeyword) {
1250
1350
  return true;
1251
1351
  }
1252
- if (value === true) {
1352
+ if (value.kind === import_typescript.default.SyntaxKind.FalseKeyword) {
1253
1353
  return false;
1254
1354
  }
1255
- if (value && typeof value === "object" && "hidden" in value) {
1256
- return Boolean(value.hidden);
1355
+ return void 0;
1356
+ }
1357
+ function staticString(expression) {
1358
+ const value = unwrapExpression(expression);
1359
+ return import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
1360
+ }
1361
+ function staticStringArray(expression) {
1362
+ const value = unwrapExpression(expression);
1363
+ if (!import_typescript.default.isArrayLiteralExpression(value)) {
1364
+ return void 0;
1365
+ }
1366
+ const strings = [];
1367
+ for (const element of value.elements) {
1368
+ const string = staticString(element);
1369
+ if (string === void 0) {
1370
+ return void 0;
1371
+ }
1372
+ strings.push(string);
1373
+ }
1374
+ return strings;
1375
+ }
1376
+ function propertyName(name) {
1377
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
1378
+ return name.text;
1257
1379
  }
1258
1380
  return void 0;
1259
1381
  }
1260
- function collectHidden(route, routeModule, loadShared) {
1382
+ function collectImportedNames(source) {
1383
+ const names = /* @__PURE__ */ new Set();
1384
+ for (const statement of source.statements) {
1385
+ if (!import_typescript.default.isImportDeclaration(statement)) {
1386
+ continue;
1387
+ }
1388
+ const clause = statement.importClause;
1389
+ if (!clause) {
1390
+ continue;
1391
+ }
1392
+ if (clause.name) {
1393
+ names.add(clause.name.text);
1394
+ }
1395
+ const bindings = clause.namedBindings;
1396
+ if (bindings && import_typescript.default.isNamespaceImport(bindings)) {
1397
+ names.add(bindings.name.text);
1398
+ }
1399
+ if (bindings && import_typescript.default.isNamedImports(bindings)) {
1400
+ for (const element of bindings.elements) {
1401
+ names.add(element.name.text);
1402
+ }
1403
+ }
1404
+ }
1405
+ return names;
1406
+ }
1407
+ function expressionReferencesImportedMiddleware(expression, importedNames) {
1408
+ let found = false;
1409
+ const allowedImportedHelpers = /* @__PURE__ */ new Set(["stack", "fromHono"]);
1410
+ const visit = (node) => {
1411
+ if (found) {
1412
+ return;
1413
+ }
1414
+ if (import_typescript.default.isIdentifier(node) && importedNames.has(node.text) && !allowedImportedHelpers.has(node.text)) {
1415
+ found = true;
1416
+ return;
1417
+ }
1418
+ import_typescript.default.forEachChild(node, visit);
1419
+ };
1420
+ visit(expression);
1421
+ return found;
1422
+ }
1423
+ function parseStaticOpenApi(expression) {
1424
+ const value = unwrapExpression(expression);
1425
+ const boolean = staticBoolean(value);
1426
+ if (boolean !== void 0) {
1427
+ return boolean;
1428
+ }
1429
+ if (!import_typescript.default.isObjectLiteralExpression(value)) {
1430
+ return void 0;
1431
+ }
1432
+ const openapi = {};
1433
+ for (const property of value.properties) {
1434
+ if (!import_typescript.default.isPropertyAssignment(property)) {
1435
+ return void 0;
1436
+ }
1437
+ const name = propertyName(property.name);
1438
+ if (!name) {
1439
+ return void 0;
1440
+ }
1441
+ if (name === "hidden") {
1442
+ const hidden = staticBoolean(property.initializer);
1443
+ if (hidden === void 0) {
1444
+ return void 0;
1445
+ }
1446
+ openapi.hidden = hidden;
1447
+ } else if (name === "tags") {
1448
+ const tags = staticStringArray(property.initializer);
1449
+ if (!tags) {
1450
+ return void 0;
1451
+ }
1452
+ openapi.tags = tags;
1453
+ } else if (name === "summary" || name === "description" || name === "operationId") {
1454
+ const string = staticString(property.initializer);
1455
+ if (string === void 0) {
1456
+ return void 0;
1457
+ }
1458
+ openapi[name] = string;
1459
+ } else if (name === "deprecated") {
1460
+ const deprecated = staticBoolean(property.initializer);
1461
+ if (deprecated === void 0) {
1462
+ return void 0;
1463
+ }
1464
+ openapi.deprecated = deprecated;
1465
+ } else {
1466
+ return void 0;
1467
+ }
1468
+ }
1469
+ return openapi;
1470
+ }
1471
+ function readStaticModuleMeta(file) {
1472
+ let source;
1473
+ try {
1474
+ source = import_typescript.default.createSourceFile(file, (0, import_node_fs6.readFileSync)(file, "utf8"), import_typescript.default.ScriptTarget.Latest, true);
1475
+ } catch {
1476
+ return void 0;
1477
+ }
1478
+ const importedNames = collectImportedNames(source);
1479
+ const sourceText = source.getFullText();
1480
+ const canSkipMiddlewareRuntime = !sourceText.includes("defineMiddleware") && !sourceText.includes(".openapi");
1481
+ const meta = { middlewareSecurity: false };
1482
+ for (const statement of source.statements) {
1483
+ if (import_typescript.default.isImportDeclaration(statement) || import_typescript.default.isInterfaceDeclaration(statement) || import_typescript.default.isTypeAliasDeclaration(statement) || import_typescript.default.isEmptyStatement(statement) || !hasExportModifier(statement)) {
1484
+ continue;
1485
+ }
1486
+ if (import_typescript.default.isFunctionDeclaration(statement) && statement.name?.text === "handle") {
1487
+ continue;
1488
+ }
1489
+ if (import_typescript.default.isVariableStatement(statement)) {
1490
+ for (const declaration of statement.declarationList.declarations) {
1491
+ if (!import_typescript.default.isIdentifier(declaration.name)) {
1492
+ return void 0;
1493
+ }
1494
+ const name = declaration.name.text;
1495
+ if (name === "openapi") {
1496
+ if (!declaration.initializer) {
1497
+ return void 0;
1498
+ }
1499
+ const openapi = parseStaticOpenApi(declaration.initializer);
1500
+ if (openapi === void 0) {
1501
+ return void 0;
1502
+ }
1503
+ meta.openapi = openapi;
1504
+ } else if (name === "handle") {
1505
+ if (!declaration.initializer || !import_typescript.default.isArrowFunction(declaration.initializer) && !import_typescript.default.isFunctionExpression(declaration.initializer)) {
1506
+ return void 0;
1507
+ }
1508
+ continue;
1509
+ } else if (name === "middleware") {
1510
+ if (!declaration.initializer || !canSkipMiddlewareRuntime || expressionReferencesImportedMiddleware(declaration.initializer, importedNames)) {
1511
+ return void 0;
1512
+ }
1513
+ meta.middlewareSecurity = false;
1514
+ continue;
1515
+ } else {
1516
+ return void 0;
1517
+ }
1518
+ }
1519
+ continue;
1520
+ }
1521
+ return void 0;
1522
+ }
1523
+ return meta;
1524
+ }
1525
+ function resolveStaticOpenApi(route, routeModule, loadShared) {
1261
1526
  let hidden = false;
1527
+ const tags = [];
1528
+ const meta = {};
1529
+ const apply = (value, isVerb) => {
1530
+ if (value === false) {
1531
+ hidden = true;
1532
+ return;
1533
+ }
1534
+ if (value === true) {
1535
+ hidden = false;
1536
+ return;
1537
+ }
1538
+ if (!value) {
1539
+ return;
1540
+ }
1541
+ if (typeof value.hidden === "boolean") {
1542
+ hidden = value.hidden;
1543
+ }
1544
+ if (value.tags) {
1545
+ tags.push(...value.tags);
1546
+ }
1547
+ if (typeof value.summary === "string") {
1548
+ meta.summary = value.summary;
1549
+ }
1550
+ if (typeof value.description === "string") {
1551
+ meta.description = value.description;
1552
+ }
1553
+ if (typeof value.deprecated === "boolean") {
1554
+ meta.deprecated = value.deprecated;
1555
+ }
1556
+ if (isVerb && typeof value.operationId === "string") {
1557
+ meta.operationId = value.operationId;
1558
+ }
1559
+ };
1262
1560
  for (const file of route.sharedFiles) {
1263
- const opinion = hiddenFrom(loadShared(file).openapi);
1264
- if (opinion !== void 0) {
1265
- hidden = opinion;
1561
+ const shared = loadShared(file);
1562
+ if (!shared) {
1563
+ return void 0;
1266
1564
  }
1565
+ apply(shared.openapi, false);
1267
1566
  }
1268
- const verb = hiddenFrom(routeModule.openapi);
1269
- return verb ?? hidden;
1567
+ apply(routeModule.openapi, true);
1568
+ if (tags.length > 0) {
1569
+ meta.tags = [...new Set(tags)];
1570
+ }
1571
+ return { hidden, meta };
1572
+ }
1573
+ function extractStaticMeta(route, routeModule, loadShared) {
1574
+ const openapi = resolveStaticOpenApi(route, routeModule, loadShared);
1575
+ if (!openapi) {
1576
+ return void 0;
1577
+ }
1578
+ const meta = {};
1579
+ if (openapi.hidden) {
1580
+ meta.hidden = true;
1581
+ }
1582
+ if (Object.keys(openapi.meta).length > 0) {
1583
+ meta.openapi = openapi.meta;
1584
+ }
1585
+ return meta;
1586
+ }
1587
+ function extractRuntimeSharedMeta(route, routeModule, loadShared) {
1588
+ const meta = {};
1589
+ const security = collectSecurity(route, {}, loadShared);
1590
+ const { hidden, meta: openapi } = resolveOpenApi(route, { openapi: routeModule.openapi }, loadShared);
1591
+ if (security) {
1592
+ meta.security = security;
1593
+ }
1594
+ if (hidden) {
1595
+ meta.hidden = true;
1596
+ }
1597
+ if (Object.keys(openapi).length > 0) {
1598
+ meta.openapi = openapi;
1599
+ }
1600
+ return meta;
1601
+ }
1602
+ function resolveOpenApi(route, routeModule, loadShared) {
1603
+ let hidden = false;
1604
+ const tags = [];
1605
+ const meta = {};
1606
+ const apply = (value, isVerb) => {
1607
+ if (value === false) {
1608
+ hidden = true;
1609
+ return;
1610
+ }
1611
+ if (value === true) {
1612
+ hidden = false;
1613
+ return;
1614
+ }
1615
+ if (!value || typeof value !== "object") {
1616
+ return;
1617
+ }
1618
+ const o = value;
1619
+ if ("hidden" in o) {
1620
+ hidden = Boolean(o.hidden);
1621
+ }
1622
+ if (Array.isArray(o.tags)) {
1623
+ tags.push(...o.tags.filter((tag2) => typeof tag2 === "string"));
1624
+ }
1625
+ if (typeof o.summary === "string") {
1626
+ meta.summary = o.summary;
1627
+ }
1628
+ if (typeof o.description === "string") {
1629
+ meta.description = o.description;
1630
+ }
1631
+ if (typeof o.deprecated === "boolean") {
1632
+ meta.deprecated = o.deprecated;
1633
+ }
1634
+ if (isVerb && typeof o.operationId === "string") {
1635
+ meta.operationId = o.operationId;
1636
+ }
1637
+ };
1638
+ for (const file of route.sharedFiles) {
1639
+ apply(loadShared(file).openapi, false);
1640
+ }
1641
+ apply(routeModule.openapi, true);
1642
+ if (tags.length > 0) {
1643
+ meta.tags = [...new Set(tags)];
1644
+ }
1645
+ return { hidden, meta };
1270
1646
  }
1271
1647
  function collectSecurity(route, routeModule, loadShared) {
1272
1648
  const skipInherited = Boolean(
@@ -1298,6 +1674,33 @@ function collectSecurity(route, routeModule, loadShared) {
1298
1674
  }
1299
1675
  async function extractRouteMeta(config, paths, routes) {
1300
1676
  const byFile = /* @__PURE__ */ new Map();
1677
+ const remainingRoutes = [];
1678
+ const runtimeSharedRoutes = [];
1679
+ const staticCache = /* @__PURE__ */ new Map();
1680
+ const loadStatic = (file) => {
1681
+ if (!staticCache.has(file)) {
1682
+ staticCache.set(file, readStaticModuleMeta(file));
1683
+ }
1684
+ return staticCache.get(file);
1685
+ };
1686
+ for (const route of routes) {
1687
+ const routeModule = loadStatic(route.file);
1688
+ if (!routeModule) {
1689
+ remainingRoutes.push(route);
1690
+ continue;
1691
+ }
1692
+ const meta = extractStaticMeta(route, routeModule, loadStatic);
1693
+ if (meta) {
1694
+ if (meta.hidden || meta.openapi) {
1695
+ byFile.set(route.file, meta);
1696
+ }
1697
+ continue;
1698
+ }
1699
+ runtimeSharedRoutes.push({ route, routeModule });
1700
+ }
1701
+ if (remainingRoutes.length === 0 && runtimeSharedRoutes.length === 0) {
1702
+ return byFile;
1703
+ }
1301
1704
  const { unregister } = await safeRegister();
1302
1705
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1303
1706
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1312,13 +1715,19 @@ async function extractRouteMeta(config, paths, routes) {
1312
1715
  return sharedCache.get(file);
1313
1716
  };
1314
1717
  try {
1315
- for (const route of routes) {
1718
+ for (const { route, routeModule } of runtimeSharedRoutes) {
1719
+ const meta = extractRuntimeSharedMeta(route, routeModule, loadShared);
1720
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1721
+ byFile.set(route.file, meta);
1722
+ }
1723
+ }
1724
+ for (const route of remainingRoutes) {
1316
1725
  try {
1317
1726
  const routeModule = loadModule2(route.file);
1318
1727
  const meta = {};
1319
1728
  const input = readInput(routeModule);
1320
1729
  const security = collectSecurity(route, routeModule, loadShared);
1321
- const hidden = collectHidden(route, routeModule, loadShared);
1730
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1322
1731
  if (input) {
1323
1732
  meta.input = input;
1324
1733
  }
@@ -1328,7 +1737,10 @@ async function extractRouteMeta(config, paths, routes) {
1328
1737
  if (hidden) {
1329
1738
  meta.hidden = true;
1330
1739
  }
1331
- if (meta.input || meta.security || meta.hidden) {
1740
+ if (Object.keys(openapi).length > 0) {
1741
+ meta.openapi = openapi;
1742
+ }
1743
+ if (meta.input || meta.security || meta.hidden || meta.openapi) {
1332
1744
  byFile.set(route.file, meta);
1333
1745
  }
1334
1746
  } catch {
@@ -1410,10 +1822,11 @@ async function typeFolders(paths, routes) {
1410
1822
  verbsByDir.set(key, list);
1411
1823
  }
1412
1824
  const dirs = await scanRouteFolders(paths.routesDir);
1825
+ const sharedCache = /* @__PURE__ */ new Map();
1413
1826
  return dirs.map((dir) => ({
1414
1827
  dir,
1415
1828
  params: routeParamsForDir(paths.routesDir, dir),
1416
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
1829
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1417
1830
  verbs: verbsByDir.get(slash(dir)) ?? []
1418
1831
  }));
1419
1832
  }
@@ -1426,24 +1839,63 @@ async function extractResponses(paths, routes) {
1426
1839
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1427
1840
  const files = [...new Set(routes.map((route) => route.file))];
1428
1841
  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
- );
1842
+ const roots = (0, import_node_fs7.existsSync)(appTypes) ? [...files, appTypes] : files;
1843
+ const program = createSchemaProgram2(paths, roots, { lean: true });
1844
+ const fallbackFiles = [];
1433
1845
  for (const file of files) {
1434
- byFile.set(file, extractRouteResponses2(program, file));
1846
+ const responses = extractRouteResponses2(program, file);
1847
+ byFile.set(file, responses);
1848
+ if (hasLooseResponseSchema(responses)) {
1849
+ fallbackFiles.push(file);
1850
+ }
1851
+ }
1852
+ if (fallbackFiles.length > 0) {
1853
+ const fullProgram = createSchemaProgram2(
1854
+ paths,
1855
+ (0, import_node_fs7.existsSync)(appTypes) ? [...fallbackFiles, appTypes] : fallbackFiles
1856
+ );
1857
+ for (const file of fallbackFiles) {
1858
+ byFile.set(file, extractRouteResponses2(fullProgram, file));
1859
+ }
1435
1860
  }
1436
1861
  } catch (error) {
1437
1862
  console.warn(`giri: skipped response schema generation (${error.message}).`);
1438
1863
  }
1439
1864
  return byFile;
1440
1865
  }
1866
+ function isLooseSchema(value) {
1867
+ if (!value || typeof value !== "object") {
1868
+ return false;
1869
+ }
1870
+ const schema = value;
1871
+ const keys = Object.keys(schema);
1872
+ if (keys.length === 0) {
1873
+ return true;
1874
+ }
1875
+ if (typeof schema.$ref === "string") {
1876
+ return false;
1877
+ }
1878
+ if (Array.isArray(schema.anyOf) && schema.anyOf.some(isLooseSchema)) {
1879
+ return true;
1880
+ }
1881
+ if (schema.items && isLooseSchema(schema.items)) {
1882
+ return true;
1883
+ }
1884
+ if (schema.properties && typeof schema.properties === "object") {
1885
+ return Object.values(schema.properties).some(isLooseSchema);
1886
+ }
1887
+ return false;
1888
+ }
1889
+ function hasLooseResponseSchema(responses) {
1890
+ return responses.responses.some((response) => isLooseSchema(response.schema));
1891
+ }
1441
1892
  async function extractMeta(config, paths, routes) {
1442
1893
  const inputsByFile = /* @__PURE__ */ new Map();
1443
1894
  const securityByFile = /* @__PURE__ */ new Map();
1444
1895
  const hiddenFiles = /* @__PURE__ */ new Set();
1896
+ const openapiByFile = /* @__PURE__ */ new Map();
1445
1897
  if (routes.length === 0) {
1446
- return { inputsByFile, securityByFile, hiddenFiles };
1898
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1447
1899
  }
1448
1900
  try {
1449
1901
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1457,15 +1909,19 @@ async function extractMeta(config, paths, routes) {
1457
1909
  if (entry.hidden) {
1458
1910
  hiddenFiles.add(file);
1459
1911
  }
1912
+ if (entry.openapi) {
1913
+ openapiByFile.set(file, entry.openapi);
1914
+ }
1460
1915
  }
1461
1916
  } catch (error) {
1462
1917
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1463
1918
  }
1464
- return { inputsByFile, securityByFile, hiddenFiles };
1919
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1465
1920
  }
1466
1921
  async function syncProject(config, options = {}) {
1467
1922
  const paths = resolveGiriPaths(config, options.cwd);
1468
1923
  assertSafeOutDir(paths);
1924
+ const hadOutDir = (0, import_node_fs7.existsSync)(paths.outDir);
1469
1925
  const routes = await scanRoutes(paths.routesDir);
1470
1926
  const folders = await typeFolders(paths, routes);
1471
1927
  await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
@@ -1474,62 +1930,142 @@ async function syncProject(config, options = {}) {
1474
1930
  await writeAppTypes(paths);
1475
1931
  await writeTsConfig(paths, config);
1476
1932
  const responsesByFile = await extractResponses(paths, routes);
1477
- const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1478
- const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
1933
+ const { inputsByFile, securityByFile, hiddenFiles, openapiByFile } = await extractMeta(config, paths, routes);
1934
+ const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1479
1935
  await writeManifest(paths, routes, data);
1480
1936
  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
- );
1937
+ if (hadOutDir) {
1938
+ await pruneDir(
1939
+ paths.outDir,
1940
+ /* @__PURE__ */ new Set([
1941
+ (0, import_node_path11.join)(paths.outDir, "tsconfig.json"),
1942
+ (0, import_node_path11.join)(paths.outDir, "manifest.json"),
1943
+ (0, import_node_path11.join)(paths.outDir, "openapi.json"),
1944
+ (0, import_node_path11.join)(paths.outDir, "routes.d.ts"),
1945
+ (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts"),
1946
+ ...folders.map((folder) => typeFilePath(paths, folder.dir))
1947
+ ])
1948
+ );
1949
+ }
1492
1950
  return { paths, routes, folders, data };
1493
1951
  }
1494
1952
 
1495
1953
  // src/generator/watch.ts
1496
- var import_node_fs7 = require("fs");
1497
- var import_node_path13 = require("path");
1954
+ var import_node_fs9 = require("fs");
1955
+ var import_node_path14 = require("path");
1498
1956
 
1499
- // src/loader/module-loader.ts
1957
+ // src/loader/import-graph.ts
1958
+ var import_node_fs8 = require("fs");
1500
1959
  var import_node_path12 = require("path");
1960
+ var import_typescript5 = __toESM(require("typescript"));
1961
+ var import_tinyglobby2 = require("tinyglobby");
1962
+ var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1963
+ var JS_EXT = /\.(?:c|m)?jsx?$/;
1501
1964
  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;
1965
+ function probeFile(base) {
1966
+ if ((0, import_node_fs8.existsSync)(base) && (0, import_node_fs8.statSync)(base).isFile()) {
1967
+ return base;
1968
+ }
1969
+ for (const ext of RESOLVE_EXTS) {
1970
+ const candidate = base + ext;
1971
+ if ((0, import_node_fs8.existsSync)(candidate)) {
1972
+ return candidate;
1973
+ }
1974
+ }
1975
+ for (const ext of RESOLVE_EXTS) {
1976
+ const candidate = (0, import_node_path12.join)(base, `index${ext}`);
1977
+ if ((0, import_node_fs8.existsSync)(candidate)) {
1978
+ return candidate;
1979
+ }
1980
+ }
1981
+ return void 0;
1982
+ }
1983
+ function resolveSpecifier(specifier, fromFile, alias, cwd) {
1984
+ let target;
1985
+ if (specifier.startsWith(".")) {
1986
+ target = (0, import_node_path12.resolve)((0, import_node_path12.dirname)(fromFile), specifier);
1987
+ } else {
1988
+ const aliased = resolveAliasRequest(specifier, alias, cwd);
1989
+ if (aliased === void 0) {
1990
+ return void 0;
1991
+ }
1992
+ target = aliased;
1993
+ }
1994
+ const resolved = probeFile(target);
1995
+ if (resolved) {
1996
+ return resolved;
1997
+ }
1998
+ if (JS_EXT.test(target)) {
1999
+ return probeFile(target.replace(JS_EXT, ""));
2000
+ }
2001
+ return void 0;
2002
+ }
2003
+ function importSpecifiers(file) {
2004
+ let source;
2005
+ try {
2006
+ source = import_typescript5.default.createSourceFile(file, (0, import_node_fs8.readFileSync)(file, "utf8"), import_typescript5.default.ScriptTarget.Latest, false);
2007
+ } catch {
2008
+ return [];
2009
+ }
2010
+ const specifiers = [];
2011
+ const visit = (node) => {
2012
+ if ((import_typescript5.default.isImportDeclaration(node) || import_typescript5.default.isExportDeclaration(node)) && node.moduleSpecifier && import_typescript5.default.isStringLiteral(node.moduleSpecifier)) {
2013
+ specifiers.push(node.moduleSpecifier.text);
2014
+ } else if (import_typescript5.default.isImportEqualsDeclaration(node) && import_typescript5.default.isExternalModuleReference(node.moduleReference) && import_typescript5.default.isStringLiteralLike(node.moduleReference.expression)) {
2015
+ specifiers.push(node.moduleReference.expression.text);
2016
+ } else if (import_typescript5.default.isCallExpression(node)) {
2017
+ const isRequire = import_typescript5.default.isIdentifier(node.expression) && node.expression.text === "require";
2018
+ const isDynamicImport = node.expression.kind === import_typescript5.default.SyntaxKind.ImportKeyword;
2019
+ const [first] = node.arguments;
2020
+ if ((isRequire || isDynamicImport) && first && import_typescript5.default.isStringLiteralLike(first)) {
2021
+ specifiers.push(first.text);
2022
+ }
2023
+ }
2024
+ import_typescript5.default.forEachChild(node, visit);
2025
+ };
2026
+ visit(source);
2027
+ return specifiers;
2028
+ }
2029
+ async function buildImportGraph(config, cwd) {
2030
+ const root = (0, import_node_path12.resolve)(cwd);
2031
+ const outDir = (0, import_node_path12.resolve)(root, config.outDir ?? ".giri") + import_node_path12.sep;
2032
+ const files = await (0, import_tinyglobby2.glob)("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", {
2033
+ cwd: root,
2034
+ absolute: true,
2035
+ onlyFiles: true,
2036
+ ignore: ["**/node_modules/**"]
2037
+ });
1507
2038
  const importers = /* @__PURE__ */ new Map();
1508
2039
  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) {
2040
+ for (const file of files) {
2041
+ if (file.startsWith(outDir)) {
1515
2042
  continue;
1516
2043
  }
1517
- nodes.add(toSlash(id));
1518
- for (const child of mod.children) {
1519
- if (!isProjectModule(child.id, root)) {
2044
+ for (const specifier of importSpecifiers(file)) {
2045
+ const dep = resolveSpecifier(specifier, file, config.alias, root);
2046
+ if (!dep || dep.startsWith(outDir)) {
1520
2047
  continue;
1521
2048
  }
1522
- nodes.add(toSlash(child.id));
1523
- const dep = toSlash(child.id);
1524
- let set = importers.get(dep);
2049
+ const from = toSlash(file);
2050
+ const to = toSlash(dep);
2051
+ nodes.add(from);
2052
+ nodes.add(to);
2053
+ let set = importers.get(to);
1525
2054
  if (!set) {
1526
2055
  set = /* @__PURE__ */ new Set();
1527
- importers.set(dep, set);
2056
+ importers.set(to, set);
1528
2057
  }
1529
- set.add(toSlash(id));
2058
+ set.add(from);
1530
2059
  }
1531
2060
  }
1532
2061
  return { importers, nodes };
2062
+ }
2063
+
2064
+ // src/loader/module-loader.ts
2065
+ var import_node_path13 = require("path");
2066
+ var toSlash2 = (path) => path.split(import_node_path13.sep).join("/");
2067
+ var isProjectModule = (id, root) => {
2068
+ return id.startsWith(root) && !id.includes(`${import_node_path13.sep}node_modules${import_node_path13.sep}`) && !id.includes(`${import_node_path13.sep}.giri${import_node_path13.sep}`);
1533
2069
  };
1534
2070
  var collectDependents = (graph, start) => {
1535
2071
  const out = /* @__PURE__ */ new Set([start]);
@@ -1547,19 +2083,27 @@ var collectDependents = (graph, start) => {
1547
2083
  };
1548
2084
  var purgeModules = (files) => {
1549
2085
  for (const id of Object.keys(require.cache)) {
1550
- if (files.has(toSlash(id))) {
2086
+ if (files.has(toSlash2(id))) {
1551
2087
  delete require.cache[id];
1552
2088
  }
1553
2089
  }
1554
2090
  };
1555
2091
  var purgeProjectModules = (cwd) => {
1556
- const root = (0, import_node_path12.resolve)(cwd) + import_node_path12.sep;
2092
+ const root = (0, import_node_path13.resolve)(cwd) + import_node_path13.sep;
1557
2093
  for (const id of Object.keys(require.cache)) {
1558
2094
  if (isProjectModule(id, root)) {
1559
2095
  delete require.cache[id];
1560
2096
  }
1561
2097
  }
1562
2098
  };
2099
+ var purgeGeneratedModules = (outDir) => {
2100
+ const root = (0, import_node_path13.resolve)(outDir) + import_node_path13.sep;
2101
+ for (const id of Object.keys(require.cache)) {
2102
+ if ((0, import_node_path13.resolve)(id).startsWith(root)) {
2103
+ delete require.cache[id];
2104
+ }
2105
+ }
2106
+ };
1563
2107
 
1564
2108
  // src/generator/watch.ts
1565
2109
  function createWatchUpdater(config, initial) {
@@ -1574,14 +2118,16 @@ function createWatchUpdater(config, initial) {
1574
2118
  data.inputsByFile = result.data.inputsByFile;
1575
2119
  data.securityByFile = result.data.securityByFile;
1576
2120
  data.hiddenFiles = result.data.hiddenFiles;
2121
+ data.openapiByFile = result.data.openapiByFile;
2122
+ purgeGeneratedModules(paths.outDir);
1577
2123
  return "full";
1578
2124
  };
1579
2125
  const reextractRoute = async (route) => {
1580
2126
  const key = route.file;
1581
2127
  try {
1582
2128
  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]);
2129
+ const appTypes = (0, import_node_path14.join)(paths.outDir, "types", "app.d.ts");
2130
+ const program = createSchemaProgram2(paths, (0, import_node_fs9.existsSync)(appTypes) ? [key, appTypes] : [key]);
1585
2131
  data.responsesByFile.set(key, extractRouteResponses2(program, key));
1586
2132
  } catch {
1587
2133
  }
@@ -1591,6 +2137,7 @@ function createWatchUpdater(config, initial) {
1591
2137
  data.inputsByFile.delete(key);
1592
2138
  data.securityByFile.delete(key);
1593
2139
  data.hiddenFiles.delete(key);
2140
+ data.openapiByFile.delete(key);
1594
2141
  if (entry?.input) {
1595
2142
  data.inputsByFile.set(key, entry.input);
1596
2143
  }
@@ -1600,6 +2147,9 @@ function createWatchUpdater(config, initial) {
1600
2147
  if (entry?.hidden) {
1601
2148
  data.hiddenFiles.add(key);
1602
2149
  }
2150
+ if (entry?.openapi) {
2151
+ data.openapiByFile.set(key, entry.openapi);
2152
+ }
1603
2153
  } catch {
1604
2154
  }
1605
2155
  };
@@ -1608,12 +2158,15 @@ function createWatchUpdater(config, initial) {
1608
2158
  if (!filename) {
1609
2159
  return fullResync();
1610
2160
  }
1611
- const abs = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(paths.routesDir), filename);
2161
+ const abs = (0, import_node_path14.resolve)((0, import_node_path14.dirname)(paths.routesDir), filename);
1612
2162
  const file = slash(abs);
1613
- if (!(0, import_node_fs7.existsSync)(abs)) {
2163
+ if (!(0, import_node_fs9.existsSync)(abs)) {
1614
2164
  return fullResync();
1615
2165
  }
1616
- const graph = buildModuleGraph(paths.cwd);
2166
+ if ((0, import_node_fs9.statSync)(abs).isDirectory()) {
2167
+ return "skip";
2168
+ }
2169
+ const graph = await buildImportGraph(config, paths.cwd);
1617
2170
  const isRoute = routes.some((candidate) => slash(candidate.file) === file);
1618
2171
  if (!graph.nodes.has(file) && !isRoute) {
1619
2172
  return fullResync();
@@ -1628,26 +2181,27 @@ function createWatchUpdater(config, initial) {
1628
2181
  }
1629
2182
  await writeManifest(paths, routes, data);
1630
2183
  await writeOpenApi(paths, routes, data);
2184
+ purgeGeneratedModules(paths.outDir);
1631
2185
  return "incremental";
1632
2186
  }
1633
2187
  };
1634
2188
  }
1635
2189
 
1636
2190
  // src/lifecycle.ts
1637
- var import_node_fs8 = require("fs");
1638
- var import_node_path14 = require("path");
2191
+ var import_node_fs10 = require("fs");
2192
+ var import_node_path15 = require("path");
1639
2193
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1640
2194
  function resolveMainFile(cwd) {
1641
2195
  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)) {
2196
+ const file = (0, import_node_path15.join)(cwd, "src", `main.${ext}`);
2197
+ if ((0, import_node_fs10.existsSync)(file)) {
1644
2198
  return file;
1645
2199
  }
1646
2200
  }
1647
2201
  return void 0;
1648
2202
  }
1649
2203
  async function loadLifecycle(cwd = process.cwd()) {
1650
- const file = resolveMainFile((0, import_node_path14.resolve)(cwd));
2204
+ const file = resolveMainFile((0, import_node_path15.resolve)(cwd));
1651
2205
  if (!file) {
1652
2206
  return {};
1653
2207
  }
@@ -1805,9 +2359,9 @@ function parseFlags(args) {
1805
2359
  return flags;
1806
2360
  }
1807
2361
  async function ensureGitignore(cwd) {
1808
- const file = (0, import_node_path15.join)(cwd, ".gitignore");
2362
+ const file = (0, import_node_path16.join)(cwd, ".gitignore");
1809
2363
  const entry = ".giri";
1810
- if (!(0, import_node_fs9.existsSync)(file)) {
2364
+ if (!(0, import_node_fs11.existsSync)(file)) {
1811
2365
  await (0, import_promises4.writeFile)(file, `${entry}
1812
2366
  `);
1813
2367
  return;
@@ -1819,8 +2373,8 @@ async function ensureGitignore(cwd) {
1819
2373
  }
1820
2374
  }
1821
2375
  async function ensureTsConfig(cwd) {
1822
- const file = (0, import_node_path15.join)(cwd, "tsconfig.json");
1823
- if ((0, import_node_fs9.existsSync)(file)) {
2376
+ const file = (0, import_node_path16.join)(cwd, "tsconfig.json");
2377
+ if ((0, import_node_fs11.existsSync)(file)) {
1824
2378
  return;
1825
2379
  }
1826
2380
  await (0, import_promises4.writeFile)(
@@ -1849,7 +2403,7 @@ async function ensureTsConfig(cwd) {
1849
2403
  async function missingDeps(cwd, candidates) {
1850
2404
  let pkg = {};
1851
2405
  try {
1852
- pkg = JSON.parse(await (0, import_promises4.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8"));
2406
+ pkg = JSON.parse(await (0, import_promises4.readFile)((0, import_node_path16.join)(cwd, "package.json"), "utf8"));
1853
2407
  } catch {
1854
2408
  }
1855
2409
  const present = /* @__PURE__ */ new Set();
@@ -1918,7 +2472,7 @@ async function selectAdapter(interactive) {
1918
2472
  return ADAPTERS.find((adapter) => adapter.value === picked) ?? null;
1919
2473
  }
1920
2474
  async function initProject(cwd, flags) {
1921
- if (!(0, import_node_fs9.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
2475
+ if (!(0, import_node_fs11.existsSync)((0, import_node_path16.join)(cwd, "package.json"))) {
1922
2476
  throw new Error(
1923
2477
  "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
2478
  );
@@ -1943,13 +2497,13 @@ async function initProject(cwd, flags) {
1943
2497
  prompts.cancel(`The ${adapter.label} adapter isn't available yet - only Hono ships today.`);
1944
2498
  return;
1945
2499
  }
1946
- const configPath = (0, import_node_path15.join)(cwd, "giri.config.ts");
1947
- if (!(0, import_node_fs9.existsSync)(configPath)) {
2500
+ const configPath = (0, import_node_path16.join)(cwd, "giri.config.ts");
2501
+ if (!(0, import_node_fs11.existsSync)(configPath)) {
1948
2502
  await (0, import_promises4.writeFile)(configPath, configSource(adapter));
1949
2503
  }
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 });
2504
+ const routePath = (0, import_node_path16.join)(cwd, "src", "routes", "+get.ts");
2505
+ if (!(0, import_node_fs11.existsSync)(routePath)) {
2506
+ await (0, import_promises4.mkdir)((0, import_node_path16.join)(cwd, "src", "routes"), { recursive: true });
1953
2507
  await (0, import_promises4.writeFile)(
1954
2508
  routePath,
1955
2509
  [
@@ -2017,69 +2571,146 @@ function displayHost(address) {
2017
2571
  return address.includes(":") ? `[${address}]` : address;
2018
2572
  }
2019
2573
  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));
2574
+ Error.stackTraceLimit = 30;
2575
+ const { watch } = await import("chokidar");
2576
+ let stop;
2577
+ const boot = async (cfg) => {
2578
+ const initial = await syncProject(cfg);
2579
+ log2.success(
2580
+ `synced ${initial.routes.length} route${initial.routes.length === 1 ? "" : "s"} ${muted(`at ${initial.paths.outDir}`)}`,
2581
+ "sync"
2582
+ );
2583
+ const closers = [];
2584
+ closers.push(registerAliasResolver(cfg.alias, initial.paths.cwd));
2585
+ const lifecycle = await loadLifecycle();
2586
+ const services = await runInit(lifecycle);
2587
+ let current = await buildGiriApp(cfg, { services });
2588
+ const port = flags.port ?? cfg.server?.port ?? 3e3;
2589
+ const hostname = flags.hostname ?? cfg.server?.hostname;
2590
+ if (flags.watch) {
2591
+ const srcDir = (0, import_node_path16.resolve)(current.paths.routesDir, "..");
2592
+ if ((0, import_node_fs11.existsSync)(srcDir)) {
2593
+ let timer;
2594
+ let syncing = false;
2595
+ const changed = /* @__PURE__ */ new Set();
2596
+ const updater = createWatchUpdater(cfg, initial);
2597
+ const hmrCount = /* @__PURE__ */ new Map();
2598
+ const bump = (key) => {
2599
+ const next = (hmrCount.get(key) ?? 0) + 1;
2600
+ hmrCount.set(key, next);
2601
+ return next;
2602
+ };
2603
+ const flush = async () => {
2604
+ if (syncing) {
2605
+ return;
2606
+ }
2607
+ syncing = true;
2608
+ try {
2609
+ while (changed.size > 0) {
2610
+ const batch = [...changed];
2611
+ changed.clear();
2612
+ let rebuild = false;
2613
+ let fullSync = false;
2614
+ const dirtySet = /* @__PURE__ */ new Set();
2615
+ for (const name of batch) {
2616
+ const outcome = await updater.apply(name || null);
2617
+ if (outcome === "skip") {
2618
+ continue;
2619
+ }
2620
+ rebuild = true;
2621
+ if (name) {
2622
+ dirtySet.add((0, import_node_path16.resolve)(srcDir, name));
2623
+ }
2624
+ const rel = name ? `src/${name.replace(/\\/g, "/")}` : "src";
2625
+ log2.change(outcome === "full" ? "sync" : "update", rel, bump(rel));
2626
+ if (outcome === "full") {
2627
+ fullSync = true;
2628
+ break;
2629
+ }
2630
+ }
2631
+ if (rebuild) {
2632
+ current = await buildGiriApp(cfg, {
2633
+ services,
2634
+ dirty: fullSync ? void 0 : dirtySet
2635
+ });
2636
+ }
2057
2637
  }
2058
- current = await buildGiriApp(config, { services });
2638
+ } catch (error) {
2639
+ log2.error(error instanceof Error ? error.message : String(error), "watch");
2640
+ } finally {
2641
+ syncing = false;
2059
2642
  }
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
- });
2643
+ if (changed.size > 0) {
2644
+ void flush();
2645
+ }
2646
+ };
2647
+ const watcher = watch(srcDir, { persistent: true });
2648
+ const onFileChange = (filename) => {
2649
+ changed.add(filename);
2650
+ clearTimeout(timer);
2651
+ timer = setTimeout(() => void flush(), 150);
2652
+ };
2653
+ watcher.on("change", onFileChange);
2654
+ watcher.on("add", onFileChange);
2655
+ watcher.on("unlink", onFileChange);
2656
+ closers.push(() => {
2657
+ clearTimeout(timer);
2658
+ watcher.close();
2659
+ });
2660
+ }
2074
2661
  }
2662
+ const handler = (request) => cfg.adapter.fetch(current.app, request);
2663
+ const server = cfg.adapter.serve(handler, { port, hostname }, (info) => {
2664
+ log2.ready(`http://${displayHost(info.address)}:${info.port}`);
2665
+ });
2666
+ stop = async () => {
2667
+ for (const close of closers) {
2668
+ await close();
2669
+ }
2670
+ try {
2671
+ await server.close();
2672
+ } catch {
2673
+ }
2674
+ if (lifecycle.teardown) {
2675
+ await lifecycle.teardown(services);
2676
+ }
2677
+ };
2678
+ };
2679
+ await boot(config);
2680
+ const configPath = flags.watch ? findConfigPath((0, import_node_path16.resolve)(process.cwd())) : void 0;
2681
+ if (configPath) {
2682
+ let timer;
2683
+ let restarting = false;
2684
+ const configName = (0, import_node_path16.basename)(configPath);
2685
+ const restart = async () => {
2686
+ if (restarting) {
2687
+ return;
2688
+ }
2689
+ restarting = true;
2690
+ try {
2691
+ log2.info(`${color.green("restart")} ${configName} changed`, "config");
2692
+ delete require.cache[configPath];
2693
+ const next = await load({ throwOnError: true });
2694
+ await stop?.();
2695
+ await boot(next);
2696
+ } catch (error) {
2697
+ log2.error(error instanceof Error ? error.message : String(error), "config");
2698
+ log2.info("kept the previous server running \u2014 fix the config and save again", "config");
2699
+ } finally {
2700
+ restarting = false;
2701
+ }
2702
+ };
2703
+ const configWatcher = watch((0, import_node_path16.dirname)(configPath), { persistent: true });
2704
+ configWatcher.on("all", (_event, filename) => {
2705
+ if (filename && (0, import_node_path16.basename)(filename.toString()) === configName) {
2706
+ clearTimeout(timer);
2707
+ timer = setTimeout(() => void restart(), 150);
2708
+ }
2709
+ });
2075
2710
  }
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);
2711
+ registerShutdown(() => stop?.());
2081
2712
  }
2082
- function registerShutdown(server, lifecycle, services) {
2713
+ function registerShutdown(cleanup) {
2083
2714
  let shuttingDown = false;
2084
2715
  const shutdown = async () => {
2085
2716
  if (shuttingDown) {
@@ -2087,10 +2718,7 @@ function registerShutdown(server, lifecycle, services) {
2087
2718
  }
2088
2719
  shuttingDown = true;
2089
2720
  try {
2090
- await server.close();
2091
- if (lifecycle.teardown) {
2092
- await lifecycle.teardown(services);
2093
- }
2721
+ await cleanup();
2094
2722
  } catch (error) {
2095
2723
  log2.error(error instanceof Error ? error.message : String(error));
2096
2724
  process.exitCode = 1;
@@ -2103,7 +2731,7 @@ function registerShutdown(server, lifecycle, services) {
2103
2731
  }
2104
2732
  async function main() {
2105
2733
  const [command = "help", ...args] = process.argv.slice(2);
2106
- const cwd = (0, import_node_path15.resolve)(process.cwd());
2734
+ const cwd = (0, import_node_path16.resolve)(process.cwd());
2107
2735
  if (command === "help" || command === "--help" || command === "-h") {
2108
2736
  help();
2109
2737
  return;