@boon4681/giri 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -45,1286 +45,1320 @@ var init_es5 = __esm({
45
45
  }
46
46
  });
47
47
 
48
- // src/cli.ts
49
- var import_node_child_process = require("child_process");
50
- var import_node_fs9 = require("fs");
51
- var import_promises4 = require("fs/promises");
52
- var import_node_path15 = require("path");
53
- var prompts = __toESM(require("@clack/prompts"));
54
-
55
- // src/app.ts
56
- var import_node_module = __toESM(require("module"));
57
- var import_node_path3 = require("path");
58
-
59
- // src/loader/loader.ts
60
- var import_prompts = require("@clack/prompts");
61
- var import_node_fs = require("fs");
62
- var import_node_path = require("path");
63
- var import_node_process = require("process");
64
-
65
- // src/config/schema.ts
66
- var import_typebox = require("@sinclair/typebox");
67
- var configSchema = import_typebox.Type.Object({
68
- adapter: import_typebox.Type.Any(),
69
- alias: import_typebox.Type.Optional(import_typebox.Type.Record(
70
- import_typebox.Type.String(),
71
- import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
72
- )),
73
- outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
74
- server: import_typebox.Type.Optional(import_typebox.Type.Object({
75
- port: import_typebox.Type.Optional(import_typebox.Type.Number()),
76
- hostname: import_typebox.Type.Optional(import_typebox.Type.String())
77
- }, { additionalProperties: false })),
78
- errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
79
- }, { additionalProperties: false });
80
-
81
- // src/loader/loader.ts
82
- var import_value = require("@sinclair/typebox/value");
83
- var assertES5 = async (unregister) => {
84
- try {
85
- init_es5();
86
- } catch (e) {
87
- if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
88
- const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
89
- if (es5Error) {
90
- import_prompts.log.error(
91
- `Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
92
- );
93
- (0, import_node_process.exit)(1);
48
+ // src/generator/schema/program.ts
49
+ function createSchemaProgram(paths, routeFiles) {
50
+ let options = { ...DEFAULT_OPTIONS };
51
+ const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
52
+ if (configPath) {
53
+ const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
54
+ ...import_typescript.default.sys,
55
+ onUnRecoverableConfigFileDiagnostic: () => {
94
56
  }
57
+ });
58
+ if (parsed) {
59
+ options = { ...parsed.options, noEmit: true };
95
60
  }
96
- import_prompts.log.error(e);
97
- (0, import_node_process.exit)(1);
98
61
  }
99
- };
100
- var safeRegister = async () => {
101
- const { register } = await import("esbuild-register/dist/node");
102
- let res;
103
- try {
104
- res = register({
105
- format: "cjs",
106
- loader: "ts"
107
- });
108
- } catch {
109
- res = {
110
- unregister: () => {
111
- }
62
+ return import_typescript.default.createProgram(routeFiles, options);
63
+ }
64
+ var import_typescript, DEFAULT_OPTIONS;
65
+ var init_program = __esm({
66
+ "src/generator/schema/program.ts"() {
67
+ "use strict";
68
+ import_typescript = __toESM(require("typescript"));
69
+ DEFAULT_OPTIONS = {
70
+ target: import_typescript.default.ScriptTarget.ES2022,
71
+ module: import_typescript.default.ModuleKind.NodeNext,
72
+ moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
73
+ strict: true,
74
+ skipLibCheck: true,
75
+ noEmit: true
112
76
  };
113
77
  }
114
- await assertES5(res.unregister);
115
- return res;
116
- };
117
- var load = async () => {
118
- const defaultTsConfigExists = (0, import_node_fs.existsSync)((0, import_node_path.resolve)("giri.config.ts"));
119
- const defaultJsConfigExists = (0, import_node_fs.existsSync)((0, import_node_path.resolve)("giri.config.js"));
120
- const defaultConfigPath = defaultTsConfigExists ? "giri.config.ts" : defaultJsConfigExists ? "giri.config.js" : void 0;
121
- if (!defaultConfigPath) {
122
- import_prompts.log.error("Config file not found.");
123
- (0, import_node_process.exit)(1);
124
- }
125
- const path = (0, import_node_path.resolve)(defaultConfigPath);
126
- if (!(0, import_node_fs.existsSync)(path)) {
127
- import_prompts.log.error(`${path} file does not exist`);
128
- (0, import_node_process.exit)(1);
129
- }
130
- const { unregister } = await safeRegister();
131
- const required = require(`${path}`);
132
- const content = required.default ?? required;
133
- unregister();
134
- const res = import_value.Value.Check(configSchema, content);
135
- if (!res) {
136
- for (const error of [...import_value.Value.Errors(configSchema, content)]) {
137
- import_prompts.log.error(error.message);
138
- }
139
- (0, import_node_process.exit)(1);
140
- }
141
- return content;
142
- };
78
+ });
143
79
 
144
- // src/routes.ts
145
- var import_node_fs2 = require("fs");
146
- var import_promises = require("fs/promises");
147
- var import_node_path2 = require("path");
148
- var import_tinyglobby = require("tinyglobby");
149
- var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
150
- var METHOD_FROM_FILE = new Map(
151
- METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
152
- );
153
- function normalizeSlashes(path) {
154
- return path.split(import_node_path2.sep).join("/");
80
+ // src/generator/schema/json-schema.ts
81
+ function createWalkContext(checker, location) {
82
+ return {
83
+ checker,
84
+ location,
85
+ defs: {},
86
+ inProgress: /* @__PURE__ */ new Map(),
87
+ usedDefs: /* @__PURE__ */ new Set(),
88
+ warnings: []
89
+ };
155
90
  }
156
- function isRouteSourceFile(fileName) {
157
- return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
91
+ function typeId(type) {
92
+ return type.id;
158
93
  }
159
- function methodFromFile(fileName) {
160
- if (!isRouteSourceFile(fileName)) {
161
- return void 0;
162
- }
163
- const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
164
- return METHOD_FROM_FILE.get(stem);
94
+ function intrinsicName(type) {
95
+ return type.intrinsicName;
165
96
  }
166
- function sharedFileIn(dir) {
167
- for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
168
- const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
169
- if ((0, import_node_fs2.existsSync)(file)) {
170
- return file;
97
+ function isDateType(type) {
98
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
99
+ return symbol?.getName() === "Date";
100
+ }
101
+ function literalValuesOf(types) {
102
+ const values = [];
103
+ for (const member of types) {
104
+ if (member.isStringLiteral() || member.isNumberLiteral()) {
105
+ values.push(member.value);
106
+ } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
107
+ values.push(intrinsicName(member) === "true");
108
+ } else {
109
+ return void 0;
171
110
  }
172
111
  }
173
- return void 0;
112
+ return values;
174
113
  }
175
- function physicalRouteSegments(routesDir, routeDir) {
176
- const rel = (0, import_node_path2.relative)(routesDir, routeDir);
177
- if (!rel) {
178
- return [];
114
+ function walkUnion(type, ctx) {
115
+ const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
116
+ const members = type.types.filter((member) => !(member.flags & flag));
117
+ if (members.length === 1) {
118
+ return walkType(members[0], ctx);
179
119
  }
180
- return normalizeSlashes(rel).split("/").filter(Boolean);
120
+ const enumValues = literalValuesOf(members);
121
+ if (enumValues) {
122
+ return { enum: enumValues };
123
+ }
124
+ return { anyOf: members.map((member) => walkType(member, ctx)) };
181
125
  }
182
- function urlSegment(segment) {
183
- if (/^\(.+\)$/.test(segment)) {
184
- return {};
126
+ function buildObjectSchema(type, ctx) {
127
+ const { checker } = ctx;
128
+ const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
129
+ const properties = {};
130
+ const required = [];
131
+ for (const symbol of checker.getPropertiesOfType(type)) {
132
+ const name = symbol.getName();
133
+ const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
134
+ const optional = Boolean(symbol.getFlags() & import_typescript2.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript2.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript2.default.TypeFlags.Undefined));
135
+ properties[name] = walkType(propType, ctx);
136
+ if (!optional) {
137
+ required.push(name);
138
+ }
185
139
  }
186
- const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
187
- if (catchAll) {
188
- const name = catchAll[1];
189
- return {
190
- value: `:${name}{.*}`,
191
- param: { name, catchAll: true }
192
- };
140
+ const schema = { type: "object" };
141
+ if (Object.keys(properties).length > 0) {
142
+ schema.properties = properties;
193
143
  }
194
- const param = /^\[(.+)\]$/.exec(segment);
195
- if (param) {
196
- const name = param[1];
197
- return {
198
- value: `:${name}`,
199
- param: { name, catchAll: false }
200
- };
144
+ if (required.length > 0) {
145
+ schema.required = required;
201
146
  }
202
- return { value: segment };
203
- }
204
- function pathFromSegments(segments) {
205
- const pathSegments = [];
206
- const params = [];
207
- for (const segment of segments) {
208
- const converted = urlSegment(segment);
209
- if (converted.value) {
210
- pathSegments.push(converted.value);
211
- }
212
- if (converted.param) {
213
- params.push(converted.param);
214
- }
147
+ if (indexInfo) {
148
+ schema.additionalProperties = walkType(indexInfo.type, ctx);
149
+ } else if (Object.keys(properties).length > 0) {
150
+ schema.additionalProperties = false;
215
151
  }
216
- return {
217
- path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
218
- params
219
- };
152
+ return schema;
220
153
  }
221
- async function scanRouteFolders(routesDir) {
222
- if (!(0, import_node_fs2.existsSync)(routesDir)) {
223
- return [];
154
+ function defName(type) {
155
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
156
+ const name = symbol?.getName();
157
+ if (name && name !== "__type" && name !== "__object") {
158
+ return name;
224
159
  }
225
- const folders = [routesDir];
226
- const walk = async (dir) => {
227
- for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
228
- if (entry.isDirectory() && entry.name !== "node_modules") {
229
- const full = (0, import_node_path2.join)(dir, entry.name);
230
- folders.push(full);
231
- await walk(full);
232
- }
233
- }
234
- };
235
- await walk(routesDir);
236
- return folders;
237
- }
238
- function routeParamsForDir(routesDir, dir) {
239
- return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
160
+ return `Anonymous${typeId(type)}`;
240
161
  }
241
- function sharedFilesForDir(routesDir, dir) {
242
- const segments = physicalRouteSegments(routesDir, dir);
243
- const dirs = [routesDir];
244
- let current = routesDir;
245
- for (const segment of segments) {
246
- current = (0, import_node_path2.join)(current, segment);
247
- dirs.push(current);
162
+ function walkObject(type, ctx) {
163
+ const { checker } = ctx;
164
+ if (isDateType(type)) {
165
+ return { type: "string", format: "date-time" };
248
166
  }
249
- return dirs.map(sharedFileIn).filter((file) => Boolean(file));
250
- }
251
- async function scanRoutes(routesDir) {
252
- if (!(0, import_node_fs2.existsSync)(routesDir)) {
253
- return [];
167
+ if (checker.isArrayType(type)) {
168
+ const [element] = checker.getTypeArguments(type);
169
+ return { type: "array", items: element ? walkType(element, ctx) : {} };
254
170
  }
255
- const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
256
- cwd: routesDir,
257
- absolute: true,
258
- onlyFiles: true
259
- });
260
- const routes = [];
261
- for (const file of files) {
262
- const method = methodFromFile((0, import_node_path2.basename)(file));
263
- if (!method) {
264
- continue;
265
- }
266
- const routeDir = (0, import_node_path2.dirname)(file);
267
- const routeSegments = physicalRouteSegments(routesDir, routeDir);
268
- const { path, params } = pathFromSegments(routeSegments);
269
- routes.push({
270
- method,
271
- path,
272
- file,
273
- routeDir,
274
- routeSegments,
275
- params,
276
- sharedFiles: sharedFilesForDir(routesDir, routeDir)
277
- });
171
+ if (checker.isTupleType(type)) {
172
+ const elements = checker.getTypeArguments(type);
173
+ return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
278
174
  }
279
- return routes.sort((left, right) => {
280
- const pathOrder = left.path.localeCompare(right.path);
281
- if (pathOrder !== 0) {
282
- return pathOrder;
283
- }
284
- return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
285
- });
286
- }
287
-
288
- // src/types.ts
289
- var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
290
- var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
291
-
292
- // src/validation.ts
293
- function isGiriInputSchema(value) {
294
- return Boolean(
295
- value && typeof value === "object" && value[inputSchemaBrand] === true
296
- );
297
- }
298
- function isGiriBodySchema(value) {
299
- return Boolean(
300
- value && typeof value === "object" && value[bodySchemaBrand] === true
301
- );
302
- }
303
-
304
- // src/app.ts
305
- function loadModule(file) {
306
- const resolved = require.resolve(file);
307
- delete require.cache[resolved];
308
- return require(resolved);
309
- }
310
- function interopDefault(value) {
311
- if (value && typeof value === "object" && "default" in value) {
312
- return value.default;
175
+ const id = typeId(type);
176
+ const existing = ctx.inProgress.get(id);
177
+ if (existing) {
178
+ ctx.usedDefs.add(existing);
179
+ return { $ref: `#/$defs/${existing}` };
313
180
  }
314
- return value;
181
+ const name = defName(type);
182
+ ctx.inProgress.set(id, name);
183
+ const schema = buildObjectSchema(type, ctx);
184
+ ctx.inProgress.delete(id);
185
+ if (ctx.usedDefs.has(name)) {
186
+ ctx.defs[name] = schema;
187
+ return { $ref: `#/$defs/${name}` };
188
+ }
189
+ return schema;
315
190
  }
316
- function normalizeMiddleware(value, file) {
317
- const exported = interopDefault(value);
318
- if (exported === void 0) {
319
- return [];
191
+ function walkType(type, ctx) {
192
+ const flags = type.flags;
193
+ if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
194
+ return {};
320
195
  }
321
- if (typeof exported === "function") {
322
- return [exported];
196
+ if (flags & import_typescript2.default.TypeFlags.Null) {
197
+ return { type: "null" };
323
198
  }
324
- if (Array.isArray(exported)) {
325
- for (const middleware of exported) {
326
- if (typeof middleware !== "function") {
327
- throw new Error(`Middleware export in ${file} must contain only functions.`);
328
- }
329
- }
330
- return exported;
199
+ if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
200
+ return {};
331
201
  }
332
- throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
333
- }
334
- function assertBodySchema(value, file) {
335
- if (!isGiriBodySchema(value)) {
336
- throw new Error(
337
- `${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
338
- );
202
+ if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
203
+ ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
204
+ return { type: "string" };
339
205
  }
340
- }
341
- function assertQuerySchema(value, file) {
342
- if (!isGiriInputSchema(value)) {
343
- throw new Error(
344
- `${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
345
- );
206
+ if (type.isStringLiteral()) {
207
+ return { type: "string", const: type.value };
346
208
  }
347
- }
348
- function routeInput(routeModule, file) {
349
- const input = {};
350
- if (routeModule.body !== void 0) {
351
- assertBodySchema(routeModule.body, file);
352
- input.body = routeModule.body;
209
+ if (type.isNumberLiteral()) {
210
+ return { type: "number", const: type.value };
353
211
  }
354
- if (routeModule.query !== void 0) {
355
- assertQuerySchema(routeModule.query, file);
356
- input.query = routeModule.query;
212
+ if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
213
+ return { type: "boolean", const: intrinsicName(type) === "true" };
357
214
  }
358
- return input.body || input.query ? input : void 0;
359
- }
360
- function aliasValues(value) {
361
- return Array.isArray(value) ? value : [value];
362
- }
363
- function resolveAliasTarget(cwd, target, capture = "") {
364
- const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
365
- return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
366
- }
367
- function matchAlias(request, key) {
368
- if (key.includes("*")) {
369
- const [prefix2, suffix = ""] = key.split("*");
370
- if (request.startsWith(prefix2) && request.endsWith(suffix)) {
371
- return request.slice(prefix2.length, request.length - suffix.length);
372
- }
373
- return void 0;
215
+ if (flags & import_typescript2.default.TypeFlags.String) {
216
+ return { type: "string" };
374
217
  }
375
- if (request === key) {
376
- return "";
218
+ if (flags & import_typescript2.default.TypeFlags.Number) {
219
+ return { type: "number" };
377
220
  }
378
- const prefix = `${key}/`;
379
- if (request.startsWith(prefix)) {
380
- return request.slice(prefix.length);
221
+ if (flags & import_typescript2.default.TypeFlags.Boolean) {
222
+ return { type: "boolean" };
381
223
  }
382
- return void 0;
224
+ if (type.isUnion()) {
225
+ return walkUnion(type, ctx);
226
+ }
227
+ if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
228
+ return walkObject(type, ctx);
229
+ }
230
+ return {};
383
231
  }
384
- function resolveAliasRequest(request, alias, cwd) {
385
- for (const [key, value] of Object.entries(alias ?? {})) {
386
- const capture = matchAlias(request, key);
387
- if (capture === void 0) {
388
- continue;
232
+ var import_typescript2;
233
+ var init_json_schema = __esm({
234
+ "src/generator/schema/json-schema.ts"() {
235
+ "use strict";
236
+ import_typescript2 = __toESM(require("typescript"));
237
+ }
238
+ });
239
+
240
+ // src/generator/schema/responses.ts
241
+ function findHandleFunction(source) {
242
+ let found;
243
+ const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
244
+ for (const statement of source.statements) {
245
+ if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
246
+ found = statement;
389
247
  }
390
- const [target] = aliasValues(value);
391
- if (!target) {
392
- continue;
248
+ if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
249
+ for (const declaration of statement.declarationList.declarations) {
250
+ if (import_typescript3.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript3.default.isArrowFunction(declaration.initializer) || import_typescript3.default.isFunctionExpression(declaration.initializer))) {
251
+ found = declaration.initializer;
252
+ }
253
+ }
393
254
  }
394
- return resolveAliasTarget(cwd, target, capture);
395
255
  }
396
- return void 0;
256
+ return found;
397
257
  }
398
- function registerAliasResolver(alias, cwd) {
399
- if (!alias || Object.keys(alias).length === 0) {
400
- return () => {
401
- };
258
+ function collectReturnExpressions(fn) {
259
+ if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
260
+ return [fn.body];
402
261
  }
403
- const moduleWithResolver = import_node_module.default;
404
- const originalResolveFilename = moduleWithResolver._resolveFilename;
405
- moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
406
- return originalResolveFilename.call(
407
- this,
408
- resolveAliasRequest(request, alias, cwd) ?? request,
409
- parent,
410
- isMain,
411
- options
412
- );
413
- };
414
- return () => {
415
- moduleWithResolver._resolveFilename = originalResolveFilename;
416
- };
417
- }
418
- var GIRI_ALIAS_PREFIX = "$giri/";
419
- var giriOutDir;
420
- var giriResolverInstalled = false;
421
- function ensureGiriAliasResolver(outDir) {
422
- giriOutDir = outDir;
423
- if (giriResolverInstalled) {
424
- return;
262
+ if (!fn.body) {
263
+ return [];
425
264
  }
426
- giriResolverInstalled = true;
427
- const moduleWithResolver = import_node_module.default;
428
- const originalResolveFilename = moduleWithResolver._resolveFilename;
429
- moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
430
- const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
431
- return originalResolveFilename.call(this, mapped, parent, isMain, options);
265
+ const expressions = [];
266
+ const visit = (node) => {
267
+ if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
268
+ return;
269
+ }
270
+ if (import_typescript3.default.isReturnStatement(node) && node.expression) {
271
+ expressions.push(node.expression);
272
+ }
273
+ import_typescript3.default.forEachChild(node, visit);
432
274
  };
275
+ import_typescript3.default.forEachChild(fn.body, visit);
276
+ return expressions;
433
277
  }
434
- function resolveGiriPaths(config, cwd = process.cwd()) {
435
- return {
436
- cwd: (0, import_node_path3.resolve)(cwd),
437
- routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
438
- outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
439
- };
440
- }
441
- async function buildGiriApp(config, options = {}) {
442
- const paths = resolveGiriPaths(config, options.cwd);
443
- const routes = await scanRoutes(paths.routesDir);
444
- const app = config.adapter.createApp();
445
- ensureGiriAliasResolver(paths.outDir);
446
- const { unregister } = await safeRegister();
447
- const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
448
- try {
449
- for (const route of routes) {
450
- const routeModule = loadModule(route.file);
451
- if (typeof routeModule.handle !== "function") {
452
- throw new Error(`${route.file} must export a named handle function.`);
453
- }
454
- const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
455
- (file) => normalizeMiddleware(loadModule(file).middleware, file)
456
- );
457
- const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
458
- config.adapter.register(app, {
459
- method: route.method,
460
- path: route.path,
461
- handle: routeModule.handle,
462
- middleware: [...folderMiddleware, ...verbMiddleware],
463
- input: routeInput(routeModule, route.file),
464
- services: options.services
465
- });
466
- }
467
- } finally {
468
- unregisterAliasResolver();
469
- unregister();
470
- }
471
- return { app, routes, paths };
278
+ function propertyType(checker, type, name, location) {
279
+ const symbol = checker.getPropertyOfType(type, name);
280
+ return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
472
281
  }
473
-
474
- // src/generator/sync.ts
475
- var import_node_fs6 = require("fs");
476
- var import_promises3 = require("fs/promises");
477
- var import_node_path11 = require("path");
478
-
479
- // src/generator/app-types.ts
480
- var import_node_fs4 = require("fs");
481
- var import_node_path5 = require("path");
482
-
483
- // src/generator/util.ts
484
- var import_node_fs3 = require("fs");
485
- var import_promises2 = require("fs/promises");
486
- var import_node_path4 = require("path");
487
- var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
488
- function slash(path) {
489
- return path.split(import_node_path4.sep).join("/");
282
+ function isTypedResponse(checker, type) {
283
+ return Boolean(
284
+ checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
285
+ );
490
286
  }
491
- function importPath(fromFile, toFile) {
492
- let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
493
- if (!path.startsWith(".")) {
494
- path = `./${path}`;
287
+ function readFromCall(checker, expression) {
288
+ if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
289
+ return void 0;
495
290
  }
496
- return path;
497
- }
498
- function relativeConfigPath(fromDir, toPath) {
499
- let path = slash((0, import_node_path4.relative)(fromDir, toPath));
500
- if (!path.startsWith(".")) {
501
- path = `./${path}`;
291
+ const method = expression.expression.name.text;
292
+ if (method !== "json" && method !== "text") {
293
+ return void 0;
502
294
  }
503
- return path;
504
- }
505
- function moduleSpecifier(fromDir, target) {
506
- let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
507
- if (!path.startsWith(".")) {
508
- path = `./${path}`;
295
+ if (!isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
296
+ return void 0;
509
297
  }
510
- return path;
511
- }
512
- function typeFilePath(paths, routeDir) {
513
- const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
514
- return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
515
- }
516
- function assertSafeOutDir(paths) {
517
- const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
518
- if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
519
- throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
298
+ const [dataArg, statusArg] = expression.arguments;
299
+ if (!dataArg) {
300
+ return void 0;
301
+ }
302
+ let status = 200;
303
+ if (statusArg) {
304
+ const statusType = checker.getTypeAtLocation(statusArg);
305
+ status = statusType.isNumberLiteral() ? statusType.value : "default";
520
306
  }
307
+ return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
521
308
  }
522
- var writeCache = /* @__PURE__ */ new Map();
523
- async function writeGenerated(path, content) {
524
- if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
525
- return;
309
+ function readFromType(checker, type, location) {
310
+ const dataType = propertyType(checker, type, "data", location);
311
+ const statusType = propertyType(checker, type, "status", location);
312
+ const formatType = propertyType(checker, type, "format", location);
313
+ if (!dataType || !statusType || !formatType) {
314
+ return void 0;
526
315
  }
527
- await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
528
- await (0, import_promises2.writeFile)(path, content);
529
- writeCache.set(path, content);
316
+ const status = statusType.isNumberLiteral() ? statusType.value : "default";
317
+ const format = formatType.isStringLiteral() && formatType.value === "text" ? "text" : "json";
318
+ return { status, format, data: dataType };
530
319
  }
531
- async function writeJson(path, value) {
532
- await writeGenerated(path, `${JSON.stringify(value, null, 2)}
533
- `);
320
+ function constituents(type) {
321
+ return type.isUnion() ? type.types : [type];
534
322
  }
535
- async function pruneDir(dir, keep) {
536
- let entries;
537
- try {
538
- entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
539
- } catch {
540
- return;
323
+ function extractRouteResponses(program, file) {
324
+ const result = { responses: [], opaque: false, warnings: [], $defs: {} };
325
+ const source = program.getSourceFile(file);
326
+ if (!source) {
327
+ return result;
541
328
  }
542
- for (const entry of entries) {
543
- const full = (0, import_node_path4.join)(dir, entry.name);
544
- if (entry.isDirectory()) {
545
- await pruneDir(full, keep);
546
- await (0, import_promises2.rmdir)(full).catch(() => {
547
- });
548
- } else if (!keep.has(full)) {
549
- await (0, import_promises2.rm)(full, { force: true });
550
- writeCache.delete(full);
551
- }
329
+ const checker = program.getTypeChecker();
330
+ const fn = findHandleFunction(source);
331
+ if (!fn) {
332
+ return result;
552
333
  }
553
- }
554
-
555
- // src/generator/app-types.ts
556
- var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
557
- function findMainFile(cwd) {
558
- for (const ext of MAIN_EXTENSIONS) {
559
- const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
560
- if ((0, import_node_fs4.existsSync)(file)) {
561
- return file;
334
+ const ctx = createWalkContext(checker, fn);
335
+ const byStatus = /* @__PURE__ */ new Map();
336
+ const record = (hit) => {
337
+ const schema = walkType(hit.data, ctx);
338
+ const bucket = byStatus.get(hit.status) ?? { format: hit.format, schemas: [] };
339
+ bucket.schemas.push(schema);
340
+ byStatus.set(hit.status, bucket);
341
+ };
342
+ for (const expression of collectReturnExpressions(fn)) {
343
+ const fromCall = readFromCall(checker, expression);
344
+ if (fromCall) {
345
+ record(fromCall);
346
+ continue;
347
+ }
348
+ let matched = false;
349
+ for (const member of constituents(checker.getTypeAtLocation(expression))) {
350
+ const hit = readFromType(checker, member, expression);
351
+ if (hit) {
352
+ record(hit);
353
+ matched = true;
354
+ }
355
+ }
356
+ if (!matched) {
357
+ result.opaque = true;
562
358
  }
563
359
  }
564
- return void 0;
565
- }
566
- function moduleSpecifier2(fromDir, target) {
567
- let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
568
- if (!path.startsWith(".")) {
569
- path = `./${path}`;
360
+ for (const [status, { format, schemas }] of byStatus) {
361
+ const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
362
+ result.responses.push({ status, format, schema });
570
363
  }
571
- return path;
364
+ result.responses.sort((a, b) => Number(a.status) - Number(b.status));
365
+ result.warnings = ctx.warnings;
366
+ result.$defs = ctx.defs;
367
+ return result;
572
368
  }
573
- async function writeAppTypes(paths) {
574
- const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
575
- const mainFile = findMainFile(paths.cwd);
576
- if (!mainFile) {
577
- await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
578
- return;
369
+ var import_typescript3;
370
+ var init_responses = __esm({
371
+ "src/generator/schema/responses.ts"() {
372
+ "use strict";
373
+ import_typescript3 = __toESM(require("typescript"));
374
+ init_json_schema();
579
375
  }
580
- const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
581
- await writeGenerated(
582
- file,
583
- [
584
- GENERATED_HEADER,
585
- "declare global {",
586
- " namespace Giri {",
587
- " interface Register {",
588
- ` app: typeof import(${JSON.stringify(spec)}) extends {`,
589
- " init: (...args: any[]) => infer R;",
590
- " }",
591
- " ? Awaited<R>",
592
- " : Record<string, unknown>;",
593
- " }",
594
- " }",
595
- "}",
596
- "export {};",
597
- ""
598
- ].join("\n")
599
- );
600
- }
376
+ });
601
377
 
602
- // src/generator/manifest.ts
603
- var import_node_path6 = require("path");
604
- async function writeManifest(paths, routes, data = {}) {
605
- const manifest = {
606
- version: 1,
607
- routes: routes.map((route) => {
608
- const responses = data.responsesByFile?.get(route.file);
609
- const input = data.inputsByFile?.get(route.file);
610
- const security = data.securityByFile?.get(route.file);
611
- return {
612
- method: route.method,
613
- path: route.path,
614
- file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
615
- params: route.params,
616
- shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
617
- types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
618
- ...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
619
- ...input ? { input } : {},
620
- ...security && security.security.length > 0 ? { security: security.security } : {},
621
- responses: responses?.responses ?? [],
622
- ...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
623
- };
624
- })
625
- };
626
- await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
627
- }
378
+ // src/generator/schema/index.ts
379
+ var schema_exports = {};
380
+ __export(schema_exports, {
381
+ createSchemaProgram: () => createSchemaProgram,
382
+ createWalkContext: () => createWalkContext,
383
+ extractRouteResponses: () => extractRouteResponses,
384
+ walkType: () => walkType
385
+ });
386
+ var init_schema = __esm({
387
+ "src/generator/schema/index.ts"() {
388
+ "use strict";
389
+ init_program();
390
+ init_responses();
391
+ init_json_schema();
392
+ }
393
+ });
628
394
 
629
- // src/generator/openapi.ts
630
- var import_node_fs5 = require("fs");
631
- var import_node_path7 = require("path");
632
- var REASON = {
633
- 200: "OK",
634
- 201: "Created",
635
- 202: "Accepted",
636
- 204: "No Content",
637
- 400: "Bad Request",
638
- 401: "Unauthorized",
639
- 403: "Forbidden",
640
- 404: "Not Found",
641
- 409: "Conflict",
642
- 422: "Unprocessable Entity",
643
- 500: "Internal Server Error"
395
+ // src/cli.ts
396
+ var import_node_child_process = require("child_process");
397
+ var import_node_fs9 = require("fs");
398
+ var import_promises4 = require("fs/promises");
399
+ var import_node_path15 = require("path");
400
+ var prompts = __toESM(require("@clack/prompts"));
401
+
402
+ // src/app.ts
403
+ var import_node_module = __toESM(require("module"));
404
+ var import_node_path3 = require("path");
405
+
406
+ // src/loader/loader.ts
407
+ var import_prompts = require("@clack/prompts");
408
+ var import_node_fs = require("fs");
409
+ var import_node_path = require("path");
410
+ var import_node_process = require("process");
411
+
412
+ // src/config/schema.ts
413
+ var import_typebox = require("@sinclair/typebox");
414
+ var configSchema = import_typebox.Type.Object({
415
+ adapter: import_typebox.Type.Any(),
416
+ alias: import_typebox.Type.Optional(import_typebox.Type.Record(
417
+ import_typebox.Type.String(),
418
+ import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
419
+ )),
420
+ outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
421
+ server: import_typebox.Type.Optional(import_typebox.Type.Object({
422
+ port: import_typebox.Type.Optional(import_typebox.Type.Number()),
423
+ hostname: import_typebox.Type.Optional(import_typebox.Type.String())
424
+ }, { additionalProperties: false })),
425
+ errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
426
+ }, { additionalProperties: false });
427
+
428
+ // src/loader/loader.ts
429
+ var import_value = require("@sinclair/typebox/value");
430
+ var assertES5 = async (unregister) => {
431
+ try {
432
+ init_es5();
433
+ } catch (e) {
434
+ if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
435
+ const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
436
+ if (es5Error) {
437
+ import_prompts.log.error(
438
+ `Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
439
+ );
440
+ (0, import_node_process.exit)(1);
441
+ }
442
+ }
443
+ import_prompts.log.error(e);
444
+ (0, import_node_process.exit)(1);
445
+ }
644
446
  };
645
- function toOpenApiPath(path) {
646
- return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
447
+ var safeRegister = async () => {
448
+ const { register } = await import("esbuild-register/dist/node");
449
+ let res;
450
+ try {
451
+ res = register({
452
+ format: "cjs",
453
+ loader: "ts"
454
+ });
455
+ } catch {
456
+ res = {
457
+ unregister: () => {
458
+ }
459
+ };
460
+ }
461
+ await assertES5(res.unregister);
462
+ return res;
463
+ };
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);
471
+ }
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`);
475
+ (0, import_node_process.exit)(1);
476
+ }
477
+ const { unregister } = await safeRegister();
478
+ const required = require(`${path}`);
479
+ const content = required.default ?? required;
480
+ unregister();
481
+ const res = import_value.Value.Check(configSchema, content);
482
+ if (!res) {
483
+ for (const error of [...import_value.Value.Errors(configSchema, content)]) {
484
+ import_prompts.log.error(error.message);
485
+ }
486
+ (0, import_node_process.exit)(1);
487
+ }
488
+ return content;
489
+ };
490
+
491
+ // src/routes.ts
492
+ var import_node_fs2 = require("fs");
493
+ var import_promises = require("fs/promises");
494
+ var import_node_path2 = require("path");
495
+ var import_tinyglobby = require("tinyglobby");
496
+ var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
497
+ var METHOD_FROM_FILE = new Map(
498
+ METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
499
+ );
500
+ function normalizeSlashes(path) {
501
+ return path.split(import_node_path2.sep).join("/");
647
502
  }
648
- function rewriteRefs(value) {
649
- if (Array.isArray(value)) {
650
- return value.map(rewriteRefs);
503
+ function isRouteSourceFile(fileName) {
504
+ return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
505
+ }
506
+ function methodFromFile(fileName) {
507
+ if (!isRouteSourceFile(fileName)) {
508
+ return void 0;
651
509
  }
652
- if (value && typeof value === "object") {
653
- const out = {};
654
- for (const [key, child] of Object.entries(value)) {
655
- if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
656
- out.$ref = child.replace("#/$defs/", "#/components/schemas/");
657
- } else {
658
- out[key] = rewriteRefs(child);
659
- }
510
+ const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
511
+ return METHOD_FROM_FILE.get(stem);
512
+ }
513
+ function sharedFileIn(dir) {
514
+ for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
515
+ const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
516
+ if ((0, import_node_fs2.existsSync)(file)) {
517
+ return file;
660
518
  }
661
- return out;
662
519
  }
663
- return value;
520
+ return void 0;
664
521
  }
665
- function mediaTypeFor(format) {
666
- return format === "text" ? "text/plain" : "application/json";
522
+ function physicalRouteSegments(routesDir, routeDir) {
523
+ const rel = (0, import_node_path2.relative)(routesDir, routeDir);
524
+ if (!rel) {
525
+ return [];
526
+ }
527
+ return normalizeSlashes(rel).split("/").filter(Boolean);
667
528
  }
668
- var BODY_MEDIA_TYPE = {
669
- json: "application/json",
670
- form: "multipart/form-data",
671
- urlencoded: "application/x-www-form-urlencoded",
672
- text: "text/plain"
673
- };
674
- function buildResponses(responses) {
675
- if (responses.length === 0) {
676
- return { default: { description: "Response" } };
529
+ function urlSegment(segment) {
530
+ if (/^\(.+\)$/.test(segment)) {
531
+ return {};
677
532
  }
678
- const out = {};
679
- for (const response of responses) {
680
- const key = response.status === "default" ? "default" : String(response.status);
681
- const description = typeof response.status === "number" && REASON[response.status] || "Response";
682
- out[key] = {
683
- description,
684
- content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
533
+ const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
534
+ if (catchAll) {
535
+ const name = catchAll[1];
536
+ return {
537
+ value: `:${name}{.*}`,
538
+ param: { name, catchAll: true }
685
539
  };
686
540
  }
687
- return out;
541
+ const param = /^\[(.+)\]$/.exec(segment);
542
+ if (param) {
543
+ const name = param[1];
544
+ return {
545
+ value: `:${name}`,
546
+ param: { name, catchAll: false }
547
+ };
548
+ }
549
+ return { value: segment };
688
550
  }
689
- function pathParameters(route) {
690
- const seen = /* @__PURE__ */ new Set();
551
+ function pathFromSegments(segments) {
552
+ const pathSegments = [];
691
553
  const params = [];
692
- for (const param of route.params) {
693
- if (seen.has(param.name)) {
694
- continue;
554
+ for (const segment of segments) {
555
+ const converted = urlSegment(segment);
556
+ if (converted.value) {
557
+ pathSegments.push(converted.value);
558
+ }
559
+ if (converted.param) {
560
+ params.push(converted.param);
695
561
  }
696
- seen.add(param.name);
697
- params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
698
562
  }
699
- return params;
563
+ return {
564
+ path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
565
+ params
566
+ };
700
567
  }
701
- function queryParameters(query) {
702
- if (!query || query.type !== "object" || typeof query.properties !== "object") {
568
+ async function scanRouteFolders(routesDir) {
569
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
703
570
  return [];
704
571
  }
705
- const properties = query.properties;
706
- const required = Array.isArray(query.required) ? query.required : [];
707
- return Object.entries(properties).map(([name, schema]) => ({
708
- name,
709
- in: "query",
710
- required: required.includes(name),
711
- schema: rewriteRefs(schema)
712
- }));
713
- }
714
- function readProjectInfo(cwd) {
715
- const file = (0, import_node_path7.join)(cwd, "package.json");
716
- if ((0, import_node_fs5.existsSync)(file)) {
717
- try {
718
- const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
719
- return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
720
- } catch {
721
- }
722
- }
723
- return { title: "giri API", version: "0.0.0" };
724
- }
725
- function buildOpenApiDocument(paths, routes, data = {}) {
726
- const documentPaths = {};
727
- const schemas = {};
728
- const securitySchemes = {};
729
- for (const route of routes) {
730
- if (data.hiddenFiles?.has(route.file)) {
731
- continue;
732
- }
733
- const responses = data.responsesByFile?.get(route.file);
734
- const input = data.inputsByFile?.get(route.file);
735
- const security = data.securityByFile?.get(route.file);
736
- for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
737
- schemas[name] = rewriteRefs(schema);
738
- }
739
- const operation = { responses: buildResponses(responses?.responses ?? []) };
740
- const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
741
- if (parameters.length > 0) {
742
- operation.parameters = parameters;
743
- }
744
- if (input?.body) {
745
- const content = {};
746
- for (const [contentType, schema] of Object.entries(input.body)) {
747
- content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
748
- schema: rewriteRefs(schema)
749
- };
750
- }
751
- if (Object.keys(content).length > 0) {
752
- operation.requestBody = { required: true, content };
572
+ const folders = [routesDir];
573
+ const walk = async (dir) => {
574
+ for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
575
+ if (entry.isDirectory() && entry.name !== "node_modules") {
576
+ const full = (0, import_node_path2.join)(dir, entry.name);
577
+ folders.push(full);
578
+ await walk(full);
753
579
  }
754
580
  }
755
- if (security && security.security.length > 0) {
756
- operation.security = security.security;
757
- }
758
- if (security) {
759
- Object.assign(securitySchemes, security.securitySchemes);
760
- }
761
- const openApiPath = toOpenApiPath(route.path);
762
- const pathItem = documentPaths[openApiPath] ?? {};
763
- pathItem[route.method.toLowerCase()] = operation;
764
- documentPaths[openApiPath] = pathItem;
765
- }
766
- const document = {
767
- openapi: "3.1.0",
768
- info: readProjectInfo(paths.cwd),
769
- paths: documentPaths
770
581
  };
771
- const components = {};
772
- if (Object.keys(schemas).length > 0) {
773
- components.schemas = schemas;
774
- }
775
- if (Object.keys(securitySchemes).length > 0) {
776
- components.securitySchemes = securitySchemes;
777
- }
778
- if (Object.keys(components).length > 0) {
779
- document.components = components;
780
- }
781
- return document;
582
+ await walk(routesDir);
583
+ return folders;
782
584
  }
783
- async function writeOpenApi(paths, routes, data = {}) {
784
- await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
585
+ function routeParamsForDir(routesDir, dir) {
586
+ return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
785
587
  }
786
-
787
- // src/generator/param-types.ts
788
- var import_node_path8 = require("path");
789
- function paramsType(params) {
790
- if (params.length === 0) {
791
- return "{}";
792
- }
793
- const unique = /* @__PURE__ */ new Map();
794
- for (const param of params) {
795
- unique.set(param.name, param);
588
+ function sharedFilesForDir(routesDir, dir) {
589
+ const segments = physicalRouteSegments(routesDir, dir);
590
+ const dirs = [routesDir];
591
+ let current = routesDir;
592
+ for (const segment of segments) {
593
+ current = (0, import_node_path2.join)(current, segment);
594
+ dirs.push(current);
796
595
  }
797
- const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
798
- return `{
799
- ${fields}
800
- }`;
596
+ return dirs.map(sharedFileIn).filter((file) => Boolean(file));
801
597
  }
802
- function varsType(typesDir, sharedFiles) {
803
- if (sharedFiles.length === 0) {
804
- return "{}";
598
+ async function scanRoutes(routesDir) {
599
+ if (!(0, import_node_fs2.existsSync)(routesDir)) {
600
+ return [];
805
601
  }
806
- return sharedFiles.map((file) => {
807
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
808
- return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
809
- }).join("\n & ");
810
- }
811
- function methodExports(typesDir, verbs) {
812
- return verbs.map(({ method, file }) => {
813
- const spec = JSON.stringify(moduleSpecifier(typesDir, file));
814
- const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
815
- const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
816
- return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
602
+ const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
603
+ cwd: routesDir,
604
+ absolute: true,
605
+ onlyFiles: true
817
606
  });
818
- }
819
- async function writeParamTypes(paths, folders) {
820
- for (const { dir, params, sharedFiles, verbs } of folders) {
821
- const file = typeFilePath(paths, dir);
822
- const typesDir = (0, import_node_path8.dirname)(file);
823
- const lines = [
824
- GENERATED_HEADER,
825
- `export type Params = ${paramsType(params)};`,
826
- "export type RouteParams = Params;",
827
- `type Vars = ${varsType(typesDir, sharedFiles)};`,
828
- "export type Middleware<Injects extends Record<string, unknown> = {}> =",
829
- ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
830
- 'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
831
- ' import("@boon4681/giri").Handle<Params, Input, Vars>;'
832
- ];
833
- if (verbs.length > 0) {
834
- lines.push(...methodExports(typesDir, verbs));
607
+ const routes = [];
608
+ for (const file of files) {
609
+ const method = methodFromFile((0, import_node_path2.basename)(file));
610
+ if (!method) {
611
+ continue;
835
612
  }
836
- lines.push("");
837
- await writeGenerated(file, lines.join("\n"));
613
+ const routeDir = (0, import_node_path2.dirname)(file);
614
+ const routeSegments = physicalRouteSegments(routesDir, routeDir);
615
+ const { path, params } = pathFromSegments(routeSegments);
616
+ routes.push({
617
+ method,
618
+ path,
619
+ file,
620
+ routeDir,
621
+ routeSegments,
622
+ params,
623
+ sharedFiles: sharedFilesForDir(routesDir, routeDir)
624
+ });
838
625
  }
626
+ return routes.sort((left, right) => {
627
+ const pathOrder = left.path.localeCompare(right.path);
628
+ if (pathOrder !== 0) {
629
+ return pathOrder;
630
+ }
631
+ return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
632
+ });
839
633
  }
840
634
 
841
- // src/generator/inputs.ts
842
- function sanitize(schema) {
843
- const { $schema, ...rest } = schema;
844
- void $schema;
845
- return rest;
846
- }
847
- function inputToJsonSchema(schema) {
848
- if (!isGiriInputSchema(schema)) {
849
- return void 0;
850
- }
851
- return sanitize(schema.toJsonSchema());
635
+ // src/types.ts
636
+ var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
637
+ var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
638
+
639
+ // src/validation.ts
640
+ function isGiriInputSchema(value) {
641
+ return Boolean(
642
+ value && typeof value === "object" && value[inputSchemaBrand] === true
643
+ );
852
644
  }
853
- function bodyToJsonSchemas(value) {
854
- if (!isGiriBodySchema(value)) {
855
- return void 0;
856
- }
857
- const out = {};
858
- for (const [contentType, schema] of Object.entries(value.contents)) {
859
- const json = inputToJsonSchema(schema);
860
- if (json) {
861
- out[contentType] = json;
862
- }
863
- }
864
- return Object.keys(out).length > 0 ? out : void 0;
645
+ function isGiriBodySchema(value) {
646
+ return Boolean(
647
+ value && typeof value === "object" && value[bodySchemaBrand] === true
648
+ );
865
649
  }
866
650
 
867
- // src/generator/route-meta.ts
868
- function loadModule2(file) {
651
+ // src/app.ts
652
+ function loadModule(file) {
869
653
  const resolved = require.resolve(file);
870
654
  delete require.cache[resolved];
871
655
  return require(resolved);
872
656
  }
873
- function interopDefault2(value) {
657
+ function interopDefault(value) {
874
658
  if (value && typeof value === "object" && "default" in value) {
875
659
  return value.default;
876
660
  }
877
661
  return value;
878
662
  }
879
- function middlewareFunctions(value) {
880
- const exported = interopDefault2(value);
663
+ function normalizeMiddleware(value, file) {
664
+ const exported = interopDefault(value);
665
+ if (exported === void 0) {
666
+ return [];
667
+ }
881
668
  if (typeof exported === "function") {
882
669
  return [exported];
883
670
  }
884
671
  if (Array.isArray(exported)) {
885
- return exported.filter((fn) => typeof fn === "function");
672
+ for (const middleware of exported) {
673
+ if (typeof middleware !== "function") {
674
+ throw new Error(`Middleware export in ${file} must contain only functions.`);
675
+ }
676
+ }
677
+ return exported;
886
678
  }
887
- return [];
679
+ throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
888
680
  }
889
- function readInput(routeModule) {
681
+ function assertBodySchema(value, file) {
682
+ if (!isGiriBodySchema(value)) {
683
+ throw new Error(
684
+ `${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
685
+ );
686
+ }
687
+ }
688
+ function assertQuerySchema(value, file) {
689
+ if (!isGiriInputSchema(value)) {
690
+ throw new Error(
691
+ `${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
692
+ );
693
+ }
694
+ }
695
+ function routeInput(routeModule, file) {
890
696
  const input = {};
891
- const body = bodyToJsonSchemas(routeModule.body);
892
- const query = inputToJsonSchema(routeModule.query);
893
- if (body) {
894
- input.body = body;
697
+ if (routeModule.body !== void 0) {
698
+ assertBodySchema(routeModule.body, file);
699
+ input.body = routeModule.body;
895
700
  }
896
- if (query) {
897
- input.query = query;
701
+ if (routeModule.query !== void 0) {
702
+ assertQuerySchema(routeModule.query, file);
703
+ input.query = routeModule.query;
898
704
  }
899
705
  return input.body || input.query ? input : void 0;
900
706
  }
901
- function hiddenFrom(value) {
902
- if (value === false) {
903
- return true;
707
+ function aliasValues(value) {
708
+ return Array.isArray(value) ? value : [value];
709
+ }
710
+ function resolveAliasTarget(cwd, target, capture = "") {
711
+ const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
712
+ return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
713
+ }
714
+ function matchAlias(request, key) {
715
+ 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);
719
+ }
720
+ return void 0;
904
721
  }
905
- if (value === true) {
906
- return false;
722
+ if (request === key) {
723
+ return "";
907
724
  }
908
- if (value && typeof value === "object" && "hidden" in value) {
909
- return Boolean(value.hidden);
725
+ const prefix = `${key}/`;
726
+ if (request.startsWith(prefix)) {
727
+ return request.slice(prefix.length);
910
728
  }
911
729
  return void 0;
912
730
  }
913
- function collectHidden(route, routeModule, loadShared) {
914
- let hidden = false;
915
- for (const file of route.sharedFiles) {
916
- const opinion = hiddenFrom(loadShared(file).openapi);
917
- if (opinion !== void 0) {
918
- hidden = opinion;
731
+ function resolveAliasRequest(request, alias, cwd) {
732
+ for (const [key, value] of Object.entries(alias ?? {})) {
733
+ const capture = matchAlias(request, key);
734
+ if (capture === void 0) {
735
+ continue;
736
+ }
737
+ const [target] = aliasValues(value);
738
+ if (!target) {
739
+ continue;
919
740
  }
741
+ return resolveAliasTarget(cwd, target, capture);
920
742
  }
921
- const verb = hiddenFrom(routeModule.openapi);
922
- return verb ?? hidden;
743
+ return void 0;
923
744
  }
924
- function collectSecurity(route, routeModule, loadShared) {
925
- const skipInherited = Boolean(
926
- routeModule.config?.skipInherited
927
- );
928
- const middleware = [];
929
- if (!skipInherited) {
930
- for (const file of route.sharedFiles) {
931
- middleware.push(...middlewareFunctions(loadShared(file).middleware));
932
- }
745
+ function registerAliasResolver(alias, cwd) {
746
+ if (!alias || Object.keys(alias).length === 0) {
747
+ return () => {
748
+ };
933
749
  }
934
- middleware.push(...middlewareFunctions(routeModule.middleware));
935
- const security = [];
936
- const securitySchemes = {};
937
- for (const fn of middleware) {
938
- const openapi = fn.openapi;
939
- if (openapi?.security) {
940
- for (const requirement of openapi.security) {
941
- if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
942
- security.push(requirement);
943
- }
944
- }
945
- }
946
- if (openapi?.securitySchemes) {
947
- Object.assign(securitySchemes, openapi.securitySchemes);
948
- }
750
+ const moduleWithResolver = import_node_module.default;
751
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
752
+ moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
753
+ return originalResolveFilename.call(
754
+ this,
755
+ resolveAliasRequest(request, alias, cwd) ?? request,
756
+ parent,
757
+ isMain,
758
+ options
759
+ );
760
+ };
761
+ return () => {
762
+ moduleWithResolver._resolveFilename = originalResolveFilename;
763
+ };
764
+ }
765
+ var GIRI_ALIAS_PREFIX = "$giri/";
766
+ var giriOutDir;
767
+ var giriResolverInstalled = false;
768
+ function ensureGiriAliasResolver(outDir) {
769
+ giriOutDir = outDir;
770
+ if (giriResolverInstalled) {
771
+ return;
949
772
  }
950
- return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
773
+ giriResolverInstalled = true;
774
+ const moduleWithResolver = import_node_module.default;
775
+ const originalResolveFilename = moduleWithResolver._resolveFilename;
776
+ moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
777
+ const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
778
+ return originalResolveFilename.call(this, mapped, parent, isMain, options);
779
+ };
951
780
  }
952
- async function extractRouteMeta(config, paths, routes) {
953
- const byFile = /* @__PURE__ */ new Map();
954
- const { unregister } = await safeRegister();
955
- const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
956
- const sharedCache = /* @__PURE__ */ new Map();
957
- const loadShared = (file) => {
958
- if (!sharedCache.has(file)) {
959
- try {
960
- sharedCache.set(file, loadModule2(file));
961
- } catch {
962
- sharedCache.set(file, {});
963
- }
964
- }
965
- return sharedCache.get(file);
781
+ function resolveGiriPaths(config, cwd = process.cwd()) {
782
+ return {
783
+ cwd: (0, import_node_path3.resolve)(cwd),
784
+ routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
785
+ outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
966
786
  };
787
+ }
788
+ async function buildGiriApp(config, options = {}) {
789
+ const paths = resolveGiriPaths(config, options.cwd);
790
+ const routes = await scanRoutes(paths.routesDir);
791
+ const app = config.adapter.createApp();
792
+ ensureGiriAliasResolver(paths.outDir);
793
+ const { unregister } = await safeRegister();
794
+ const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
967
795
  try {
968
796
  for (const route of routes) {
969
- try {
970
- const routeModule = loadModule2(route.file);
971
- const meta = {};
972
- const input = readInput(routeModule);
973
- const security = collectSecurity(route, routeModule, loadShared);
974
- const hidden = collectHidden(route, routeModule, loadShared);
975
- if (input) {
976
- meta.input = input;
977
- }
978
- if (security) {
979
- meta.security = security;
980
- }
981
- if (hidden) {
982
- meta.hidden = true;
983
- }
984
- if (meta.input || meta.security || meta.hidden) {
985
- byFile.set(route.file, meta);
986
- }
987
- } catch {
797
+ const routeModule = loadModule(route.file);
798
+ if (typeof routeModule.handle !== "function") {
799
+ throw new Error(`${route.file} must export a named handle function.`);
988
800
  }
801
+ const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
802
+ (file) => normalizeMiddleware(loadModule(file).middleware, file)
803
+ );
804
+ const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
805
+ config.adapter.register(app, {
806
+ method: route.method,
807
+ path: route.path,
808
+ handle: routeModule.handle,
809
+ middleware: [...folderMiddleware, ...verbMiddleware],
810
+ input: routeInput(routeModule, route.file),
811
+ services: options.services
812
+ });
989
813
  }
990
814
  } finally {
991
- unregisterAlias();
815
+ unregisterAliasResolver();
992
816
  unregister();
993
817
  }
994
- return byFile;
818
+ return { app, routes, paths };
995
819
  }
996
820
 
997
- // src/generator/route-types.ts
998
- var import_node_path9 = require("path");
999
- async function writeRouteTypes(paths, routes) {
1000
- const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
1001
- const lines = [
1002
- GENERATED_HEADER,
1003
- "export interface RouteParams {"
1004
- ];
1005
- for (const route of routes) {
1006
- const typeFile = typeFilePath(paths, route.routeDir);
1007
- lines.push(
1008
- ` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
1009
- importPath(file, typeFile)
1010
- )}).Params;`
1011
- );
1012
- }
1013
- lines.push("}", "");
1014
- await writeGenerated(file, lines.join("\n"));
1015
- }
821
+ // src/generator/sync.ts
822
+ var import_node_fs6 = require("fs");
823
+ var import_promises3 = require("fs/promises");
824
+ var import_node_path11 = require("path");
1016
825
 
1017
- // src/generator/schema/program.ts
1018
- var import_typescript = __toESM(require("typescript"));
1019
- var DEFAULT_OPTIONS = {
1020
- target: import_typescript.default.ScriptTarget.ES2022,
1021
- module: import_typescript.default.ModuleKind.NodeNext,
1022
- moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
1023
- strict: true,
1024
- skipLibCheck: true,
1025
- noEmit: true
1026
- };
1027
- function createSchemaProgram(paths, routeFiles) {
1028
- let options = { ...DEFAULT_OPTIONS };
1029
- const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
1030
- if (configPath) {
1031
- const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
1032
- ...import_typescript.default.sys,
1033
- onUnRecoverableConfigFileDiagnostic: () => {
1034
- }
1035
- });
1036
- if (parsed) {
1037
- options = { ...parsed.options, noEmit: true };
1038
- }
826
+ // src/generator/app-types.ts
827
+ var import_node_fs4 = require("fs");
828
+ var import_node_path5 = require("path");
829
+
830
+ // src/generator/util.ts
831
+ var import_node_fs3 = require("fs");
832
+ var import_promises2 = require("fs/promises");
833
+ var import_node_path4 = require("path");
834
+ var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
835
+ function slash(path) {
836
+ return path.split(import_node_path4.sep).join("/");
837
+ }
838
+ function importPath(fromFile, toFile) {
839
+ let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
840
+ if (!path.startsWith(".")) {
841
+ path = `./${path}`;
1039
842
  }
1040
- return import_typescript.default.createProgram(routeFiles, options);
843
+ return path;
1041
844
  }
1042
-
1043
- // src/generator/schema/responses.ts
1044
- var import_typescript3 = __toESM(require("typescript"));
1045
-
1046
- // src/generator/schema/json-schema.ts
1047
- var import_typescript2 = __toESM(require("typescript"));
1048
- function createWalkContext(checker, location) {
1049
- return {
1050
- checker,
1051
- location,
1052
- defs: {},
1053
- inProgress: /* @__PURE__ */ new Map(),
1054
- usedDefs: /* @__PURE__ */ new Set(),
1055
- warnings: []
1056
- };
845
+ function relativeConfigPath(fromDir, toPath) {
846
+ let path = slash((0, import_node_path4.relative)(fromDir, toPath));
847
+ if (!path.startsWith(".")) {
848
+ path = `./${path}`;
849
+ }
850
+ return path;
1057
851
  }
1058
- function typeId(type) {
1059
- return type.id;
852
+ function moduleSpecifier(fromDir, target) {
853
+ let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
854
+ if (!path.startsWith(".")) {
855
+ path = `./${path}`;
856
+ }
857
+ return path;
1060
858
  }
1061
- function intrinsicName(type) {
1062
- return type.intrinsicName;
859
+ function typeFilePath(paths, routeDir) {
860
+ const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
861
+ return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
1063
862
  }
1064
- function isDateType(type) {
1065
- const symbol = type.getSymbol() ?? type.aliasSymbol;
1066
- return symbol?.getName() === "Date";
863
+ function assertSafeOutDir(paths) {
864
+ const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
865
+ if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
866
+ throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
867
+ }
1067
868
  }
1068
- function literalValuesOf(types) {
1069
- const values = [];
1070
- for (const member of types) {
1071
- if (member.isStringLiteral() || member.isNumberLiteral()) {
1072
- values.push(member.value);
1073
- } else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
1074
- values.push(intrinsicName(member) === "true");
1075
- } else {
1076
- return void 0;
1077
- }
869
+ var writeCache = /* @__PURE__ */ new Map();
870
+ async function writeGenerated(path, content) {
871
+ if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
872
+ return;
1078
873
  }
1079
- return values;
874
+ await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
875
+ await (0, import_promises2.writeFile)(path, content);
876
+ writeCache.set(path, content);
1080
877
  }
1081
- function walkUnion(type, ctx) {
1082
- const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
1083
- const members = type.types.filter((member) => !(member.flags & flag));
1084
- if (members.length === 1) {
1085
- return walkType(members[0], ctx);
878
+ async function writeJson(path, value) {
879
+ await writeGenerated(path, `${JSON.stringify(value, null, 2)}
880
+ `);
881
+ }
882
+ async function pruneDir(dir, keep) {
883
+ let entries;
884
+ try {
885
+ entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
886
+ } catch {
887
+ return;
1086
888
  }
1087
- const enumValues = literalValuesOf(members);
1088
- if (enumValues) {
1089
- return { enum: enumValues };
889
+ for (const entry of entries) {
890
+ const full = (0, import_node_path4.join)(dir, entry.name);
891
+ if (entry.isDirectory()) {
892
+ await pruneDir(full, keep);
893
+ await (0, import_promises2.rmdir)(full).catch(() => {
894
+ });
895
+ } else if (!keep.has(full)) {
896
+ await (0, import_promises2.rm)(full, { force: true });
897
+ writeCache.delete(full);
898
+ }
1090
899
  }
1091
- return { anyOf: members.map((member) => walkType(member, ctx)) };
1092
900
  }
1093
- function buildObjectSchema(type, ctx) {
1094
- const { checker } = ctx;
1095
- const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
1096
- const properties = {};
1097
- const required = [];
1098
- for (const symbol of checker.getPropertiesOfType(type)) {
1099
- const name = symbol.getName();
1100
- const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
1101
- 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));
1102
- properties[name] = walkType(propType, ctx);
1103
- if (!optional) {
1104
- required.push(name);
901
+
902
+ // src/generator/app-types.ts
903
+ var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
904
+ function findMainFile(cwd) {
905
+ for (const ext of MAIN_EXTENSIONS) {
906
+ const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
907
+ if ((0, import_node_fs4.existsSync)(file)) {
908
+ return file;
1105
909
  }
1106
910
  }
1107
- const schema = { type: "object" };
1108
- if (Object.keys(properties).length > 0) {
1109
- schema.properties = properties;
911
+ return void 0;
912
+ }
913
+ function moduleSpecifier2(fromDir, target) {
914
+ let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
915
+ if (!path.startsWith(".")) {
916
+ path = `./${path}`;
1110
917
  }
1111
- if (required.length > 0) {
1112
- schema.required = required;
918
+ return path;
919
+ }
920
+ async function writeAppTypes(paths) {
921
+ const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
922
+ const mainFile = findMainFile(paths.cwd);
923
+ if (!mainFile) {
924
+ await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
925
+ return;
926
+ }
927
+ const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
928
+ await writeGenerated(
929
+ file,
930
+ [
931
+ GENERATED_HEADER,
932
+ "declare global {",
933
+ " namespace Giri {",
934
+ " interface Register {",
935
+ ` app: typeof import(${JSON.stringify(spec)}) extends {`,
936
+ " init: (...args: any[]) => infer R;",
937
+ " }",
938
+ " ? Awaited<R>",
939
+ " : Record<string, unknown>;",
940
+ " }",
941
+ " }",
942
+ "}",
943
+ "export {};",
944
+ ""
945
+ ].join("\n")
946
+ );
947
+ }
948
+
949
+ // src/generator/manifest.ts
950
+ var import_node_path6 = require("path");
951
+ async function writeManifest(paths, routes, data = {}) {
952
+ const manifest = {
953
+ version: 1,
954
+ routes: routes.map((route) => {
955
+ const responses = data.responsesByFile?.get(route.file);
956
+ const input = data.inputsByFile?.get(route.file);
957
+ const security = data.securityByFile?.get(route.file);
958
+ return {
959
+ method: route.method,
960
+ path: route.path,
961
+ file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
962
+ params: route.params,
963
+ shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
964
+ types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
965
+ ...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
966
+ ...input ? { input } : {},
967
+ ...security && security.security.length > 0 ? { security: security.security } : {},
968
+ responses: responses?.responses ?? [],
969
+ ...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
970
+ };
971
+ })
972
+ };
973
+ await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
974
+ }
975
+
976
+ // src/generator/openapi.ts
977
+ var import_node_fs5 = require("fs");
978
+ var import_node_path7 = require("path");
979
+ var REASON = {
980
+ 200: "OK",
981
+ 201: "Created",
982
+ 202: "Accepted",
983
+ 204: "No Content",
984
+ 400: "Bad Request",
985
+ 401: "Unauthorized",
986
+ 403: "Forbidden",
987
+ 404: "Not Found",
988
+ 409: "Conflict",
989
+ 422: "Unprocessable Entity",
990
+ 500: "Internal Server Error"
991
+ };
992
+ function toOpenApiPath(path) {
993
+ return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
994
+ }
995
+ function rewriteRefs(value) {
996
+ if (Array.isArray(value)) {
997
+ return value.map(rewriteRefs);
1113
998
  }
1114
- if (indexInfo) {
1115
- schema.additionalProperties = walkType(indexInfo.type, ctx);
1116
- } else if (Object.keys(properties).length > 0) {
1117
- schema.additionalProperties = false;
999
+ if (value && typeof value === "object") {
1000
+ const out = {};
1001
+ for (const [key, child] of Object.entries(value)) {
1002
+ if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
1003
+ out.$ref = child.replace("#/$defs/", "#/components/schemas/");
1004
+ } else {
1005
+ out[key] = rewriteRefs(child);
1006
+ }
1007
+ }
1008
+ return out;
1118
1009
  }
1119
- return schema;
1010
+ return value;
1120
1011
  }
1121
- function defName(type) {
1122
- const symbol = type.getSymbol() ?? type.aliasSymbol;
1123
- const name = symbol?.getName();
1124
- if (name && name !== "__type" && name !== "__object") {
1125
- return name;
1126
- }
1127
- return `Anonymous${typeId(type)}`;
1012
+ function mediaTypeFor(format) {
1013
+ return format === "text" ? "text/plain" : "application/json";
1128
1014
  }
1129
- function walkObject(type, ctx) {
1130
- const { checker } = ctx;
1131
- if (isDateType(type)) {
1132
- return { type: "string", format: "date-time" };
1133
- }
1134
- if (checker.isArrayType(type)) {
1135
- const [element] = checker.getTypeArguments(type);
1136
- return { type: "array", items: element ? walkType(element, ctx) : {} };
1015
+ var BODY_MEDIA_TYPE = {
1016
+ json: "application/json",
1017
+ form: "multipart/form-data",
1018
+ urlencoded: "application/x-www-form-urlencoded",
1019
+ text: "text/plain"
1020
+ };
1021
+ function buildResponses(responses) {
1022
+ if (responses.length === 0) {
1023
+ return { default: { description: "Response" } };
1137
1024
  }
1138
- if (checker.isTupleType(type)) {
1139
- const elements = checker.getTypeArguments(type);
1140
- return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
1025
+ const out = {};
1026
+ for (const response of responses) {
1027
+ const key = response.status === "default" ? "default" : String(response.status);
1028
+ const description = typeof response.status === "number" && REASON[response.status] || "Response";
1029
+ out[key] = {
1030
+ description,
1031
+ content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
1032
+ };
1141
1033
  }
1142
- const id = typeId(type);
1143
- const existing = ctx.inProgress.get(id);
1144
- if (existing) {
1145
- ctx.usedDefs.add(existing);
1146
- return { $ref: `#/$defs/${existing}` };
1034
+ return out;
1035
+ }
1036
+ function pathParameters(route) {
1037
+ const seen = /* @__PURE__ */ new Set();
1038
+ const params = [];
1039
+ for (const param of route.params) {
1040
+ if (seen.has(param.name)) {
1041
+ continue;
1042
+ }
1043
+ seen.add(param.name);
1044
+ params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
1147
1045
  }
1148
- const name = defName(type);
1149
- ctx.inProgress.set(id, name);
1150
- const schema = buildObjectSchema(type, ctx);
1151
- ctx.inProgress.delete(id);
1152
- if (ctx.usedDefs.has(name)) {
1153
- ctx.defs[name] = schema;
1154
- return { $ref: `#/$defs/${name}` };
1046
+ return params;
1047
+ }
1048
+ function queryParameters(query) {
1049
+ if (!query || query.type !== "object" || typeof query.properties !== "object") {
1050
+ return [];
1155
1051
  }
1156
- return schema;
1052
+ const properties = query.properties;
1053
+ const required = Array.isArray(query.required) ? query.required : [];
1054
+ return Object.entries(properties).map(([name, schema]) => ({
1055
+ name,
1056
+ in: "query",
1057
+ required: required.includes(name),
1058
+ schema: rewriteRefs(schema)
1059
+ }));
1157
1060
  }
1158
- function walkType(type, ctx) {
1159
- const flags = type.flags;
1160
- if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
1161
- return {};
1061
+ function readProjectInfo(cwd) {
1062
+ const file = (0, import_node_path7.join)(cwd, "package.json");
1063
+ if ((0, import_node_fs5.existsSync)(file)) {
1064
+ try {
1065
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
1066
+ return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
1067
+ } catch {
1068
+ }
1162
1069
  }
1163
- if (flags & import_typescript2.default.TypeFlags.Null) {
1164
- return { type: "null" };
1070
+ return { title: "giri API", version: "0.0.0" };
1071
+ }
1072
+ function buildOpenApiDocument(paths, routes, data = {}) {
1073
+ const documentPaths = {};
1074
+ const schemas = {};
1075
+ const securitySchemes = {};
1076
+ for (const route of routes) {
1077
+ if (data.hiddenFiles?.has(route.file)) {
1078
+ continue;
1079
+ }
1080
+ const responses = data.responsesByFile?.get(route.file);
1081
+ const input = data.inputsByFile?.get(route.file);
1082
+ const security = data.securityByFile?.get(route.file);
1083
+ for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
1084
+ schemas[name] = rewriteRefs(schema);
1085
+ }
1086
+ const operation = { responses: buildResponses(responses?.responses ?? []) };
1087
+ const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
1088
+ if (parameters.length > 0) {
1089
+ operation.parameters = parameters;
1090
+ }
1091
+ if (input?.body) {
1092
+ const content = {};
1093
+ for (const [contentType, schema] of Object.entries(input.body)) {
1094
+ content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
1095
+ schema: rewriteRefs(schema)
1096
+ };
1097
+ }
1098
+ if (Object.keys(content).length > 0) {
1099
+ operation.requestBody = { required: true, content };
1100
+ }
1101
+ }
1102
+ if (security && security.security.length > 0) {
1103
+ operation.security = security.security;
1104
+ }
1105
+ if (security) {
1106
+ Object.assign(securitySchemes, security.securitySchemes);
1107
+ }
1108
+ const openApiPath = toOpenApiPath(route.path);
1109
+ const pathItem = documentPaths[openApiPath] ?? {};
1110
+ pathItem[route.method.toLowerCase()] = operation;
1111
+ documentPaths[openApiPath] = pathItem;
1165
1112
  }
1166
- if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
1167
- return {};
1113
+ const document = {
1114
+ openapi: "3.1.0",
1115
+ info: readProjectInfo(paths.cwd),
1116
+ paths: documentPaths
1117
+ };
1118
+ const components = {};
1119
+ if (Object.keys(schemas).length > 0) {
1120
+ components.schemas = schemas;
1168
1121
  }
1169
- if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
1170
- ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
1171
- return { type: "string" };
1122
+ if (Object.keys(securitySchemes).length > 0) {
1123
+ components.securitySchemes = securitySchemes;
1172
1124
  }
1173
- if (type.isStringLiteral()) {
1174
- return { type: "string", const: type.value };
1125
+ if (Object.keys(components).length > 0) {
1126
+ document.components = components;
1175
1127
  }
1176
- if (type.isNumberLiteral()) {
1177
- return { type: "number", const: type.value };
1128
+ return document;
1129
+ }
1130
+ async function writeOpenApi(paths, routes, data = {}) {
1131
+ await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
1132
+ }
1133
+
1134
+ // src/generator/param-types.ts
1135
+ var import_node_path8 = require("path");
1136
+ function paramsType(params) {
1137
+ if (params.length === 0) {
1138
+ return "{}";
1178
1139
  }
1179
- if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
1180
- return { type: "boolean", const: intrinsicName(type) === "true" };
1140
+ const unique = /* @__PURE__ */ new Map();
1141
+ for (const param of params) {
1142
+ unique.set(param.name, param);
1181
1143
  }
1182
- if (flags & import_typescript2.default.TypeFlags.String) {
1183
- return { type: "string" };
1144
+ const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
1145
+ return `{
1146
+ ${fields}
1147
+ }`;
1148
+ }
1149
+ function varsType(typesDir, sharedFiles) {
1150
+ if (sharedFiles.length === 0) {
1151
+ return "{}";
1184
1152
  }
1185
- if (flags & import_typescript2.default.TypeFlags.Number) {
1186
- return { type: "number" };
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 & ");
1157
+ }
1158
+ function methodExports(typesDir, verbs) {
1159
+ return verbs.map(({ method, file }) => {
1160
+ const spec = JSON.stringify(moduleSpecifier(typesDir, file));
1161
+ const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
1162
+ const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
1163
+ return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
1164
+ });
1165
+ }
1166
+ async function writeParamTypes(paths, folders) {
1167
+ for (const { dir, params, sharedFiles, verbs } of folders) {
1168
+ const file = typeFilePath(paths, dir);
1169
+ const typesDir = (0, import_node_path8.dirname)(file);
1170
+ const lines = [
1171
+ GENERATED_HEADER,
1172
+ `export type Params = ${paramsType(params)};`,
1173
+ "export type RouteParams = Params;",
1174
+ `type Vars = ${varsType(typesDir, sharedFiles)};`,
1175
+ "export type Middleware<Injects extends Record<string, unknown> = {}> =",
1176
+ ' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
1177
+ 'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
1178
+ ' import("@boon4681/giri").Handle<Params, Input, Vars>;'
1179
+ ];
1180
+ if (verbs.length > 0) {
1181
+ lines.push(...methodExports(typesDir, verbs));
1182
+ }
1183
+ lines.push("");
1184
+ await writeGenerated(file, lines.join("\n"));
1187
1185
  }
1188
- if (flags & import_typescript2.default.TypeFlags.Boolean) {
1189
- return { type: "boolean" };
1186
+ }
1187
+
1188
+ // src/generator/inputs.ts
1189
+ function sanitize(schema) {
1190
+ const { $schema, ...rest } = schema;
1191
+ void $schema;
1192
+ return rest;
1193
+ }
1194
+ function inputToJsonSchema(schema) {
1195
+ if (!isGiriInputSchema(schema)) {
1196
+ return void 0;
1190
1197
  }
1191
- if (type.isUnion()) {
1192
- return walkUnion(type, ctx);
1198
+ return sanitize(schema.toJsonSchema());
1199
+ }
1200
+ function bodyToJsonSchemas(value) {
1201
+ if (!isGiriBodySchema(value)) {
1202
+ return void 0;
1193
1203
  }
1194
- if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
1195
- return walkObject(type, ctx);
1204
+ const out = {};
1205
+ for (const [contentType, schema] of Object.entries(value.contents)) {
1206
+ const json = inputToJsonSchema(schema);
1207
+ if (json) {
1208
+ out[contentType] = json;
1209
+ }
1196
1210
  }
1197
- return {};
1211
+ return Object.keys(out).length > 0 ? out : void 0;
1198
1212
  }
1199
1213
 
1200
- // src/generator/schema/responses.ts
1201
- function findHandleFunction(source) {
1202
- let found;
1203
- const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
1204
- for (const statement of source.statements) {
1205
- if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
1206
- found = statement;
1207
- }
1208
- if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
1209
- for (const declaration of statement.declarationList.declarations) {
1210
- 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))) {
1211
- found = declaration.initializer;
1212
- }
1213
- }
1214
- }
1214
+ // src/generator/route-meta.ts
1215
+ function loadModule2(file) {
1216
+ const resolved = require.resolve(file);
1217
+ delete require.cache[resolved];
1218
+ return require(resolved);
1219
+ }
1220
+ function interopDefault2(value) {
1221
+ if (value && typeof value === "object" && "default" in value) {
1222
+ return value.default;
1215
1223
  }
1216
- return found;
1224
+ return value;
1217
1225
  }
1218
- function collectReturnExpressions(fn) {
1219
- if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
1220
- return [fn.body];
1226
+ function middlewareFunctions(value) {
1227
+ const exported = interopDefault2(value);
1228
+ if (typeof exported === "function") {
1229
+ return [exported];
1221
1230
  }
1222
- if (!fn.body) {
1223
- return [];
1231
+ if (Array.isArray(exported)) {
1232
+ return exported.filter((fn) => typeof fn === "function");
1224
1233
  }
1225
- const expressions = [];
1226
- const visit = (node) => {
1227
- if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
1228
- return;
1229
- }
1230
- if (import_typescript3.default.isReturnStatement(node) && node.expression) {
1231
- expressions.push(node.expression);
1232
- }
1233
- import_typescript3.default.forEachChild(node, visit);
1234
- };
1235
- import_typescript3.default.forEachChild(fn.body, visit);
1236
- return expressions;
1237
- }
1238
- function propertyType(checker, type, name, location) {
1239
- const symbol = checker.getPropertyOfType(type, name);
1240
- return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
1241
- }
1242
- function isTypedResponse(checker, type) {
1243
- return Boolean(
1244
- checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
1245
- );
1234
+ return [];
1246
1235
  }
1247
- function readFromCall(checker, expression) {
1248
- if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
1249
- return void 0;
1236
+ function readInput(routeModule) {
1237
+ const input = {};
1238
+ const body = bodyToJsonSchemas(routeModule.body);
1239
+ const query = inputToJsonSchema(routeModule.query);
1240
+ if (body) {
1241
+ input.body = body;
1250
1242
  }
1251
- const method = expression.expression.name.text;
1252
- if (method !== "json" && method !== "text") {
1253
- return void 0;
1243
+ if (query) {
1244
+ input.query = query;
1254
1245
  }
1255
- if (!isTypedResponse(checker, checker.getTypeAtLocation(expression))) {
1256
- return void 0;
1246
+ return input.body || input.query ? input : void 0;
1247
+ }
1248
+ function hiddenFrom(value) {
1249
+ if (value === false) {
1250
+ return true;
1257
1251
  }
1258
- const [dataArg, statusArg] = expression.arguments;
1259
- if (!dataArg) {
1260
- return void 0;
1252
+ if (value === true) {
1253
+ return false;
1261
1254
  }
1262
- let status = 200;
1263
- if (statusArg) {
1264
- const statusType = checker.getTypeAtLocation(statusArg);
1265
- status = statusType.isNumberLiteral() ? statusType.value : "default";
1255
+ if (value && typeof value === "object" && "hidden" in value) {
1256
+ return Boolean(value.hidden);
1266
1257
  }
1267
- return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
1258
+ return void 0;
1268
1259
  }
1269
- function readFromType(checker, type, location) {
1270
- const dataType = propertyType(checker, type, "data", location);
1271
- const statusType = propertyType(checker, type, "status", location);
1272
- const formatType = propertyType(checker, type, "format", location);
1273
- if (!dataType || !statusType || !formatType) {
1274
- return void 0;
1260
+ function collectHidden(route, routeModule, loadShared) {
1261
+ let hidden = false;
1262
+ for (const file of route.sharedFiles) {
1263
+ const opinion = hiddenFrom(loadShared(file).openapi);
1264
+ if (opinion !== void 0) {
1265
+ hidden = opinion;
1266
+ }
1275
1267
  }
1276
- const status = statusType.isNumberLiteral() ? statusType.value : "default";
1277
- const format = formatType.isStringLiteral() && formatType.value === "text" ? "text" : "json";
1278
- return { status, format, data: dataType };
1279
- }
1280
- function constituents(type) {
1281
- return type.isUnion() ? type.types : [type];
1268
+ const verb = hiddenFrom(routeModule.openapi);
1269
+ return verb ?? hidden;
1282
1270
  }
1283
- function extractRouteResponses(program, file) {
1284
- const result = { responses: [], opaque: false, warnings: [], $defs: {} };
1285
- const source = program.getSourceFile(file);
1286
- if (!source) {
1287
- return result;
1288
- }
1289
- const checker = program.getTypeChecker();
1290
- const fn = findHandleFunction(source);
1291
- if (!fn) {
1292
- return result;
1271
+ function collectSecurity(route, routeModule, loadShared) {
1272
+ const skipInherited = Boolean(
1273
+ routeModule.config?.skipInherited
1274
+ );
1275
+ const middleware = [];
1276
+ if (!skipInherited) {
1277
+ for (const file of route.sharedFiles) {
1278
+ middleware.push(...middlewareFunctions(loadShared(file).middleware));
1279
+ }
1293
1280
  }
1294
- const ctx = createWalkContext(checker, fn);
1295
- const byStatus = /* @__PURE__ */ new Map();
1296
- const record = (hit) => {
1297
- const schema = walkType(hit.data, ctx);
1298
- const bucket = byStatus.get(hit.status) ?? { format: hit.format, schemas: [] };
1299
- bucket.schemas.push(schema);
1300
- byStatus.set(hit.status, bucket);
1301
- };
1302
- for (const expression of collectReturnExpressions(fn)) {
1303
- const fromCall = readFromCall(checker, expression);
1304
- if (fromCall) {
1305
- record(fromCall);
1306
- continue;
1281
+ middleware.push(...middlewareFunctions(routeModule.middleware));
1282
+ const security = [];
1283
+ const securitySchemes = {};
1284
+ for (const fn of middleware) {
1285
+ const openapi = fn.openapi;
1286
+ if (openapi?.security) {
1287
+ for (const requirement of openapi.security) {
1288
+ if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
1289
+ security.push(requirement);
1290
+ }
1291
+ }
1307
1292
  }
1308
- let matched = false;
1309
- for (const member of constituents(checker.getTypeAtLocation(expression))) {
1310
- const hit = readFromType(checker, member, expression);
1311
- if (hit) {
1312
- record(hit);
1313
- matched = true;
1293
+ if (openapi?.securitySchemes) {
1294
+ Object.assign(securitySchemes, openapi.securitySchemes);
1295
+ }
1296
+ }
1297
+ return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
1298
+ }
1299
+ async function extractRouteMeta(config, paths, routes) {
1300
+ const byFile = /* @__PURE__ */ new Map();
1301
+ const { unregister } = await safeRegister();
1302
+ const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
1303
+ const sharedCache = /* @__PURE__ */ new Map();
1304
+ const loadShared = (file) => {
1305
+ if (!sharedCache.has(file)) {
1306
+ try {
1307
+ sharedCache.set(file, loadModule2(file));
1308
+ } catch {
1309
+ sharedCache.set(file, {});
1314
1310
  }
1315
1311
  }
1316
- if (!matched) {
1317
- result.opaque = true;
1312
+ return sharedCache.get(file);
1313
+ };
1314
+ try {
1315
+ for (const route of routes) {
1316
+ try {
1317
+ const routeModule = loadModule2(route.file);
1318
+ const meta = {};
1319
+ const input = readInput(routeModule);
1320
+ const security = collectSecurity(route, routeModule, loadShared);
1321
+ const hidden = collectHidden(route, routeModule, loadShared);
1322
+ if (input) {
1323
+ meta.input = input;
1324
+ }
1325
+ if (security) {
1326
+ meta.security = security;
1327
+ }
1328
+ if (hidden) {
1329
+ meta.hidden = true;
1330
+ }
1331
+ if (meta.input || meta.security || meta.hidden) {
1332
+ byFile.set(route.file, meta);
1333
+ }
1334
+ } catch {
1335
+ }
1318
1336
  }
1337
+ } finally {
1338
+ unregisterAlias();
1339
+ unregister();
1319
1340
  }
1320
- for (const [status, { format, schemas }] of byStatus) {
1321
- const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
1322
- result.responses.push({ status, format, schema });
1341
+ return byFile;
1342
+ }
1343
+
1344
+ // src/generator/route-types.ts
1345
+ var import_node_path9 = require("path");
1346
+ async function writeRouteTypes(paths, routes) {
1347
+ const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
1348
+ const lines = [
1349
+ GENERATED_HEADER,
1350
+ "export interface RouteParams {"
1351
+ ];
1352
+ for (const route of routes) {
1353
+ const typeFile = typeFilePath(paths, route.routeDir);
1354
+ lines.push(
1355
+ ` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
1356
+ importPath(file, typeFile)
1357
+ )}).Params;`
1358
+ );
1323
1359
  }
1324
- result.responses.sort((a, b) => Number(a.status) - Number(b.status));
1325
- result.warnings = ctx.warnings;
1326
- result.$defs = ctx.defs;
1327
- return result;
1360
+ lines.push("}", "");
1361
+ await writeGenerated(file, lines.join("\n"));
1328
1362
  }
1329
1363
 
1330
1364
  // src/generator/tsconfig.ts
@@ -1383,20 +1417,21 @@ async function typeFolders(paths, routes) {
1383
1417
  verbs: verbsByDir.get(slash(dir)) ?? []
1384
1418
  }));
1385
1419
  }
1386
- function extractResponses(paths, routes) {
1420
+ async function extractResponses(paths, routes) {
1387
1421
  const byFile = /* @__PURE__ */ new Map();
1388
1422
  if (routes.length === 0) {
1389
1423
  return byFile;
1390
1424
  }
1391
1425
  try {
1426
+ const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1392
1427
  const files = [...new Set(routes.map((route) => route.file))];
1393
1428
  const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
1394
- const program = createSchemaProgram(
1429
+ const program = createSchemaProgram2(
1395
1430
  paths,
1396
1431
  (0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
1397
1432
  );
1398
1433
  for (const file of files) {
1399
- byFile.set(file, extractRouteResponses(program, file));
1434
+ byFile.set(file, extractRouteResponses2(program, file));
1400
1435
  }
1401
1436
  } catch (error) {
1402
1437
  console.warn(`giri: skipped response schema generation (${error.message}).`);
@@ -1438,7 +1473,7 @@ async function syncProject(config, options = {}) {
1438
1473
  await writeRouteTypes(paths, routes);
1439
1474
  await writeAppTypes(paths);
1440
1475
  await writeTsConfig(paths, config);
1441
- const responsesByFile = extractResponses(paths, routes);
1476
+ const responsesByFile = await extractResponses(paths, routes);
1442
1477
  const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
1443
1478
  const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
1444
1479
  await writeManifest(paths, routes, data);
@@ -1544,9 +1579,10 @@ function createWatchUpdater(config, initial) {
1544
1579
  const reextractRoute = async (route) => {
1545
1580
  const key = route.file;
1546
1581
  try {
1582
+ const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
1547
1583
  const appTypes = (0, import_node_path13.join)(paths.outDir, "types", "app.d.ts");
1548
- const program = createSchemaProgram(paths, (0, import_node_fs7.existsSync)(appTypes) ? [key, appTypes] : [key]);
1549
- data.responsesByFile.set(key, extractRouteResponses(program, key));
1584
+ const program = createSchemaProgram2(paths, (0, import_node_fs7.existsSync)(appTypes) ? [key, appTypes] : [key]);
1585
+ data.responsesByFile.set(key, extractRouteResponses2(program, key));
1550
1586
  } catch {
1551
1587
  }
1552
1588
  try {
@@ -1810,6 +1846,23 @@ async function ensureTsConfig(cwd) {
1810
1846
  `
1811
1847
  );
1812
1848
  }
1849
+ async function missingDeps(cwd, candidates) {
1850
+ let pkg = {};
1851
+ try {
1852
+ pkg = JSON.parse(await (0, import_promises4.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8"));
1853
+ } catch {
1854
+ }
1855
+ const present = /* @__PURE__ */ new Set();
1856
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
1857
+ const map = pkg[field];
1858
+ if (map && typeof map === "object") {
1859
+ for (const name of Object.keys(map)) {
1860
+ present.add(name);
1861
+ }
1862
+ }
1863
+ }
1864
+ return candidates.filter((name) => !present.has(name));
1865
+ }
1813
1866
  function detectPackageManager() {
1814
1867
  const ua = process.env.npm_config_user_agent ?? "";
1815
1868
  if (ua.startsWith("yarn")) return "yarn";
@@ -1865,6 +1918,11 @@ async function selectAdapter(interactive) {
1865
1918
  return ADAPTERS.find((adapter) => adapter.value === picked) ?? null;
1866
1919
  }
1867
1920
  async function initProject(cwd, flags) {
1921
+ if (!(0, import_node_fs9.existsSync)((0, import_node_path15.join)(cwd, "package.json"))) {
1922
+ throw new Error(
1923
+ "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
+ );
1925
+ }
1868
1926
  const interactive = Boolean(process.stdout.isTTY) && !flags.yes;
1869
1927
  prompts.intro("giri init");
1870
1928
  let adapter;
@@ -1882,7 +1940,7 @@ async function initProject(cwd, flags) {
1882
1940
  }
1883
1941
  }
1884
1942
  if (!adapter.available) {
1885
- prompts.cancel(`The ${adapter.label} adapter isn't available yet \u2014 only Hono ships today.`);
1943
+ prompts.cancel(`The ${adapter.label} adapter isn't available yet - only Hono ships today.`);
1886
1944
  return;
1887
1945
  }
1888
1946
  const configPath = (0, import_node_path15.join)(cwd, "giri.config.ts");
@@ -1906,6 +1964,16 @@ async function initProject(cwd, flags) {
1906
1964
  await ensureTsConfig(cwd);
1907
1965
  prompts.log.success(`scaffolded a ${adapter.label} project`);
1908
1966
  const pm = flags.packageManager ?? detectPackageManager();
1967
+ const deps = await missingDeps(cwd, ["@boon4681/giri", ...adapter.deps, "zod"]);
1968
+ const devDeps = await missingDeps(cwd, ["typescript", "@types/node"]);
1969
+ if (deps.length === 0 && devDeps.length === 0) {
1970
+ prompts.outro("All dependencies already present. Run `giri serve` to start the dev server.");
1971
+ return;
1972
+ }
1973
+ const planLines = [
1974
+ ...deps.length ? [` ${pm} ${installArgs(pm, deps, false).join(" ")}`] : [],
1975
+ ...devDeps.length ? [` ${pm} ${installArgs(pm, devDeps, true).join(" ")}`] : []
1976
+ ];
1909
1977
  let install = flags.install;
1910
1978
  if (install === void 0) {
1911
1979
  if (!interactive) {
@@ -1913,34 +1981,34 @@ async function initProject(cwd, flags) {
1913
1981
  } else {
1914
1982
  const answer = await prompts.confirm({ message: `Install dependencies with ${pm}?` });
1915
1983
  if (prompts.isCancel(answer)) {
1916
- prompts.cancel("Cancelled \u2014 files written, skipped install.");
1984
+ prompts.cancel("Cancelled - files written, skipped install.");
1917
1985
  return;
1918
1986
  }
1919
1987
  install = answer;
1920
1988
  }
1921
1989
  }
1922
- const deps = ["@boon4681/giri", ...adapter.deps, "zod"];
1923
- const devDeps = ["typescript", "@types/node"];
1924
1990
  if (install) {
1925
1991
  try {
1926
- prompts.log.step(`Installing ${deps.join(", ")}`);
1927
- await runCommand(pm, installArgs(pm, deps, false), cwd);
1928
- prompts.log.step(`Installing dev deps ${devDeps.join(", ")}`);
1929
- await runCommand(pm, installArgs(pm, devDeps, true), cwd);
1992
+ if (deps.length) {
1993
+ prompts.log.step(`Installing ${deps.join(", ")}`);
1994
+ await runCommand(pm, installArgs(pm, deps, false), cwd);
1995
+ }
1996
+ if (devDeps.length) {
1997
+ prompts.log.step(`Installing dev deps ${devDeps.join(", ")}`);
1998
+ await runCommand(pm, installArgs(pm, devDeps, true), cwd);
1999
+ }
1930
2000
  } catch (error) {
1931
2001
  prompts.log.error(error instanceof Error ? error.message : String(error));
1932
- prompts.outro(`Install failed \u2014 run \`${pm} ${installArgs(pm, deps, false).join(" ")}\` yourself, then \`giri serve\`.`);
2002
+ prompts.outro(`Install failed - run these yourself, then \`giri serve\`:
2003
+ ${planLines.join("\n")}`);
1933
2004
  return;
1934
2005
  }
1935
2006
  prompts.outro("Ready. Run `giri serve` to start the dev server.");
1936
2007
  return;
1937
2008
  }
1938
- prompts.outro(
1939
- `Next:
1940
- ${pm} ${installArgs(pm, deps, false).join(" ")}
1941
- ${pm} ${installArgs(pm, devDeps, true).join(" ")}
1942
- giri serve`
1943
- );
2009
+ prompts.outro(`Next:
2010
+ ${planLines.join("\n")}
2011
+ giri serve`);
1944
2012
  }
1945
2013
  function displayHost(address) {
1946
2014
  if (!address || address === "::" || address === "0.0.0.0") {