@boon4681/giri 0.0.2-alpha-6 → 0.0.2-alpha-8

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,7 +410,7 @@ 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_fs10 = require("fs");
398
414
  var import_promises4 = require("fs/promises");
399
415
  var import_node_path15 = require("path");
400
416
  var prompts = __toESM(require("@clack/prompts"));
@@ -526,13 +542,18 @@ function methodFromFile(fileName) {
526
542
  const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
527
543
  return METHOD_FROM_FILE.get(stem);
528
544
  }
529
- function sharedFileIn(dir) {
545
+ function sharedFileIn(dir, cache) {
546
+ if (cache?.has(dir)) {
547
+ return cache.get(dir);
548
+ }
530
549
  for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
531
550
  const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
532
551
  if ((0, import_node_fs2.existsSync)(file)) {
552
+ cache?.set(dir, file);
533
553
  return file;
534
554
  }
535
555
  }
556
+ cache?.set(dir, void 0);
536
557
  return void 0;
537
558
  }
538
559
  function physicalRouteSegments(routesDir, routeDir) {
@@ -601,7 +622,7 @@ async function scanRouteFolders(routesDir) {
601
622
  function routeParamsForDir(routesDir, dir) {
602
623
  return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
603
624
  }
604
- function sharedFilesForDir(routesDir, dir) {
625
+ function sharedFilesForDir(routesDir, dir, cache) {
605
626
  const segments = physicalRouteSegments(routesDir, dir);
606
627
  const dirs = [routesDir];
607
628
  let current = routesDir;
@@ -609,7 +630,7 @@ function sharedFilesForDir(routesDir, dir) {
609
630
  current = (0, import_node_path2.join)(current, segment);
610
631
  dirs.push(current);
611
632
  }
612
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
633
+ return dirs.map((currentDir) => sharedFileIn(currentDir, cache)).filter((file) => Boolean(file));
613
634
  }
614
635
  async function scanRoutes(routesDir) {
615
636
  if (!(0, import_node_fs2.existsSync)(routesDir)) {
@@ -621,6 +642,7 @@ async function scanRoutes(routesDir) {
621
642
  onlyFiles: true
622
643
  });
623
644
  const routes = [];
645
+ const sharedCache = /* @__PURE__ */ new Map();
624
646
  for (const file of files) {
625
647
  const method = methodFromFile((0, import_node_path2.basename)(file));
626
648
  if (!method) {
@@ -636,7 +658,7 @@ async function scanRoutes(routesDir) {
636
658
  routeDir,
637
659
  routeSegments,
638
660
  params,
639
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
661
+ sharedFiles: sharedFilesForDir(routesDir, routeDir, sharedCache)
640
662
  });
641
663
  }
642
664
  return routes.sort((left, right) => {
@@ -665,9 +687,11 @@ function isGiriBodySchema(value) {
665
687
  }
666
688
 
667
689
  // src/app.ts
668
- function loadModule(file) {
690
+ function loadModule(file, force = true) {
669
691
  const resolved = require.resolve(file);
670
- delete require.cache[resolved];
692
+ if (force) {
693
+ delete require.cache[resolved];
694
+ }
671
695
  return require(resolved);
672
696
  }
673
697
  function interopDefault(value) {
@@ -802,13 +826,23 @@ async function buildGiriApp(config, options = {}) {
802
826
  const { unregister } = await safeRegister();
803
827
  const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
804
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
+ };
805
839
  for (const route of routes) {
806
- const routeModule = loadModule(route.file);
840
+ const routeModule = loadModule(route.file, isDirty(route.file));
807
841
  if (typeof routeModule.handle !== "function") {
808
842
  throw new Error(`${route.file} must export a named handle function.`);
809
843
  }
810
844
  const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
811
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
845
+ (file) => normalizeMiddleware(loadShared(file).middleware, file)
812
846
  );
813
847
  const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
814
848
  config.adapter.register(app, {
@@ -829,7 +863,7 @@ async function buildGiriApp(config, options = {}) {
829
863
  }
830
864
 
831
865
  // src/generator/sync.ts
832
- var import_node_fs6 = require("fs");
866
+ var import_node_fs7 = require("fs");
833
867
  var import_promises3 = require("fs/promises");
834
868
  var import_node_path11 = require("path");
835
869
 
@@ -1083,6 +1117,7 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1083
1117
  const documentPaths = {};
1084
1118
  const schemas = {};
1085
1119
  const securitySchemes = {};
1120
+ const tagOrder = [];
1086
1121
  for (const route of routes) {
1087
1122
  if (data.hiddenFiles?.has(route.file)) {
1088
1123
  continue;
@@ -1090,10 +1125,32 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1090
1125
  const responses = data.responsesByFile?.get(route.file);
1091
1126
  const input = data.inputsByFile?.get(route.file);
1092
1127
  const security = data.securityByFile?.get(route.file);
1128
+ const meta = data.openapiByFile?.get(route.file);
1093
1129
  for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1094
1130
  schemas[name] = rewriteRefs(schema);
1095
1131
  }
1096
- 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 ?? []);
1097
1154
  const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1098
1155
  if (parameters.length > 0) {
1099
1156
  operation.parameters = parameters;
@@ -1125,6 +1182,9 @@ function buildOpenApiDocument(paths, routes, data = {}) {
1125
1182
  info: readProjectInfo(paths.cwd),
1126
1183
  paths: documentPaths
1127
1184
  };
1185
+ if (tagOrder.length > 0) {
1186
+ document.tags = tagOrder.map((name) => ({ name }));
1187
+ }
1128
1188
  const components = {};
1129
1189
  if (Object.keys(schemas).length > 0) {
1130
1190
  components.schemas = schemas;
@@ -1156,14 +1216,29 @@ function paramsType(params) {
1156
1216
  ${fields}
1157
1217
  }`;
1158
1218
  }
1159
- function varsType(typesDir, sharedFiles) {
1160
- if (sharedFiles.length === 0) {
1161
- 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
+ }
1162
1228
  }
1163
- return sharedFiles.map((file) => {
1164
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1165
- return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
1166
- }).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 & ") : "{}";
1167
1242
  }
1168
1243
  function methodExports(typesDir, verbs) {
1169
1244
  return verbs.map(({ method, file }) => {
@@ -1174,14 +1249,14 @@ function methodExports(typesDir, verbs) {
1174
1249
  });
1175
1250
  }
1176
1251
  async function writeParamTypes(paths, folders) {
1177
- for (const { dir, params, sharedFiles, verbs } of folders) {
1252
+ await Promise.all(folders.map(({ dir, params, sharedFiles, verbs }) => {
1178
1253
  const file = typeFilePath(paths, dir);
1179
1254
  const typesDir = (0, import_node_path8.dirname)(file);
1180
1255
  const lines = [
1181
1256
  GENERATED_HEADER,
1182
1257
  `export type Params = ${paramsType(params)};`,
1183
1258
  "export type RouteParams = Params;",
1184
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
1259
+ `export type Vars = ${varsType(paths, file, dir, sharedFiles)};`,
1185
1260
  "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1186
1261
  ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1187
1262
  'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
@@ -1191,10 +1266,14 @@ async function writeParamTypes(paths, folders) {
1191
1266
  lines.push(...methodExports(typesDir, verbs));
1192
1267
  }
1193
1268
  lines.push("");
1194
- await writeGenerated(file, lines.join("\n"));
1195
- }
1269
+ return writeGenerated(file, lines.join("\n"));
1270
+ }));
1196
1271
  }
1197
1272
 
1273
+ // src/generator/route-meta.ts
1274
+ var import_node_fs6 = require("fs");
1275
+ var import_typescript = __toESM(require("typescript"));
1276
+
1198
1277
  // src/generator/inputs.ts
1199
1278
  function sanitize(schema) {
1200
1279
  const { $schema, ...rest } = schema;
@@ -1255,28 +1334,315 @@ function readInput(routeModule) {
1255
1334
  }
1256
1335
  return input.body || input.query ? input : void 0;
1257
1336
  }
1258
- function hiddenFrom(value) {
1259
- 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) {
1260
1350
  return true;
1261
1351
  }
1262
- if (value === true) {
1352
+ if (value.kind === import_typescript.default.SyntaxKind.FalseKeyword) {
1263
1353
  return false;
1264
1354
  }
1265
- if (value && typeof value === "object" && "hidden" in value) {
1266
- 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;
1267
1379
  }
1268
1380
  return void 0;
1269
1381
  }
1270
- 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) {
1271
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
+ };
1272
1560
  for (const file of route.sharedFiles) {
1273
- const opinion = hiddenFrom(loadShared(file).openapi);
1274
- if (opinion !== void 0) {
1275
- hidden = opinion;
1561
+ const shared = loadShared(file);
1562
+ if (!shared) {
1563
+ return void 0;
1276
1564
  }
1565
+ apply(shared.openapi, false);
1566
+ }
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;
1277
1584
  }
1278
- const verb = hiddenFrom(routeModule.openapi);
1279
- return verb ?? hidden;
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 };
1280
1646
  }
1281
1647
  function collectSecurity(route, routeModule, loadShared) {
1282
1648
  const skipInherited = Boolean(
@@ -1308,6 +1674,33 @@ function collectSecurity(route, routeModule, loadShared) {
1308
1674
  }
1309
1675
  async function extractRouteMeta(config, paths, routes) {
1310
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
+ }
1311
1704
  const { unregister } = await safeRegister();
1312
1705
  const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1313
1706
  const sharedCache = /* @__PURE__ */ new Map();
@@ -1322,13 +1715,19 @@ async function extractRouteMeta(config, paths, routes) {
1322
1715
  return sharedCache.get(file);
1323
1716
  };
1324
1717
  try {
1325
- 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) {
1326
1725
  try {
1327
1726
  const routeModule = loadModule2(route.file);
1328
1727
  const meta = {};
1329
1728
  const input = readInput(routeModule);
1330
1729
  const security = collectSecurity(route, routeModule, loadShared);
1331
- const hidden = collectHidden(route, routeModule, loadShared);
1730
+ const { hidden, meta: openapi } = resolveOpenApi(route, routeModule, loadShared);
1332
1731
  if (input) {
1333
1732
  meta.input = input;
1334
1733
  }
@@ -1338,7 +1737,10 @@ async function extractRouteMeta(config, paths, routes) {
1338
1737
  if (hidden) {
1339
1738
  meta.hidden = true;
1340
1739
  }
1341
- 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) {
1342
1744
  byFile.set(route.file, meta);
1343
1745
  }
1344
1746
  } catch {
@@ -1420,10 +1822,11 @@ async function typeFolders(paths, routes) {
1420
1822
  verbsByDir.set(key, list);
1421
1823
  }
1422
1824
  const dirs = await scanRouteFolders(paths.routesDir);
1825
+ const sharedCache = /* @__PURE__ */ new Map();
1423
1826
  return dirs.map((dir) => ({
1424
1827
  dir,
1425
1828
  params: routeParamsForDir(paths.routesDir, dir),
1426
- sharedFiles: sharedFilesForDir(paths.routesDir, dir),
1829
+ sharedFiles: sharedFilesForDir(paths.routesDir, dir, sharedCache),
1427
1830
  verbs: verbsByDir.get(slash(dir)) ?? []
1428
1831
  }));
1429
1832
  }
@@ -1436,24 +1839,63 @@ async function extractResponses(paths, routes) {
1436
1839
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1437
1840
  const files = [...new Set(routes.map((route) => route.file))];
1438
1841
  const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1439
- const program = createSchemaProgram2(
1440
- paths,
1441
- (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1442
- );
1842
+ const roots = (0, import_node_fs7.existsSync)(appTypes) ? [...files, appTypes] : files;
1843
+ const program = createSchemaProgram2(paths, roots, { lean: true });
1844
+ const fallbackFiles = [];
1443
1845
  for (const file of files) {
1444
- 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
+ }
1445
1860
  }
1446
1861
  } catch (error) {
1447
1862
  console.warn(`giri: skipped response schema generation (${error.message}).`);
1448
1863
  }
1449
1864
  return byFile;
1450
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
+ }
1451
1892
  async function extractMeta(config, paths, routes) {
1452
1893
  const inputsByFile = /* @__PURE__ */ new Map();
1453
1894
  const securityByFile = /* @__PURE__ */ new Map();
1454
1895
  const hiddenFiles = /* @__PURE__ */ new Set();
1896
+ const openapiByFile = /* @__PURE__ */ new Map();
1455
1897
  if (routes.length === 0) {
1456
- return { inputsByFile, securityByFile, hiddenFiles };
1898
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1457
1899
  }
1458
1900
  try {
1459
1901
  const meta = await extractRouteMeta(config, paths, routes);
@@ -1467,15 +1909,19 @@ async function extractMeta(config, paths, routes) {
1467
1909
  if (entry.hidden) {
1468
1910
  hiddenFiles.add(file);
1469
1911
  }
1912
+ if (entry.openapi) {
1913
+ openapiByFile.set(file, entry.openapi);
1914
+ }
1470
1915
  }
1471
1916
  } catch (error) {
1472
1917
  console.warn(`giri: skipped input/security generation (${error.message}).`);
1473
1918
  }
1474
- return { inputsByFile, securityByFile, hiddenFiles };
1919
+ return { inputsByFile, securityByFile, hiddenFiles, openapiByFile };
1475
1920
  }
1476
1921
  async function syncProject(config, options = {}) {
1477
1922
  const paths = resolveGiriPaths(config, options.cwd);
1478
1923
  assertSafeOutDir(paths);
1924
+ const hadOutDir = (0, import_node_fs7.existsSync)(paths.outDir);
1479
1925
  const routes = await scanRoutes(paths.routesDir);
1480
1926
  const folders = await typeFolders(paths, routes);
1481
1927
  await (0, import_promises3.mkdir)(paths.outDir, { recursive: true });
@@ -1484,26 +1930,28 @@ async function syncProject(config, options = {}) {
1484
1930
  await writeAppTypes(paths);
1485
1931
  await writeTsConfig(paths, config);
1486
1932
  const responsesByFile = await extractResponses(paths, routes);
1487
- const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1488
- 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 };
1489
1935
  await writeManifest(paths, routes, data);
1490
1936
  await writeOpenApi(paths, routes, data);
1491
- await pruneDir(
1492
- paths.outDir,
1493
- /* @__PURE__ */ new Set([
1494
- (0, import_node_path11.join)(paths.outDir, "tsconfig.json"),
1495
- (0, import_node_path11.join)(paths.outDir, "manifest.json"),
1496
- (0, import_node_path11.join)(paths.outDir, "openapi.json"),
1497
- (0, import_node_path11.join)(paths.outDir, "routes.d.ts"),
1498
- (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts"),
1499
- ...folders.map((folder) => typeFilePath(paths, folder.dir))
1500
- ])
1501
- );
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
+ }
1502
1950
  return { paths, routes, folders, data };
1503
1951
  }
1504
1952
 
1505
1953
  // src/generator/watch.ts
1506
- var import_node_fs7 = require("fs");
1954
+ var import_node_fs8 = require("fs");
1507
1955
  var import_node_path13 = require("path");
1508
1956
 
1509
1957
  // src/loader/module-loader.ts
@@ -1592,6 +2040,7 @@ function createWatchUpdater(config, initial) {
1592
2040
  data.inputsByFile = result.data.inputsByFile;
1593
2041
  data.securityByFile = result.data.securityByFile;
1594
2042
  data.hiddenFiles = result.data.hiddenFiles;
2043
+ data.openapiByFile = result.data.openapiByFile;
1595
2044
  purgeGeneratedModules(paths.outDir);
1596
2045
  return "full";
1597
2046
  };
@@ -1600,7 +2049,7 @@ function createWatchUpdater(config, initial) {
1600
2049
  try {
1601
2050
  const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1602
2051
  const appTypes = (0, import_node_path13.join)(paths.outDir, "types", "app.d.ts");
1603
- const program = createSchemaProgram2(paths, (0, import_node_fs7.existsSync)(appTypes) ? [key, appTypes] : [key]);
2052
+ const program = createSchemaProgram2(paths, (0, import_node_fs8.existsSync)(appTypes) ? [key, appTypes] : [key]);
1604
2053
  data.responsesByFile.set(key, extractRouteResponses2(program, key));
1605
2054
  } catch {
1606
2055
  }
@@ -1610,6 +2059,7 @@ function createWatchUpdater(config, initial) {
1610
2059
  data.inputsByFile.delete(key);
1611
2060
  data.securityByFile.delete(key);
1612
2061
  data.hiddenFiles.delete(key);
2062
+ data.openapiByFile.delete(key);
1613
2063
  if (entry?.input) {
1614
2064
  data.inputsByFile.set(key, entry.input);
1615
2065
  }
@@ -1619,6 +2069,9 @@ function createWatchUpdater(config, initial) {
1619
2069
  if (entry?.hidden) {
1620
2070
  data.hiddenFiles.add(key);
1621
2071
  }
2072
+ if (entry?.openapi) {
2073
+ data.openapiByFile.set(key, entry.openapi);
2074
+ }
1622
2075
  } catch {
1623
2076
  }
1624
2077
  };
@@ -1629,10 +2082,10 @@ function createWatchUpdater(config, initial) {
1629
2082
  }
1630
2083
  const abs = (0, import_node_path13.resolve)((0, import_node_path13.dirname)(paths.routesDir), filename);
1631
2084
  const file = slash(abs);
1632
- if (!(0, import_node_fs7.existsSync)(abs)) {
2085
+ if (!(0, import_node_fs8.existsSync)(abs)) {
1633
2086
  return fullResync();
1634
2087
  }
1635
- if ((0, import_node_fs7.statSync)(abs).isDirectory()) {
2088
+ if ((0, import_node_fs8.statSync)(abs).isDirectory()) {
1636
2089
  return "skip";
1637
2090
  }
1638
2091
  const graph = buildModuleGraph(paths.cwd);
@@ -1657,13 +2110,13 @@ function createWatchUpdater(config, initial) {
1657
2110
  }
1658
2111
 
1659
2112
  // src/lifecycle.ts
1660
- var import_node_fs8 = require("fs");
2113
+ var import_node_fs9 = require("fs");
1661
2114
  var import_node_path14 = require("path");
1662
2115
  var MAIN_EXTENSIONS2 = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1663
2116
  function resolveMainFile(cwd) {
1664
2117
  for (const ext of MAIN_EXTENSIONS2) {
1665
2118
  const file = (0, import_node_path14.join)(cwd, "src", `main.${ext}`);
1666
- if ((0, import_node_fs8.existsSync)(file)) {
2119
+ if ((0, import_node_fs9.existsSync)(file)) {
1667
2120
  return file;
1668
2121
  }
1669
2122
  }
@@ -1830,7 +2283,7 @@ function parseFlags(args) {
1830
2283
  async function ensureGitignore(cwd) {
1831
2284
  const file = (0, import_node_path15.join)(cwd, ".gitignore");
1832
2285
  const entry = ".giri";
1833
- if (!(0, import_node_fs9.existsSync)(file)) {
2286
+ if (!(0, import_node_fs10.existsSync)(file)) {
1834
2287
  await (0, import_promises4.writeFile)(file, `${entry}
1835
2288
  `);
1836
2289
  return;
@@ -1843,7 +2296,7 @@ async function ensureGitignore(cwd) {
1843
2296
  }
1844
2297
  async function ensureTsConfig(cwd) {
1845
2298
  const file = (0, import_node_path15.join)(cwd, "tsconfig.json");
1846
- if ((0, import_node_fs9.existsSync)(file)) {
2299
+ if ((0, import_node_fs10.existsSync)(file)) {
1847
2300
  return;
1848
2301
  }
1849
2302
  await (0, import_promises4.writeFile)(
@@ -1941,7 +2394,7 @@ async function selectAdapter(interactive) {
1941
2394
  return ADAPTERS.find((adapter) => adapter.value === picked) ?? null;
1942
2395
  }
1943
2396
  async function initProject(cwd, flags) {
1944
- if (!(0, import_node_fs9.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
2397
+ if (!(0, import_node_fs10.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
1945
2398
  throw new Error(
1946
2399
  "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."
1947
2400
  );
@@ -1967,11 +2420,11 @@ async function initProject(cwd, flags) {
1967
2420
  return;
1968
2421
  }
1969
2422
  const configPath = (0, import_node_path15.join)(cwd, "giri.config.ts");
1970
- if (!(0, import_node_fs9.existsSync)(configPath)) {
2423
+ if (!(0, import_node_fs10.existsSync)(configPath)) {
1971
2424
  await (0, import_promises4.writeFile)(configPath, configSource(adapter));
1972
2425
  }
1973
2426
  const routePath = (0, import_node_path15.join)(cwd, "src", "routes", "+get.ts");
1974
- if (!(0, import_node_fs9.existsSync)(routePath)) {
2427
+ if (!(0, import_node_fs10.existsSync)(routePath)) {
1975
2428
  await (0, import_promises4.mkdir)((0, import_node_path15.join)(cwd, "src", "routes"), { recursive: true });
1976
2429
  await (0, import_promises4.writeFile)(
1977
2430
  routePath,
@@ -2041,7 +2494,7 @@ function displayHost(address) {
2041
2494
  }
2042
2495
  async function serveProject(config, flags) {
2043
2496
  Error.stackTraceLimit = 30;
2044
- const { watch } = await import("fs");
2497
+ const { watch } = await import("chokidar");
2045
2498
  let stop;
2046
2499
  const boot = async (cfg) => {
2047
2500
  const initial = await syncProject(cfg);
@@ -2058,7 +2511,7 @@ async function serveProject(config, flags) {
2058
2511
  const hostname = flags.hostname ?? cfg.server?.hostname;
2059
2512
  if (flags.watch) {
2060
2513
  const srcDir = (0, import_node_path15.resolve)(current.paths.routesDir, "..");
2061
- if ((0, import_node_fs9.existsSync)(srcDir)) {
2514
+ if ((0, import_node_fs10.existsSync)(srcDir)) {
2062
2515
  let timer;
2063
2516
  let syncing = false;
2064
2517
  const changed = /* @__PURE__ */ new Set();
@@ -2078,21 +2531,30 @@ async function serveProject(config, flags) {
2078
2531
  while (changed.size > 0) {
2079
2532
  const batch = [...changed];
2080
2533
  changed.clear();
2081
- let dirty = false;
2534
+ let rebuild = false;
2535
+ let fullSync = false;
2536
+ const dirtySet = /* @__PURE__ */ new Set();
2082
2537
  for (const name of batch) {
2083
2538
  const outcome = await updater.apply(name || null);
2084
2539
  if (outcome === "skip") {
2085
2540
  continue;
2086
2541
  }
2087
- dirty = true;
2542
+ rebuild = true;
2543
+ if (name) {
2544
+ dirtySet.add((0, import_node_path15.resolve)(srcDir, name));
2545
+ }
2088
2546
  const rel = name ? `src/${name.replace(/\\/g, "/")}` : "src";
2089
2547
  log2.change(outcome === "full" ? "sync" : "update", rel, bump(rel));
2090
2548
  if (outcome === "full") {
2549
+ fullSync = true;
2091
2550
  break;
2092
2551
  }
2093
2552
  }
2094
- if (dirty) {
2095
- current = await buildGiriApp(cfg, { services });
2553
+ if (rebuild) {
2554
+ current = await buildGiriApp(cfg, {
2555
+ services,
2556
+ dirty: fullSync ? void 0 : dirtySet
2557
+ });
2096
2558
  }
2097
2559
  }
2098
2560
  } catch (error) {
@@ -2104,11 +2566,15 @@ async function serveProject(config, flags) {
2104
2566
  void flush();
2105
2567
  }
2106
2568
  };
2107
- const watcher = watch(srcDir, { recursive: true }, (_event, filename) => {
2108
- changed.add(filename ? filename.toString() : "");
2569
+ const watcher = watch(srcDir, { persistent: true });
2570
+ const onFileChange = (filename) => {
2571
+ changed.add(filename);
2109
2572
  clearTimeout(timer);
2110
2573
  timer = setTimeout(() => void flush(), 150);
2111
- });
2574
+ };
2575
+ watcher.on("change", onFileChange);
2576
+ watcher.on("add", onFileChange);
2577
+ watcher.on("unlink", onFileChange);
2112
2578
  closers.push(() => {
2113
2579
  clearTimeout(timer);
2114
2580
  watcher.close();
@@ -2156,7 +2622,8 @@ async function serveProject(config, flags) {
2156
2622
  restarting = false;
2157
2623
  }
2158
2624
  };
2159
- watch((0, import_node_path15.dirname)(configPath), { recursive: false }, (_event, filename) => {
2625
+ const configWatcher = watch((0, import_node_path15.dirname)(configPath), { persistent: true });
2626
+ configWatcher.on("all", (_event, filename) => {
2160
2627
  if (filename && (0, import_node_path15.basename)(filename.toString()) === configName) {
2161
2628
  clearTimeout(timer);
2162
2629
  timer = setTimeout(() => void restart(), 150);