@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/README.md +19 -17
- package/dist/adapters/hono.d.ts +26 -3
- package/dist/adapters/hono.js +49 -2
- package/dist/adapters/hono.js.map +1 -1
- package/dist/cli.js +1173 -1105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1326 -1288
- package/dist/index.js.map +1 -1
- package/dist/{types-BrUMxh5u.d.ts → types-DkrKD1S4.d.ts} +4 -2
- package/dist/validators/valibot.d.ts +1 -1
- package/dist/validators/valibot.js.map +1 -1
- package/dist/validators/zod.d.ts +1 -1
- package/dist/validators/zod.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,1506 +44,1543 @@ var init_es5 = __esm({
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
// src/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
defineConfig: () => defineConfig,
|
|
56
|
-
defineInputSchema: () => defineInputSchema,
|
|
57
|
-
defineMiddleware: () => defineMiddleware,
|
|
58
|
-
isGiriBodySchema: () => isGiriBodySchema,
|
|
59
|
-
isGiriInputSchema: () => isGiriInputSchema,
|
|
60
|
-
isTypedResponse: () => isTypedResponse,
|
|
61
|
-
loadLifecycle: () => loadLifecycle,
|
|
62
|
-
prepareRequestInput: () => prepareRequestInput,
|
|
63
|
-
resolveGiriPaths: () => resolveGiriPaths,
|
|
64
|
-
runInit: () => runInit,
|
|
65
|
-
scanRoutes: () => scanRoutes,
|
|
66
|
-
stack: () => stack,
|
|
67
|
-
syncProject: () => syncProject,
|
|
68
|
-
toResponse: () => toResponse,
|
|
69
|
-
typedResponseToResponse: () => typedResponseToResponse
|
|
70
|
-
});
|
|
71
|
-
module.exports = __toCommonJS(index_exports);
|
|
72
|
-
|
|
73
|
-
// src/types.ts
|
|
74
|
-
var typedResponseBrand = /* @__PURE__ */ Symbol.for("giri.typed-response");
|
|
75
|
-
var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
|
|
76
|
-
var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
|
|
77
|
-
|
|
78
|
-
// src/context.ts
|
|
79
|
-
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
80
|
-
function createTypedResponse(data, status, format, headers) {
|
|
81
|
-
return {
|
|
82
|
-
[typedResponseBrand]: { data, status, format },
|
|
83
|
-
data,
|
|
84
|
-
status,
|
|
85
|
-
format,
|
|
86
|
-
headers
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function isTypedResponse(value) {
|
|
90
|
-
return Boolean(value && typeof value === "object" && typedResponseBrand in value);
|
|
91
|
-
}
|
|
92
|
-
function createContext(options) {
|
|
93
|
-
const url = new URL(options.request.url);
|
|
94
|
-
const store = /* @__PURE__ */ new Map();
|
|
95
|
-
const validated = options.validated ?? {};
|
|
96
|
-
return {
|
|
97
|
-
params: options.params ?? {},
|
|
98
|
-
app: options.app ?? {},
|
|
99
|
-
req: {
|
|
100
|
-
raw: options.request,
|
|
101
|
-
url,
|
|
102
|
-
method: options.request.method,
|
|
103
|
-
header: (name) => options.request.headers.get(name),
|
|
104
|
-
json: () => options.request.json(),
|
|
105
|
-
text: () => options.request.text(),
|
|
106
|
-
arrayBuffer: () => options.request.arrayBuffer(),
|
|
107
|
-
formData: () => options.request.formData(),
|
|
108
|
-
valid: (key) => {
|
|
109
|
-
if (!(key in validated)) {
|
|
110
|
-
throw new Error(`No validated ${String(key)} data is available for this route.`);
|
|
111
|
-
}
|
|
112
|
-
return validated[key];
|
|
47
|
+
// src/generator/schema/program.ts
|
|
48
|
+
function createSchemaProgram(paths, routeFiles) {
|
|
49
|
+
let options = { ...DEFAULT_OPTIONS };
|
|
50
|
+
const configPath = import_typescript.default.findConfigFile(paths.cwd, import_typescript.default.sys.fileExists, "tsconfig.json");
|
|
51
|
+
if (configPath) {
|
|
52
|
+
const parsed = import_typescript.default.getParsedCommandLineOfConfigFile(configPath, {}, {
|
|
53
|
+
...import_typescript.default.sys,
|
|
54
|
+
onUnRecoverableConfigFileDiagnostic: () => {
|
|
113
55
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
},
|
|
118
|
-
get: (key) => store.get(key),
|
|
119
|
-
json: (data, status = 200, headers) => createTypedResponse(data, status, "json", headers),
|
|
120
|
-
text: (text, status = 200, headers) => createTypedResponse(text, status, "text", headers)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function typedResponseToResponse(response) {
|
|
124
|
-
const headers = new Headers(response.headers);
|
|
125
|
-
if (response.format === "json" && !headers.has("content-type")) {
|
|
126
|
-
headers.set("content-type", "application/json; charset=utf-8");
|
|
127
|
-
}
|
|
128
|
-
if (response.format === "text" && !headers.has("content-type")) {
|
|
129
|
-
headers.set("content-type", "text/plain; charset=utf-8");
|
|
130
|
-
}
|
|
131
|
-
const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
|
|
132
|
-
return new Response(body, {
|
|
133
|
-
status: response.status,
|
|
134
|
-
headers
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
function toResponse(response) {
|
|
138
|
-
return isTypedResponse(response) ? typedResponseToResponse(response) : response;
|
|
139
|
-
}
|
|
140
|
-
async function composeMiddleware(middleware, handle, context) {
|
|
141
|
-
let index = -1;
|
|
142
|
-
let result;
|
|
143
|
-
const dispatch = async (i) => {
|
|
144
|
-
if (i <= index) {
|
|
145
|
-
throw new Error("next() called multiple times in giri middleware.");
|
|
146
|
-
}
|
|
147
|
-
index = i;
|
|
148
|
-
if (i === middleware.length) {
|
|
149
|
-
result = await handle(context);
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
const returned = await middleware[i](context, () => dispatch(i + 1));
|
|
153
|
-
if (returned !== void 0) {
|
|
154
|
-
result = returned;
|
|
155
|
-
return returned;
|
|
56
|
+
});
|
|
57
|
+
if (parsed) {
|
|
58
|
+
options = { ...parsed.options, noEmit: true };
|
|
156
59
|
}
|
|
157
|
-
return result;
|
|
158
|
-
};
|
|
159
|
-
await dispatch(0);
|
|
160
|
-
if (result === void 0) {
|
|
161
|
-
throw new Error("Route completed without returning a response.");
|
|
162
60
|
}
|
|
163
|
-
return
|
|
61
|
+
return import_typescript.default.createProgram(routeFiles, options);
|
|
164
62
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
63
|
+
var import_typescript, DEFAULT_OPTIONS;
|
|
64
|
+
var init_program = __esm({
|
|
65
|
+
"src/generator/schema/program.ts"() {
|
|
66
|
+
"use strict";
|
|
67
|
+
import_typescript = __toESM(require("typescript"));
|
|
68
|
+
DEFAULT_OPTIONS = {
|
|
69
|
+
target: import_typescript.default.ScriptTarget.ES2022,
|
|
70
|
+
module: import_typescript.default.ModuleKind.NodeNext,
|
|
71
|
+
moduleResolution: import_typescript.default.ModuleResolutionKind.NodeNext,
|
|
72
|
+
strict: true,
|
|
73
|
+
skipLibCheck: true,
|
|
74
|
+
noEmit: true
|
|
75
|
+
};
|
|
171
76
|
}
|
|
172
|
-
|
|
173
|
-
return maybeMiddleware;
|
|
174
|
-
}
|
|
175
|
-
function stack(...middleware) {
|
|
176
|
-
return middleware;
|
|
177
|
-
}
|
|
77
|
+
});
|
|
178
78
|
|
|
179
|
-
// src/
|
|
180
|
-
function
|
|
181
|
-
return {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
79
|
+
// src/generator/schema/json-schema.ts
|
|
80
|
+
function createWalkContext(checker, location) {
|
|
81
|
+
return {
|
|
82
|
+
checker,
|
|
83
|
+
location,
|
|
84
|
+
defs: {},
|
|
85
|
+
inProgress: /* @__PURE__ */ new Map(),
|
|
86
|
+
usedDefs: /* @__PURE__ */ new Set(),
|
|
87
|
+
warnings: []
|
|
88
|
+
};
|
|
187
89
|
}
|
|
188
|
-
function
|
|
189
|
-
return
|
|
90
|
+
function typeId(type) {
|
|
91
|
+
return type.id;
|
|
190
92
|
}
|
|
191
|
-
function
|
|
192
|
-
return
|
|
193
|
-
value && typeof value === "object" && value[bodySchemaBrand] === true
|
|
194
|
-
);
|
|
93
|
+
function intrinsicName(type) {
|
|
94
|
+
return type.intrinsicName;
|
|
195
95
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"application/x-www-form-urlencoded": "urlencoded",
|
|
200
|
-
"text/plain": "text"
|
|
201
|
-
};
|
|
202
|
-
function contentTypeFromHeader(header) {
|
|
203
|
-
if (!header) {
|
|
204
|
-
return void 0;
|
|
205
|
-
}
|
|
206
|
-
const mime = header.split(";", 1)[0].trim().toLowerCase();
|
|
207
|
-
return MIME_TO_CONTENT_TYPE[mime];
|
|
96
|
+
function isDateType(type) {
|
|
97
|
+
const symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
98
|
+
return symbol?.getName() === "Date";
|
|
208
99
|
}
|
|
209
|
-
function
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
current.push(value);
|
|
100
|
+
function literalValuesOf(types) {
|
|
101
|
+
const values = [];
|
|
102
|
+
for (const member of types) {
|
|
103
|
+
if (member.isStringLiteral() || member.isNumberLiteral()) {
|
|
104
|
+
values.push(member.value);
|
|
105
|
+
} else if (member.flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
|
|
106
|
+
values.push(intrinsicName(member) === "true");
|
|
217
107
|
} else {
|
|
218
|
-
|
|
108
|
+
return void 0;
|
|
219
109
|
}
|
|
220
|
-
}
|
|
221
|
-
return
|
|
110
|
+
}
|
|
111
|
+
return values;
|
|
222
112
|
}
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
113
|
+
function walkUnion(type, ctx) {
|
|
114
|
+
const flag = import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void | import_typescript2.default.TypeFlags.Never;
|
|
115
|
+
const members = type.types.filter((member) => !(member.flags & flag));
|
|
116
|
+
if (members.length === 1) {
|
|
117
|
+
return walkType(members[0], ctx);
|
|
227
118
|
}
|
|
228
|
-
|
|
229
|
-
|
|
119
|
+
const enumValues = literalValuesOf(members);
|
|
120
|
+
if (enumValues) {
|
|
121
|
+
return { enum: enumValues };
|
|
230
122
|
}
|
|
231
|
-
return
|
|
123
|
+
return { anyOf: members.map((member) => walkType(member, ctx)) };
|
|
232
124
|
}
|
|
233
|
-
function
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
125
|
+
function buildObjectSchema(type, ctx) {
|
|
126
|
+
const { checker } = ctx;
|
|
127
|
+
const indexInfo = checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.String) ?? checker.getIndexInfoOfType(type, import_typescript2.default.IndexKind.Number);
|
|
128
|
+
const properties = {};
|
|
129
|
+
const required = [];
|
|
130
|
+
for (const symbol of checker.getPropertiesOfType(type)) {
|
|
131
|
+
const name = symbol.getName();
|
|
132
|
+
const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
|
|
133
|
+
const optional = Boolean(symbol.getFlags() & import_typescript2.default.SymbolFlags.Optional) || Boolean(propType.flags & import_typescript2.default.TypeFlags.Union && propType.types.some((t) => t.flags & import_typescript2.default.TypeFlags.Undefined));
|
|
134
|
+
properties[name] = walkType(propType, ctx);
|
|
135
|
+
if (!optional) {
|
|
136
|
+
required.push(name);
|
|
243
137
|
}
|
|
244
138
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!isGiriInputSchema(schema)) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
|
|
251
|
-
);
|
|
139
|
+
const schema = { type: "object" };
|
|
140
|
+
if (Object.keys(properties).length > 0) {
|
|
141
|
+
schema.properties = properties;
|
|
252
142
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
async function prepareRequestInput(request, input) {
|
|
256
|
-
const validated = {};
|
|
257
|
-
if (input?.query) {
|
|
258
|
-
const query = queryObject(new URL(request.url));
|
|
259
|
-
const result = await runValidation(input.query, query, "query");
|
|
260
|
-
if (!result.ok) {
|
|
261
|
-
return {
|
|
262
|
-
ok: false,
|
|
263
|
-
response: createTypedResponse(
|
|
264
|
-
{ message: "Invalid query parameters.", issues: result.issues },
|
|
265
|
-
400,
|
|
266
|
-
"json"
|
|
267
|
-
)
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
validated.query = result.value;
|
|
143
|
+
if (required.length > 0) {
|
|
144
|
+
schema.required = required;
|
|
271
145
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
|
|
277
|
-
if (!chosen) {
|
|
278
|
-
return {
|
|
279
|
-
ok: false,
|
|
280
|
-
response: createTypedResponse(
|
|
281
|
-
{ message: "Unsupported media type.", issues: { accepted: declared } },
|
|
282
|
-
415,
|
|
283
|
-
"json"
|
|
284
|
-
)
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
let rawBody;
|
|
288
|
-
try {
|
|
289
|
-
rawBody = await readRawBody(request, chosen);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
return {
|
|
292
|
-
ok: false,
|
|
293
|
-
response: createTypedResponse(
|
|
294
|
-
{ message: "Invalid request body.", issues: error },
|
|
295
|
-
400,
|
|
296
|
-
"json"
|
|
297
|
-
)
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
const result = await runValidation(contents[chosen], rawBody, "body");
|
|
301
|
-
if (!result.ok) {
|
|
302
|
-
return {
|
|
303
|
-
ok: false,
|
|
304
|
-
response: createTypedResponse(
|
|
305
|
-
{ message: "Invalid request body.", issues: result.issues },
|
|
306
|
-
400,
|
|
307
|
-
"json"
|
|
308
|
-
)
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
|
|
146
|
+
if (indexInfo) {
|
|
147
|
+
schema.additionalProperties = walkType(indexInfo.type, ctx);
|
|
148
|
+
} else if (Object.keys(properties).length > 0) {
|
|
149
|
+
schema.additionalProperties = false;
|
|
312
150
|
}
|
|
313
|
-
return
|
|
151
|
+
return schema;
|
|
314
152
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// src/loader/loader.ts
|
|
321
|
-
var import_prompts = require("@clack/prompts");
|
|
322
|
-
var import_node_fs = require("fs");
|
|
323
|
-
var import_node_path = require("path");
|
|
324
|
-
var import_node_process = require("process");
|
|
325
|
-
|
|
326
|
-
// src/config/schema.ts
|
|
327
|
-
var import_typebox = require("@sinclair/typebox");
|
|
328
|
-
var configSchema = import_typebox.Type.Object({
|
|
329
|
-
adapter: import_typebox.Type.Any(),
|
|
330
|
-
alias: import_typebox.Type.Optional(import_typebox.Type.Record(
|
|
331
|
-
import_typebox.Type.String(),
|
|
332
|
-
import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
|
|
333
|
-
)),
|
|
334
|
-
outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
|
|
335
|
-
server: import_typebox.Type.Optional(import_typebox.Type.Object({
|
|
336
|
-
port: import_typebox.Type.Optional(import_typebox.Type.Number()),
|
|
337
|
-
hostname: import_typebox.Type.Optional(import_typebox.Type.String())
|
|
338
|
-
}, { additionalProperties: false })),
|
|
339
|
-
errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
|
|
340
|
-
}, { additionalProperties: false });
|
|
341
|
-
|
|
342
|
-
// src/loader/loader.ts
|
|
343
|
-
var import_value = require("@sinclair/typebox/value");
|
|
344
|
-
var assertES5 = async (unregister) => {
|
|
345
|
-
try {
|
|
346
|
-
init_es5();
|
|
347
|
-
} catch (e) {
|
|
348
|
-
if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
|
|
349
|
-
const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
|
|
350
|
-
if (es5Error) {
|
|
351
|
-
import_prompts.log.error(
|
|
352
|
-
`Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
|
|
353
|
-
);
|
|
354
|
-
(0, import_node_process.exit)(1);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
import_prompts.log.error(e);
|
|
358
|
-
(0, import_node_process.exit)(1);
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
var safeRegister = async () => {
|
|
362
|
-
const { register } = await import("esbuild-register/dist/node");
|
|
363
|
-
let res;
|
|
364
|
-
try {
|
|
365
|
-
res = register({
|
|
366
|
-
format: "cjs",
|
|
367
|
-
loader: "ts"
|
|
368
|
-
});
|
|
369
|
-
} catch {
|
|
370
|
-
res = {
|
|
371
|
-
unregister: () => {
|
|
372
|
-
}
|
|
373
|
-
};
|
|
153
|
+
function defName(type) {
|
|
154
|
+
const symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
155
|
+
const name = symbol?.getName();
|
|
156
|
+
if (name && name !== "__type" && name !== "__object") {
|
|
157
|
+
return name;
|
|
374
158
|
}
|
|
375
|
-
|
|
376
|
-
return res;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
// src/routes.ts
|
|
380
|
-
var import_node_fs2 = require("fs");
|
|
381
|
-
var import_promises = require("fs/promises");
|
|
382
|
-
var import_node_path2 = require("path");
|
|
383
|
-
var import_tinyglobby = require("tinyglobby");
|
|
384
|
-
var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
385
|
-
var METHOD_FROM_FILE = new Map(
|
|
386
|
-
METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
|
|
387
|
-
);
|
|
388
|
-
function normalizeSlashes(path) {
|
|
389
|
-
return path.split(import_node_path2.sep).join("/");
|
|
390
|
-
}
|
|
391
|
-
function isRouteSourceFile(fileName) {
|
|
392
|
-
return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
|
|
159
|
+
return `Anonymous${typeId(type)}`;
|
|
393
160
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
161
|
+
function walkObject(type, ctx) {
|
|
162
|
+
const { checker } = ctx;
|
|
163
|
+
if (isDateType(type)) {
|
|
164
|
+
return { type: "string", format: "date-time" };
|
|
397
165
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
function sharedFileIn(dir) {
|
|
402
|
-
for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
|
|
403
|
-
const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
|
|
404
|
-
if ((0, import_node_fs2.existsSync)(file)) {
|
|
405
|
-
return file;
|
|
406
|
-
}
|
|
166
|
+
if (checker.isArrayType(type)) {
|
|
167
|
+
const [element] = checker.getTypeArguments(type);
|
|
168
|
+
return { type: "array", items: element ? walkType(element, ctx) : {} };
|
|
407
169
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const rel = (0, import_node_path2.relative)(routesDir, routeDir);
|
|
412
|
-
if (!rel) {
|
|
413
|
-
return [];
|
|
170
|
+
if (checker.isTupleType(type)) {
|
|
171
|
+
const elements = checker.getTypeArguments(type);
|
|
172
|
+
return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
|
|
414
173
|
}
|
|
415
|
-
|
|
174
|
+
const id = typeId(type);
|
|
175
|
+
const existing = ctx.inProgress.get(id);
|
|
176
|
+
if (existing) {
|
|
177
|
+
ctx.usedDefs.add(existing);
|
|
178
|
+
return { $ref: `#/$defs/${existing}` };
|
|
179
|
+
}
|
|
180
|
+
const name = defName(type);
|
|
181
|
+
ctx.inProgress.set(id, name);
|
|
182
|
+
const schema = buildObjectSchema(type, ctx);
|
|
183
|
+
ctx.inProgress.delete(id);
|
|
184
|
+
if (ctx.usedDefs.has(name)) {
|
|
185
|
+
ctx.defs[name] = schema;
|
|
186
|
+
return { $ref: `#/$defs/${name}` };
|
|
187
|
+
}
|
|
188
|
+
return schema;
|
|
416
189
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
190
|
+
function walkType(type, ctx) {
|
|
191
|
+
const flags = type.flags;
|
|
192
|
+
if (flags & (import_typescript2.default.TypeFlags.Any | import_typescript2.default.TypeFlags.Unknown)) {
|
|
419
193
|
return {};
|
|
420
194
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const name = catchAll[1];
|
|
424
|
-
return {
|
|
425
|
-
value: `:${name}{.*}`,
|
|
426
|
-
param: { name, catchAll: true }
|
|
427
|
-
};
|
|
195
|
+
if (flags & import_typescript2.default.TypeFlags.Null) {
|
|
196
|
+
return { type: "null" };
|
|
428
197
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const name = param[1];
|
|
432
|
-
return {
|
|
433
|
-
value: `:${name}`,
|
|
434
|
-
param: { name, catchAll: false }
|
|
435
|
-
};
|
|
198
|
+
if (flags & (import_typescript2.default.TypeFlags.Undefined | import_typescript2.default.TypeFlags.Void)) {
|
|
199
|
+
return {};
|
|
436
200
|
}
|
|
437
|
-
|
|
201
|
+
if (flags & (import_typescript2.default.TypeFlags.BigInt | import_typescript2.default.TypeFlags.BigIntLiteral)) {
|
|
202
|
+
ctx.warnings.push("bigint is not JSON-serializable (JSON.stringify throws); documented as string.");
|
|
203
|
+
return { type: "string" };
|
|
204
|
+
}
|
|
205
|
+
if (type.isStringLiteral()) {
|
|
206
|
+
return { type: "string", const: type.value };
|
|
207
|
+
}
|
|
208
|
+
if (type.isNumberLiteral()) {
|
|
209
|
+
return { type: "number", const: type.value };
|
|
210
|
+
}
|
|
211
|
+
if (flags & import_typescript2.default.TypeFlags.BooleanLiteral) {
|
|
212
|
+
return { type: "boolean", const: intrinsicName(type) === "true" };
|
|
213
|
+
}
|
|
214
|
+
if (flags & import_typescript2.default.TypeFlags.String) {
|
|
215
|
+
return { type: "string" };
|
|
216
|
+
}
|
|
217
|
+
if (flags & import_typescript2.default.TypeFlags.Number) {
|
|
218
|
+
return { type: "number" };
|
|
219
|
+
}
|
|
220
|
+
if (flags & import_typescript2.default.TypeFlags.Boolean) {
|
|
221
|
+
return { type: "boolean" };
|
|
222
|
+
}
|
|
223
|
+
if (type.isUnion()) {
|
|
224
|
+
return walkUnion(type, ctx);
|
|
225
|
+
}
|
|
226
|
+
if (flags & import_typescript2.default.TypeFlags.Object || type.isIntersection()) {
|
|
227
|
+
return walkObject(type, ctx);
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
438
230
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
231
|
+
var import_typescript2;
|
|
232
|
+
var init_json_schema = __esm({
|
|
233
|
+
"src/generator/schema/json-schema.ts"() {
|
|
234
|
+
"use strict";
|
|
235
|
+
import_typescript2 = __toESM(require("typescript"));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// src/generator/schema/responses.ts
|
|
240
|
+
function findHandleFunction(source) {
|
|
241
|
+
let found;
|
|
242
|
+
const isExported = (node) => import_typescript3.default.canHaveModifiers(node) && (import_typescript3.default.getModifiers(node)?.some((m) => m.kind === import_typescript3.default.SyntaxKind.ExportKeyword) ?? false);
|
|
243
|
+
for (const statement of source.statements) {
|
|
244
|
+
if (import_typescript3.default.isFunctionDeclaration(statement) && statement.name?.text === "handle" && isExported(statement)) {
|
|
245
|
+
found = statement;
|
|
446
246
|
}
|
|
447
|
-
if (
|
|
448
|
-
|
|
247
|
+
if (import_typescript3.default.isVariableStatement(statement) && isExported(statement)) {
|
|
248
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
249
|
+
if (import_typescript3.default.isIdentifier(declaration.name) && declaration.name.text === "handle" && declaration.initializer && (import_typescript3.default.isArrowFunction(declaration.initializer) || import_typescript3.default.isFunctionExpression(declaration.initializer))) {
|
|
250
|
+
found = declaration.initializer;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
449
253
|
}
|
|
450
254
|
}
|
|
451
|
-
return
|
|
452
|
-
path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
|
|
453
|
-
params
|
|
454
|
-
};
|
|
255
|
+
return found;
|
|
455
256
|
}
|
|
456
|
-
|
|
457
|
-
if (
|
|
257
|
+
function collectReturnExpressions(fn) {
|
|
258
|
+
if (import_typescript3.default.isArrowFunction(fn) && !import_typescript3.default.isBlock(fn.body)) {
|
|
259
|
+
return [fn.body];
|
|
260
|
+
}
|
|
261
|
+
if (!fn.body) {
|
|
458
262
|
return [];
|
|
459
263
|
}
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
264
|
+
const expressions = [];
|
|
265
|
+
const visit = (node) => {
|
|
266
|
+
if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (import_typescript3.default.isReturnStatement(node) && node.expression) {
|
|
270
|
+
expressions.push(node.expression);
|
|
468
271
|
}
|
|
272
|
+
import_typescript3.default.forEachChild(node, visit);
|
|
469
273
|
};
|
|
470
|
-
|
|
471
|
-
return
|
|
274
|
+
import_typescript3.default.forEachChild(fn.body, visit);
|
|
275
|
+
return expressions;
|
|
472
276
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
277
|
+
function propertyType(checker, type, name, location) {
|
|
278
|
+
const symbol = checker.getPropertyOfType(type, name);
|
|
279
|
+
return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
|
|
475
280
|
}
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
281
|
+
function isTypedResponse2(checker, type) {
|
|
282
|
+
return Boolean(
|
|
283
|
+
checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function readFromCall(checker, expression) {
|
|
287
|
+
if (!import_typescript3.default.isCallExpression(expression) || !import_typescript3.default.isPropertyAccessExpression(expression.expression)) {
|
|
288
|
+
return void 0;
|
|
483
289
|
}
|
|
484
|
-
|
|
290
|
+
const method = expression.expression.name.text;
|
|
291
|
+
if (method !== "json" && method !== "text") {
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
if (!isTypedResponse2(checker, checker.getTypeAtLocation(expression))) {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
const [dataArg, statusArg] = expression.arguments;
|
|
298
|
+
if (!dataArg) {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
let status = 200;
|
|
302
|
+
if (statusArg) {
|
|
303
|
+
const statusType = checker.getTypeAtLocation(statusArg);
|
|
304
|
+
status = statusType.isNumberLiteral() ? statusType.value : "default";
|
|
305
|
+
}
|
|
306
|
+
return { status, format: method === "text" ? "text" : "json", data: checker.getTypeAtLocation(dataArg) };
|
|
485
307
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
308
|
+
function readFromType(checker, type, location) {
|
|
309
|
+
const dataType = propertyType(checker, type, "data", location);
|
|
310
|
+
const statusType = propertyType(checker, type, "status", location);
|
|
311
|
+
const formatType = propertyType(checker, type, "format", location);
|
|
312
|
+
if (!dataType || !statusType || !formatType) {
|
|
313
|
+
return void 0;
|
|
489
314
|
}
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
315
|
+
const status = statusType.isNumberLiteral() ? statusType.value : "default";
|
|
316
|
+
const format = formatType.isStringLiteral() && formatType.value === "text" ? "text" : "json";
|
|
317
|
+
return { status, format, data: dataType };
|
|
318
|
+
}
|
|
319
|
+
function constituents(type) {
|
|
320
|
+
return type.isUnion() ? type.types : [type];
|
|
321
|
+
}
|
|
322
|
+
function extractRouteResponses(program, file) {
|
|
323
|
+
const result = { responses: [], opaque: false, warnings: [], $defs: {} };
|
|
324
|
+
const source = program.getSourceFile(file);
|
|
325
|
+
if (!source) {
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
const checker = program.getTypeChecker();
|
|
329
|
+
const fn = findHandleFunction(source);
|
|
330
|
+
if (!fn) {
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
const ctx = createWalkContext(checker, fn);
|
|
334
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
335
|
+
const record = (hit) => {
|
|
336
|
+
const schema = walkType(hit.data, ctx);
|
|
337
|
+
const bucket = byStatus.get(hit.status) ?? { format: hit.format, schemas: [] };
|
|
338
|
+
bucket.schemas.push(schema);
|
|
339
|
+
byStatus.set(hit.status, bucket);
|
|
340
|
+
};
|
|
341
|
+
for (const expression of collectReturnExpressions(fn)) {
|
|
342
|
+
const fromCall = readFromCall(checker, expression);
|
|
343
|
+
if (fromCall) {
|
|
344
|
+
record(fromCall);
|
|
499
345
|
continue;
|
|
500
346
|
}
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
sharedFiles: sharedFilesForDir(routesDir, routeDir)
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
return routes.sort((left, right) => {
|
|
515
|
-
const pathOrder = left.path.localeCompare(right.path);
|
|
516
|
-
if (pathOrder !== 0) {
|
|
517
|
-
return pathOrder;
|
|
347
|
+
let matched = false;
|
|
348
|
+
for (const member of constituents(checker.getTypeAtLocation(expression))) {
|
|
349
|
+
const hit = readFromType(checker, member, expression);
|
|
350
|
+
if (hit) {
|
|
351
|
+
record(hit);
|
|
352
|
+
matched = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!matched) {
|
|
356
|
+
result.opaque = true;
|
|
518
357
|
}
|
|
519
|
-
return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// src/app.ts
|
|
524
|
-
function loadModule(file) {
|
|
525
|
-
const resolved = require.resolve(file);
|
|
526
|
-
delete require.cache[resolved];
|
|
527
|
-
return require(resolved);
|
|
528
|
-
}
|
|
529
|
-
function interopDefault(value) {
|
|
530
|
-
if (value && typeof value === "object" && "default" in value) {
|
|
531
|
-
return value.default;
|
|
532
358
|
}
|
|
533
|
-
|
|
359
|
+
for (const [status, { format, schemas }] of byStatus) {
|
|
360
|
+
const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
|
|
361
|
+
result.responses.push({ status, format, schema });
|
|
362
|
+
}
|
|
363
|
+
result.responses.sort((a, b) => Number(a.status) - Number(b.status));
|
|
364
|
+
result.warnings = ctx.warnings;
|
|
365
|
+
result.$defs = ctx.defs;
|
|
366
|
+
return result;
|
|
534
367
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
368
|
+
var import_typescript3;
|
|
369
|
+
var init_responses = __esm({
|
|
370
|
+
"src/generator/schema/responses.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
import_typescript3 = __toESM(require("typescript"));
|
|
373
|
+
init_json_schema();
|
|
539
374
|
}
|
|
540
|
-
|
|
541
|
-
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// src/generator/schema/index.ts
|
|
378
|
+
var schema_exports = {};
|
|
379
|
+
__export(schema_exports, {
|
|
380
|
+
createSchemaProgram: () => createSchemaProgram,
|
|
381
|
+
createWalkContext: () => createWalkContext,
|
|
382
|
+
extractRouteResponses: () => extractRouteResponses,
|
|
383
|
+
walkType: () => walkType
|
|
384
|
+
});
|
|
385
|
+
var init_schema = __esm({
|
|
386
|
+
"src/generator/schema/index.ts"() {
|
|
387
|
+
"use strict";
|
|
388
|
+
init_program();
|
|
389
|
+
init_responses();
|
|
390
|
+
init_json_schema();
|
|
542
391
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// src/index.ts
|
|
395
|
+
var index_exports = {};
|
|
396
|
+
__export(index_exports, {
|
|
397
|
+
buildGiriApp: () => buildGiriApp,
|
|
398
|
+
composeMiddleware: () => composeMiddleware,
|
|
399
|
+
createContext: () => createContext,
|
|
400
|
+
createTypedResponse: () => createTypedResponse,
|
|
401
|
+
defineBodySchema: () => defineBodySchema,
|
|
402
|
+
defineConfig: () => defineConfig,
|
|
403
|
+
defineInputSchema: () => defineInputSchema,
|
|
404
|
+
defineMiddleware: () => defineMiddleware,
|
|
405
|
+
isGiriBodySchema: () => isGiriBodySchema,
|
|
406
|
+
isGiriInputSchema: () => isGiriInputSchema,
|
|
407
|
+
isTypedResponse: () => isTypedResponse,
|
|
408
|
+
loadLifecycle: () => loadLifecycle,
|
|
409
|
+
prepareRequestInput: () => prepareRequestInput,
|
|
410
|
+
resolveGiriPaths: () => resolveGiriPaths,
|
|
411
|
+
runInit: () => runInit,
|
|
412
|
+
scanRoutes: () => scanRoutes,
|
|
413
|
+
stack: () => stack,
|
|
414
|
+
syncProject: () => syncProject,
|
|
415
|
+
toResponse: () => toResponse,
|
|
416
|
+
typedResponseToResponse: () => typedResponseToResponse
|
|
417
|
+
});
|
|
418
|
+
module.exports = __toCommonJS(index_exports);
|
|
419
|
+
|
|
420
|
+
// src/types.ts
|
|
421
|
+
var typedResponseBrand = /* @__PURE__ */ Symbol.for("giri.typed-response");
|
|
422
|
+
var nativeContextBrand = /* @__PURE__ */ Symbol.for("giri.native-context");
|
|
423
|
+
var inputSchemaBrand = /* @__PURE__ */ Symbol.for("giri.input-schema");
|
|
424
|
+
var bodySchemaBrand = /* @__PURE__ */ Symbol.for("giri.body-schema");
|
|
425
|
+
|
|
426
|
+
// src/context.ts
|
|
427
|
+
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
428
|
+
function createTypedResponse(data, status, format, headers) {
|
|
429
|
+
return {
|
|
430
|
+
[typedResponseBrand]: { data, status, format },
|
|
431
|
+
data,
|
|
432
|
+
status,
|
|
433
|
+
format,
|
|
434
|
+
headers
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function isTypedResponse(value) {
|
|
438
|
+
return Boolean(value && typeof value === "object" && typedResponseBrand in value);
|
|
439
|
+
}
|
|
440
|
+
function createContext(options) {
|
|
441
|
+
const url = new URL(options.request.url);
|
|
442
|
+
const store = /* @__PURE__ */ new Map();
|
|
443
|
+
const validated = options.validated ?? {};
|
|
444
|
+
const context = {
|
|
445
|
+
params: options.params ?? {},
|
|
446
|
+
app: options.app ?? {},
|
|
447
|
+
req: {
|
|
448
|
+
raw: options.request,
|
|
449
|
+
url,
|
|
450
|
+
method: options.request.method,
|
|
451
|
+
header: (name) => options.request.headers.get(name),
|
|
452
|
+
json: () => options.request.json(),
|
|
453
|
+
text: () => options.request.text(),
|
|
454
|
+
arrayBuffer: () => options.request.arrayBuffer(),
|
|
455
|
+
formData: () => options.request.formData(),
|
|
456
|
+
valid: (key) => {
|
|
457
|
+
if (!(key in validated)) {
|
|
458
|
+
throw new Error(`No validated ${String(key)} data is available for this route.`);
|
|
459
|
+
}
|
|
460
|
+
return validated[key];
|
|
547
461
|
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
function assertQuerySchema(value, file) {
|
|
561
|
-
if (!isGiriInputSchema(value)) {
|
|
562
|
-
throw new Error(
|
|
563
|
-
`${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
|
|
564
|
-
);
|
|
565
|
-
}
|
|
462
|
+
},
|
|
463
|
+
set: (key, value) => {
|
|
464
|
+
store.set(key, value);
|
|
465
|
+
},
|
|
466
|
+
get: (key) => store.get(key),
|
|
467
|
+
json: (data, status = 200, headers) => createTypedResponse(data, status, "json", headers),
|
|
468
|
+
text: (text, status = 200, headers) => createTypedResponse(text, status, "text", headers)
|
|
469
|
+
};
|
|
470
|
+
context[nativeContextBrand] = options.native;
|
|
471
|
+
return context;
|
|
566
472
|
}
|
|
567
|
-
function
|
|
568
|
-
const
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
input.body = routeModule.body;
|
|
473
|
+
function typedResponseToResponse(response) {
|
|
474
|
+
const headers = new Headers(response.headers);
|
|
475
|
+
if (response.format === "json" && !headers.has("content-type")) {
|
|
476
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
572
477
|
}
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
input.query = routeModule.query;
|
|
478
|
+
if (response.format === "text" && !headers.has("content-type")) {
|
|
479
|
+
headers.set("content-type", "text/plain; charset=utf-8");
|
|
576
480
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
481
|
+
const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
|
|
482
|
+
return new Response(body, {
|
|
483
|
+
status: response.status,
|
|
484
|
+
headers
|
|
485
|
+
});
|
|
581
486
|
}
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
|
|
487
|
+
function toResponse(response) {
|
|
488
|
+
return isTypedResponse(response) ? typedResponseToResponse(response) : response;
|
|
585
489
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
490
|
+
async function composeMiddleware(middleware, handle, context) {
|
|
491
|
+
let index = -1;
|
|
492
|
+
let result;
|
|
493
|
+
const dispatch = async (i) => {
|
|
494
|
+
if (i <= index) {
|
|
495
|
+
throw new Error("next() called multiple times in giri middleware.");
|
|
591
496
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
const prefix = `${key}/`;
|
|
598
|
-
if (request.startsWith(prefix)) {
|
|
599
|
-
return request.slice(prefix.length);
|
|
600
|
-
}
|
|
601
|
-
return void 0;
|
|
602
|
-
}
|
|
603
|
-
function resolveAliasRequest(request, alias, cwd) {
|
|
604
|
-
for (const [key, value] of Object.entries(alias ?? {})) {
|
|
605
|
-
const capture = matchAlias(request, key);
|
|
606
|
-
if (capture === void 0) {
|
|
607
|
-
continue;
|
|
497
|
+
index = i;
|
|
498
|
+
if (i === middleware.length) {
|
|
499
|
+
result = await handle(context);
|
|
500
|
+
return result;
|
|
608
501
|
}
|
|
609
|
-
const [
|
|
610
|
-
if (
|
|
611
|
-
|
|
502
|
+
const returned = await middleware[i](context, () => dispatch(i + 1));
|
|
503
|
+
if (returned !== void 0) {
|
|
504
|
+
result = returned;
|
|
505
|
+
return returned;
|
|
612
506
|
}
|
|
613
|
-
return
|
|
507
|
+
return result;
|
|
508
|
+
};
|
|
509
|
+
await dispatch(0);
|
|
510
|
+
if (result === void 0) {
|
|
511
|
+
throw new Error("Route completed without returning a response.");
|
|
614
512
|
}
|
|
615
|
-
return
|
|
513
|
+
return result;
|
|
616
514
|
}
|
|
617
|
-
function
|
|
618
|
-
if (
|
|
619
|
-
return
|
|
620
|
-
};
|
|
515
|
+
function defineMiddleware(optionsOrMiddleware, maybeMiddleware) {
|
|
516
|
+
if (typeof optionsOrMiddleware === "function") {
|
|
517
|
+
return optionsOrMiddleware;
|
|
621
518
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
|
|
625
|
-
return originalResolveFilename.call(
|
|
626
|
-
this,
|
|
627
|
-
resolveAliasRequest(request, alias, cwd) ?? request,
|
|
628
|
-
parent,
|
|
629
|
-
isMain,
|
|
630
|
-
options
|
|
631
|
-
);
|
|
632
|
-
};
|
|
633
|
-
return () => {
|
|
634
|
-
moduleWithResolver._resolveFilename = originalResolveFilename;
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
var GIRI_ALIAS_PREFIX = "$giri/";
|
|
638
|
-
var giriOutDir;
|
|
639
|
-
var giriResolverInstalled = false;
|
|
640
|
-
function ensureGiriAliasResolver(outDir) {
|
|
641
|
-
giriOutDir = outDir;
|
|
642
|
-
if (giriResolverInstalled) {
|
|
643
|
-
return;
|
|
519
|
+
if (!maybeMiddleware) {
|
|
520
|
+
throw new Error("defineMiddleware(options, middleware) requires a middleware function.");
|
|
644
521
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
|
648
|
-
moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
|
|
649
|
-
const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
|
|
650
|
-
return originalResolveFilename.call(this, mapped, parent, isMain, options);
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
function resolveGiriPaths(config, cwd = process.cwd()) {
|
|
654
|
-
return {
|
|
655
|
-
cwd: (0, import_node_path3.resolve)(cwd),
|
|
656
|
-
routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
|
|
657
|
-
outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
|
|
658
|
-
};
|
|
522
|
+
maybeMiddleware.openapi = optionsOrMiddleware.openapi;
|
|
523
|
+
return maybeMiddleware;
|
|
659
524
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const routes = await scanRoutes(paths.routesDir);
|
|
663
|
-
const app = config.adapter.createApp();
|
|
664
|
-
ensureGiriAliasResolver(paths.outDir);
|
|
665
|
-
const { unregister } = await safeRegister();
|
|
666
|
-
const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
|
|
667
|
-
try {
|
|
668
|
-
for (const route of routes) {
|
|
669
|
-
const routeModule = loadModule(route.file);
|
|
670
|
-
if (typeof routeModule.handle !== "function") {
|
|
671
|
-
throw new Error(`${route.file} must export a named handle function.`);
|
|
672
|
-
}
|
|
673
|
-
const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
|
|
674
|
-
(file) => normalizeMiddleware(loadModule(file).middleware, file)
|
|
675
|
-
);
|
|
676
|
-
const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
|
|
677
|
-
config.adapter.register(app, {
|
|
678
|
-
method: route.method,
|
|
679
|
-
path: route.path,
|
|
680
|
-
handle: routeModule.handle,
|
|
681
|
-
middleware: [...folderMiddleware, ...verbMiddleware],
|
|
682
|
-
input: routeInput(routeModule, route.file),
|
|
683
|
-
services: options.services
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
} finally {
|
|
687
|
-
unregisterAliasResolver();
|
|
688
|
-
unregister();
|
|
689
|
-
}
|
|
690
|
-
return { app, routes, paths };
|
|
525
|
+
function stack(...middleware) {
|
|
526
|
+
return middleware;
|
|
691
527
|
}
|
|
692
528
|
|
|
693
|
-
// src/
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
var import_node_path11 = require("path");
|
|
697
|
-
|
|
698
|
-
// src/generator/app-types.ts
|
|
699
|
-
var import_node_fs4 = require("fs");
|
|
700
|
-
var import_node_path5 = require("path");
|
|
701
|
-
|
|
702
|
-
// src/generator/util.ts
|
|
703
|
-
var import_node_fs3 = require("fs");
|
|
704
|
-
var import_promises2 = require("fs/promises");
|
|
705
|
-
var import_node_path4 = require("path");
|
|
706
|
-
var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
|
|
707
|
-
function slash(path) {
|
|
708
|
-
return path.split(import_node_path4.sep).join("/");
|
|
709
|
-
}
|
|
710
|
-
function importPath(fromFile, toFile) {
|
|
711
|
-
let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
|
|
712
|
-
if (!path.startsWith(".")) {
|
|
713
|
-
path = `./${path}`;
|
|
714
|
-
}
|
|
715
|
-
return path;
|
|
716
|
-
}
|
|
717
|
-
function relativeConfigPath(fromDir, toPath) {
|
|
718
|
-
let path = slash((0, import_node_path4.relative)(fromDir, toPath));
|
|
719
|
-
if (!path.startsWith(".")) {
|
|
720
|
-
path = `./${path}`;
|
|
721
|
-
}
|
|
722
|
-
return path;
|
|
529
|
+
// src/validation.ts
|
|
530
|
+
function defineInputSchema(schema) {
|
|
531
|
+
return { [inputSchemaBrand]: true, ...schema };
|
|
723
532
|
}
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
return path;
|
|
533
|
+
function isGiriInputSchema(value) {
|
|
534
|
+
return Boolean(
|
|
535
|
+
value && typeof value === "object" && value[inputSchemaBrand] === true
|
|
536
|
+
);
|
|
730
537
|
}
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
|
|
538
|
+
function defineBodySchema(contents) {
|
|
539
|
+
return { [bodySchemaBrand]: true, contents };
|
|
734
540
|
}
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
541
|
+
function isGiriBodySchema(value) {
|
|
542
|
+
return Boolean(
|
|
543
|
+
value && typeof value === "object" && value[bodySchemaBrand] === true
|
|
544
|
+
);
|
|
740
545
|
}
|
|
741
|
-
var
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
546
|
+
var MIME_TO_CONTENT_TYPE = {
|
|
547
|
+
"application/json": "json",
|
|
548
|
+
"multipart/form-data": "form",
|
|
549
|
+
"application/x-www-form-urlencoded": "urlencoded",
|
|
550
|
+
"text/plain": "text"
|
|
551
|
+
};
|
|
552
|
+
function contentTypeFromHeader(header) {
|
|
553
|
+
if (!header) {
|
|
554
|
+
return void 0;
|
|
745
555
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
writeCache.set(path, content);
|
|
556
|
+
const mime = header.split(";", 1)[0].trim().toLowerCase();
|
|
557
|
+
return MIME_TO_CONTENT_TYPE[mime];
|
|
749
558
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
559
|
+
function formDataObject(form) {
|
|
560
|
+
const result = {};
|
|
561
|
+
form.forEach((value, key) => {
|
|
562
|
+
const current = result[key];
|
|
563
|
+
if (current === void 0) {
|
|
564
|
+
result[key] = value;
|
|
565
|
+
} else if (Array.isArray(current)) {
|
|
566
|
+
current.push(value);
|
|
567
|
+
} else {
|
|
568
|
+
result[key] = [current, value];
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
return result;
|
|
753
572
|
}
|
|
754
|
-
async function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
} catch {
|
|
759
|
-
return;
|
|
573
|
+
async function readRawBody(request, contentType) {
|
|
574
|
+
const cloned = request.clone();
|
|
575
|
+
if (contentType === "json") {
|
|
576
|
+
return cloned.json();
|
|
760
577
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (entry.isDirectory()) {
|
|
764
|
-
await pruneDir(full, keep);
|
|
765
|
-
await (0, import_promises2.rmdir)(full).catch(() => {
|
|
766
|
-
});
|
|
767
|
-
} else if (!keep.has(full)) {
|
|
768
|
-
await (0, import_promises2.rm)(full, { force: true });
|
|
769
|
-
writeCache.delete(full);
|
|
770
|
-
}
|
|
578
|
+
if (contentType === "text") {
|
|
579
|
+
return cloned.text();
|
|
771
580
|
}
|
|
581
|
+
return formDataObject(await cloned.formData());
|
|
772
582
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
583
|
+
function queryObject(url) {
|
|
584
|
+
const result = {};
|
|
585
|
+
for (const [key, value] of url.searchParams) {
|
|
586
|
+
const current = result[key];
|
|
587
|
+
if (current === void 0) {
|
|
588
|
+
result[key] = value;
|
|
589
|
+
} else if (Array.isArray(current)) {
|
|
590
|
+
current.push(value);
|
|
591
|
+
} else {
|
|
592
|
+
result[key] = [current, value];
|
|
781
593
|
}
|
|
782
594
|
}
|
|
783
|
-
return
|
|
595
|
+
return result;
|
|
784
596
|
}
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
597
|
+
async function runValidation(schema, value, label) {
|
|
598
|
+
if (!isGiriInputSchema(schema)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
|
|
601
|
+
);
|
|
789
602
|
}
|
|
790
|
-
return
|
|
603
|
+
return schema.validate(value);
|
|
791
604
|
}
|
|
792
|
-
async function
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
await
|
|
797
|
-
|
|
605
|
+
async function prepareRequestInput(request, input) {
|
|
606
|
+
const validated = {};
|
|
607
|
+
if (input?.query) {
|
|
608
|
+
const query = queryObject(new URL(request.url));
|
|
609
|
+
const result = await runValidation(input.query, query, "query");
|
|
610
|
+
if (!result.ok) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
response: createTypedResponse(
|
|
614
|
+
{ message: "Invalid query parameters.", issues: result.issues },
|
|
615
|
+
400,
|
|
616
|
+
"json"
|
|
617
|
+
)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
validated.query = result.value;
|
|
798
621
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
" namespace Giri {",
|
|
806
|
-
" interface Register {",
|
|
807
|
-
` app: typeof import(${JSON.stringify(spec)}) extends {`,
|
|
808
|
-
" init: (...args: any[]) => infer R;",
|
|
809
|
-
" }",
|
|
810
|
-
" ? Awaited<R>",
|
|
811
|
-
" : Record<string, unknown>;",
|
|
812
|
-
" }",
|
|
813
|
-
" }",
|
|
814
|
-
"}",
|
|
815
|
-
"export {};",
|
|
816
|
-
""
|
|
817
|
-
].join("\n")
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// src/generator/manifest.ts
|
|
822
|
-
var import_node_path6 = require("path");
|
|
823
|
-
async function writeManifest(paths, routes, data = {}) {
|
|
824
|
-
const manifest = {
|
|
825
|
-
version: 1,
|
|
826
|
-
routes: routes.map((route) => {
|
|
827
|
-
const responses = data.responsesByFile?.get(route.file);
|
|
828
|
-
const input = data.inputsByFile?.get(route.file);
|
|
829
|
-
const security = data.securityByFile?.get(route.file);
|
|
622
|
+
if (input?.body) {
|
|
623
|
+
const contents = input.body.contents;
|
|
624
|
+
const declared = Object.keys(contents);
|
|
625
|
+
const requested = contentTypeFromHeader(request.headers.get("content-type"));
|
|
626
|
+
const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
|
|
627
|
+
if (!chosen) {
|
|
830
628
|
return {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
|
|
838
|
-
...input ? { input } : {},
|
|
839
|
-
...security && security.security.length > 0 ? { security: security.security } : {},
|
|
840
|
-
responses: responses?.responses ?? [],
|
|
841
|
-
...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
|
|
629
|
+
ok: false,
|
|
630
|
+
response: createTypedResponse(
|
|
631
|
+
{ message: "Unsupported media type.", issues: { accepted: declared } },
|
|
632
|
+
415,
|
|
633
|
+
"json"
|
|
634
|
+
)
|
|
842
635
|
};
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
636
|
+
}
|
|
637
|
+
let rawBody;
|
|
638
|
+
try {
|
|
639
|
+
rawBody = await readRawBody(request, chosen);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
return {
|
|
642
|
+
ok: false,
|
|
643
|
+
response: createTypedResponse(
|
|
644
|
+
{ message: "Invalid request body.", issues: error },
|
|
645
|
+
400,
|
|
646
|
+
"json"
|
|
647
|
+
)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const result = await runValidation(contents[chosen], rawBody, "body");
|
|
651
|
+
if (!result.ok) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
response: createTypedResponse(
|
|
655
|
+
{ message: "Invalid request body.", issues: result.issues },
|
|
656
|
+
400,
|
|
657
|
+
"json"
|
|
658
|
+
)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
|
|
662
|
+
}
|
|
663
|
+
return { ok: true, validated };
|
|
846
664
|
}
|
|
847
665
|
|
|
848
|
-
// src/
|
|
849
|
-
var
|
|
850
|
-
var
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
666
|
+
// src/app.ts
|
|
667
|
+
var import_node_module = __toESM(require("module"));
|
|
668
|
+
var import_node_path3 = require("path");
|
|
669
|
+
|
|
670
|
+
// src/loader/loader.ts
|
|
671
|
+
var import_prompts = require("@clack/prompts");
|
|
672
|
+
var import_node_fs = require("fs");
|
|
673
|
+
var import_node_path = require("path");
|
|
674
|
+
var import_node_process = require("process");
|
|
675
|
+
|
|
676
|
+
// src/config/schema.ts
|
|
677
|
+
var import_typebox = require("@sinclair/typebox");
|
|
678
|
+
var configSchema = import_typebox.Type.Object({
|
|
679
|
+
adapter: import_typebox.Type.Any(),
|
|
680
|
+
alias: import_typebox.Type.Optional(import_typebox.Type.Record(
|
|
681
|
+
import_typebox.Type.String(),
|
|
682
|
+
import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
|
|
683
|
+
)),
|
|
684
|
+
outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
|
|
685
|
+
server: import_typebox.Type.Optional(import_typebox.Type.Object({
|
|
686
|
+
port: import_typebox.Type.Optional(import_typebox.Type.Number()),
|
|
687
|
+
hostname: import_typebox.Type.Optional(import_typebox.Type.String())
|
|
688
|
+
}, { additionalProperties: false })),
|
|
689
|
+
errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
|
|
690
|
+
}, { additionalProperties: false });
|
|
691
|
+
|
|
692
|
+
// src/loader/loader.ts
|
|
693
|
+
var import_value = require("@sinclair/typebox/value");
|
|
694
|
+
var assertES5 = async (unregister) => {
|
|
695
|
+
try {
|
|
696
|
+
init_es5();
|
|
697
|
+
} catch (e) {
|
|
698
|
+
if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
|
|
699
|
+
const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
|
|
700
|
+
if (es5Error) {
|
|
701
|
+
import_prompts.log.error(
|
|
702
|
+
`Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
|
|
703
|
+
);
|
|
704
|
+
(0, import_node_process.exit)(1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
import_prompts.log.error(e);
|
|
708
|
+
(0, import_node_process.exit)(1);
|
|
870
709
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
710
|
+
};
|
|
711
|
+
var safeRegister = async () => {
|
|
712
|
+
const { register } = await import("esbuild-register/dist/node");
|
|
713
|
+
let res;
|
|
714
|
+
try {
|
|
715
|
+
res = register({
|
|
716
|
+
format: "cjs",
|
|
717
|
+
loader: "ts"
|
|
718
|
+
});
|
|
719
|
+
} catch {
|
|
720
|
+
res = {
|
|
721
|
+
unregister: () => {
|
|
878
722
|
}
|
|
879
|
-
}
|
|
880
|
-
return out;
|
|
723
|
+
};
|
|
881
724
|
}
|
|
882
|
-
|
|
725
|
+
await assertES5(res.unregister);
|
|
726
|
+
return res;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// src/routes.ts
|
|
730
|
+
var import_node_fs2 = require("fs");
|
|
731
|
+
var import_promises = require("fs/promises");
|
|
732
|
+
var import_node_path2 = require("path");
|
|
733
|
+
var import_tinyglobby = require("tinyglobby");
|
|
734
|
+
var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
735
|
+
var METHOD_FROM_FILE = new Map(
|
|
736
|
+
METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
|
|
737
|
+
);
|
|
738
|
+
function normalizeSlashes(path) {
|
|
739
|
+
return path.split(import_node_path2.sep).join("/");
|
|
883
740
|
}
|
|
884
|
-
function
|
|
885
|
-
return
|
|
741
|
+
function isRouteSourceFile(fileName) {
|
|
742
|
+
return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
|
|
886
743
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
urlencoded: "application/x-www-form-urlencoded",
|
|
891
|
-
text: "text/plain"
|
|
892
|
-
};
|
|
893
|
-
function buildResponses(responses) {
|
|
894
|
-
if (responses.length === 0) {
|
|
895
|
-
return { default: { description: "Response" } };
|
|
896
|
-
}
|
|
897
|
-
const out = {};
|
|
898
|
-
for (const response of responses) {
|
|
899
|
-
const key = response.status === "default" ? "default" : String(response.status);
|
|
900
|
-
const description = typeof response.status === "number" && REASON[response.status] || "Response";
|
|
901
|
-
out[key] = {
|
|
902
|
-
description,
|
|
903
|
-
content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
|
|
904
|
-
};
|
|
744
|
+
function methodFromFile(fileName) {
|
|
745
|
+
if (!isRouteSourceFile(fileName)) {
|
|
746
|
+
return void 0;
|
|
905
747
|
}
|
|
906
|
-
|
|
748
|
+
const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
|
|
749
|
+
return METHOD_FROM_FILE.get(stem);
|
|
907
750
|
}
|
|
908
|
-
function
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
continue;
|
|
751
|
+
function sharedFileIn(dir) {
|
|
752
|
+
for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
|
|
753
|
+
const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
|
|
754
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
755
|
+
return file;
|
|
914
756
|
}
|
|
915
|
-
seen.add(param.name);
|
|
916
|
-
params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
|
|
917
757
|
}
|
|
918
|
-
return
|
|
758
|
+
return void 0;
|
|
919
759
|
}
|
|
920
|
-
function
|
|
921
|
-
|
|
760
|
+
function physicalRouteSegments(routesDir, routeDir) {
|
|
761
|
+
const rel = (0, import_node_path2.relative)(routesDir, routeDir);
|
|
762
|
+
if (!rel) {
|
|
922
763
|
return [];
|
|
923
764
|
}
|
|
924
|
-
|
|
925
|
-
const required = Array.isArray(query.required) ? query.required : [];
|
|
926
|
-
return Object.entries(properties).map(([name, schema]) => ({
|
|
927
|
-
name,
|
|
928
|
-
in: "query",
|
|
929
|
-
required: required.includes(name),
|
|
930
|
-
schema: rewriteRefs(schema)
|
|
931
|
-
}));
|
|
765
|
+
return normalizeSlashes(rel).split("/").filter(Boolean);
|
|
932
766
|
}
|
|
933
|
-
function
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
try {
|
|
937
|
-
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
|
|
938
|
-
return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
|
|
939
|
-
} catch {
|
|
940
|
-
}
|
|
767
|
+
function urlSegment(segment) {
|
|
768
|
+
if (/^\(.+\)$/.test(segment)) {
|
|
769
|
+
return {};
|
|
941
770
|
}
|
|
942
|
-
|
|
771
|
+
const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
|
|
772
|
+
if (catchAll) {
|
|
773
|
+
const name = catchAll[1];
|
|
774
|
+
return {
|
|
775
|
+
value: `:${name}{.*}`,
|
|
776
|
+
param: { name, catchAll: true }
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const param = /^\[(.+)\]$/.exec(segment);
|
|
780
|
+
if (param) {
|
|
781
|
+
const name = param[1];
|
|
782
|
+
return {
|
|
783
|
+
value: `:${name}`,
|
|
784
|
+
param: { name, catchAll: false }
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return { value: segment };
|
|
943
788
|
}
|
|
944
|
-
function
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
const responses = data.responsesByFile?.get(route.file);
|
|
953
|
-
const input = data.inputsByFile?.get(route.file);
|
|
954
|
-
const security = data.securityByFile?.get(route.file);
|
|
955
|
-
for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
|
|
956
|
-
schemas[name] = rewriteRefs(schema);
|
|
957
|
-
}
|
|
958
|
-
const operation = { responses: buildResponses(responses?.responses ?? []) };
|
|
959
|
-
const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
|
|
960
|
-
if (parameters.length > 0) {
|
|
961
|
-
operation.parameters = parameters;
|
|
962
|
-
}
|
|
963
|
-
if (input?.body) {
|
|
964
|
-
const content = {};
|
|
965
|
-
for (const [contentType, schema] of Object.entries(input.body)) {
|
|
966
|
-
content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
|
|
967
|
-
schema: rewriteRefs(schema)
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
if (Object.keys(content).length > 0) {
|
|
971
|
-
operation.requestBody = { required: true, content };
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
if (security && security.security.length > 0) {
|
|
975
|
-
operation.security = security.security;
|
|
789
|
+
function pathFromSegments(segments) {
|
|
790
|
+
const pathSegments = [];
|
|
791
|
+
const params = [];
|
|
792
|
+
for (const segment of segments) {
|
|
793
|
+
const converted = urlSegment(segment);
|
|
794
|
+
if (converted.value) {
|
|
795
|
+
pathSegments.push(converted.value);
|
|
976
796
|
}
|
|
977
|
-
if (
|
|
978
|
-
|
|
797
|
+
if (converted.param) {
|
|
798
|
+
params.push(converted.param);
|
|
979
799
|
}
|
|
980
|
-
const openApiPath = toOpenApiPath(route.path);
|
|
981
|
-
const pathItem = documentPaths[openApiPath] ?? {};
|
|
982
|
-
pathItem[route.method.toLowerCase()] = operation;
|
|
983
|
-
documentPaths[openApiPath] = pathItem;
|
|
984
800
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
paths: documentPaths
|
|
801
|
+
return {
|
|
802
|
+
path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
|
|
803
|
+
params
|
|
989
804
|
};
|
|
990
|
-
const components = {};
|
|
991
|
-
if (Object.keys(schemas).length > 0) {
|
|
992
|
-
components.schemas = schemas;
|
|
993
|
-
}
|
|
994
|
-
if (Object.keys(securitySchemes).length > 0) {
|
|
995
|
-
components.securitySchemes = securitySchemes;
|
|
996
|
-
}
|
|
997
|
-
if (Object.keys(components).length > 0) {
|
|
998
|
-
document.components = components;
|
|
999
|
-
}
|
|
1000
|
-
return document;
|
|
1001
|
-
}
|
|
1002
|
-
async function writeOpenApi(paths, routes, data = {}) {
|
|
1003
|
-
await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// src/generator/param-types.ts
|
|
1007
|
-
var import_node_path8 = require("path");
|
|
1008
|
-
function paramsType(params) {
|
|
1009
|
-
if (params.length === 0) {
|
|
1010
|
-
return "{}";
|
|
1011
|
-
}
|
|
1012
|
-
const unique = /* @__PURE__ */ new Map();
|
|
1013
|
-
for (const param of params) {
|
|
1014
|
-
unique.set(param.name, param);
|
|
1015
|
-
}
|
|
1016
|
-
const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
|
|
1017
|
-
return `{
|
|
1018
|
-
${fields}
|
|
1019
|
-
}`;
|
|
1020
805
|
}
|
|
1021
|
-
function
|
|
1022
|
-
if (
|
|
1023
|
-
return
|
|
806
|
+
async function scanRouteFolders(routesDir) {
|
|
807
|
+
if (!(0, import_node_fs2.existsSync)(routesDir)) {
|
|
808
|
+
return [];
|
|
1024
809
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
|
|
1034
|
-
const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
|
|
1035
|
-
return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
async function writeParamTypes(paths, folders) {
|
|
1039
|
-
for (const { dir, params, sharedFiles, verbs } of folders) {
|
|
1040
|
-
const file = typeFilePath(paths, dir);
|
|
1041
|
-
const typesDir = (0, import_node_path8.dirname)(file);
|
|
1042
|
-
const lines = [
|
|
1043
|
-
GENERATED_HEADER,
|
|
1044
|
-
`export type Params = ${paramsType(params)};`,
|
|
1045
|
-
"export type RouteParams = Params;",
|
|
1046
|
-
`type Vars = ${varsType(typesDir, sharedFiles)};`,
|
|
1047
|
-
"export type Middleware<Injects extends Record<string, unknown> = {}> =",
|
|
1048
|
-
' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
|
|
1049
|
-
'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
|
|
1050
|
-
' import("@boon4681/giri").Handle<Params, Input, Vars>;'
|
|
1051
|
-
];
|
|
1052
|
-
if (verbs.length > 0) {
|
|
1053
|
-
lines.push(...methodExports(typesDir, verbs));
|
|
810
|
+
const folders = [routesDir];
|
|
811
|
+
const walk = async (dir) => {
|
|
812
|
+
for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
|
|
813
|
+
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
814
|
+
const full = (0, import_node_path2.join)(dir, entry.name);
|
|
815
|
+
folders.push(full);
|
|
816
|
+
await walk(full);
|
|
817
|
+
}
|
|
1054
818
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// src/generator/inputs.ts
|
|
1061
|
-
function sanitize(schema) {
|
|
1062
|
-
const { $schema, ...rest } = schema;
|
|
1063
|
-
void $schema;
|
|
1064
|
-
return rest;
|
|
819
|
+
};
|
|
820
|
+
await walk(routesDir);
|
|
821
|
+
return folders;
|
|
1065
822
|
}
|
|
1066
|
-
function
|
|
1067
|
-
|
|
1068
|
-
|
|
823
|
+
function routeParamsForDir(routesDir, dir) {
|
|
824
|
+
return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
|
|
825
|
+
}
|
|
826
|
+
function sharedFilesForDir(routesDir, dir) {
|
|
827
|
+
const segments = physicalRouteSegments(routesDir, dir);
|
|
828
|
+
const dirs = [routesDir];
|
|
829
|
+
let current = routesDir;
|
|
830
|
+
for (const segment of segments) {
|
|
831
|
+
current = (0, import_node_path2.join)(current, segment);
|
|
832
|
+
dirs.push(current);
|
|
1069
833
|
}
|
|
1070
|
-
return
|
|
834
|
+
return dirs.map(sharedFileIn).filter((file) => Boolean(file));
|
|
1071
835
|
}
|
|
1072
|
-
function
|
|
1073
|
-
if (!
|
|
1074
|
-
return
|
|
836
|
+
async function scanRoutes(routesDir) {
|
|
837
|
+
if (!(0, import_node_fs2.existsSync)(routesDir)) {
|
|
838
|
+
return [];
|
|
1075
839
|
}
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
840
|
+
const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
|
|
841
|
+
cwd: routesDir,
|
|
842
|
+
absolute: true,
|
|
843
|
+
onlyFiles: true
|
|
844
|
+
});
|
|
845
|
+
const routes = [];
|
|
846
|
+
for (const file of files) {
|
|
847
|
+
const method = methodFromFile((0, import_node_path2.basename)(file));
|
|
848
|
+
if (!method) {
|
|
849
|
+
continue;
|
|
1081
850
|
}
|
|
851
|
+
const routeDir = (0, import_node_path2.dirname)(file);
|
|
852
|
+
const routeSegments = physicalRouteSegments(routesDir, routeDir);
|
|
853
|
+
const { path, params } = pathFromSegments(routeSegments);
|
|
854
|
+
routes.push({
|
|
855
|
+
method,
|
|
856
|
+
path,
|
|
857
|
+
file,
|
|
858
|
+
routeDir,
|
|
859
|
+
routeSegments,
|
|
860
|
+
params,
|
|
861
|
+
sharedFiles: sharedFilesForDir(routesDir, routeDir)
|
|
862
|
+
});
|
|
1082
863
|
}
|
|
1083
|
-
return
|
|
864
|
+
return routes.sort((left, right) => {
|
|
865
|
+
const pathOrder = left.path.localeCompare(right.path);
|
|
866
|
+
if (pathOrder !== 0) {
|
|
867
|
+
return pathOrder;
|
|
868
|
+
}
|
|
869
|
+
return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
|
|
870
|
+
});
|
|
1084
871
|
}
|
|
1085
872
|
|
|
1086
|
-
// src/
|
|
1087
|
-
function
|
|
873
|
+
// src/app.ts
|
|
874
|
+
function loadModule(file) {
|
|
1088
875
|
const resolved = require.resolve(file);
|
|
1089
876
|
delete require.cache[resolved];
|
|
1090
877
|
return require(resolved);
|
|
1091
878
|
}
|
|
1092
|
-
function
|
|
879
|
+
function interopDefault(value) {
|
|
1093
880
|
if (value && typeof value === "object" && "default" in value) {
|
|
1094
881
|
return value.default;
|
|
1095
882
|
}
|
|
1096
883
|
return value;
|
|
1097
884
|
}
|
|
1098
|
-
function
|
|
1099
|
-
const exported =
|
|
885
|
+
function normalizeMiddleware(value, file) {
|
|
886
|
+
const exported = interopDefault(value);
|
|
887
|
+
if (exported === void 0) {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
1100
890
|
if (typeof exported === "function") {
|
|
1101
891
|
return [exported];
|
|
1102
892
|
}
|
|
1103
893
|
if (Array.isArray(exported)) {
|
|
1104
|
-
|
|
894
|
+
for (const middleware of exported) {
|
|
895
|
+
if (typeof middleware !== "function") {
|
|
896
|
+
throw new Error(`Middleware export in ${file} must contain only functions.`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return exported;
|
|
1105
900
|
}
|
|
1106
|
-
|
|
901
|
+
throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
|
|
1107
902
|
}
|
|
1108
|
-
function
|
|
903
|
+
function assertBodySchema(value, file) {
|
|
904
|
+
if (!isGiriBodySchema(value)) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function assertQuerySchema(value, file) {
|
|
911
|
+
if (!isGiriInputSchema(value)) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function routeInput(routeModule, file) {
|
|
1109
918
|
const input = {};
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
input.body = body;
|
|
919
|
+
if (routeModule.body !== void 0) {
|
|
920
|
+
assertBodySchema(routeModule.body, file);
|
|
921
|
+
input.body = routeModule.body;
|
|
1114
922
|
}
|
|
1115
|
-
if (query) {
|
|
1116
|
-
|
|
923
|
+
if (routeModule.query !== void 0) {
|
|
924
|
+
assertQuerySchema(routeModule.query, file);
|
|
925
|
+
input.query = routeModule.query;
|
|
1117
926
|
}
|
|
1118
927
|
return input.body || input.query ? input : void 0;
|
|
1119
928
|
}
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
|
|
929
|
+
function aliasValues(value) {
|
|
930
|
+
return Array.isArray(value) ? value : [value];
|
|
931
|
+
}
|
|
932
|
+
function resolveAliasTarget(cwd, target, capture = "") {
|
|
933
|
+
const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
|
|
934
|
+
return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
|
|
935
|
+
}
|
|
936
|
+
function matchAlias(request, key) {
|
|
937
|
+
if (key.includes("*")) {
|
|
938
|
+
const [prefix2, suffix = ""] = key.split("*");
|
|
939
|
+
if (request.startsWith(prefix2) && request.endsWith(suffix)) {
|
|
940
|
+
return request.slice(prefix2.length, request.length - suffix.length);
|
|
941
|
+
}
|
|
942
|
+
return void 0;
|
|
1123
943
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
return
|
|
944
|
+
if (request === key) {
|
|
945
|
+
return "";
|
|
1126
946
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
947
|
+
const prefix = `${key}/`;
|
|
948
|
+
if (request.startsWith(prefix)) {
|
|
949
|
+
return request.slice(prefix.length);
|
|
1129
950
|
}
|
|
1130
951
|
return void 0;
|
|
1131
952
|
}
|
|
1132
|
-
function
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
953
|
+
function resolveAliasRequest(request, alias, cwd) {
|
|
954
|
+
for (const [key, value] of Object.entries(alias ?? {})) {
|
|
955
|
+
const capture = matchAlias(request, key);
|
|
956
|
+
if (capture === void 0) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const [target] = aliasValues(value);
|
|
960
|
+
if (!target) {
|
|
961
|
+
continue;
|
|
1138
962
|
}
|
|
963
|
+
return resolveAliasTarget(cwd, target, capture);
|
|
1139
964
|
}
|
|
1140
|
-
|
|
1141
|
-
return verb ?? hidden;
|
|
965
|
+
return void 0;
|
|
1142
966
|
}
|
|
1143
|
-
function
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const middleware = [];
|
|
1148
|
-
if (!skipInherited) {
|
|
1149
|
-
for (const file of route.sharedFiles) {
|
|
1150
|
-
middleware.push(...middlewareFunctions(loadShared(file).middleware));
|
|
1151
|
-
}
|
|
967
|
+
function registerAliasResolver(alias, cwd) {
|
|
968
|
+
if (!alias || Object.keys(alias).length === 0) {
|
|
969
|
+
return () => {
|
|
970
|
+
};
|
|
1152
971
|
}
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
972
|
+
const moduleWithResolver = import_node_module.default;
|
|
973
|
+
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
|
974
|
+
moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
|
|
975
|
+
return originalResolveFilename.call(
|
|
976
|
+
this,
|
|
977
|
+
resolveAliasRequest(request, alias, cwd) ?? request,
|
|
978
|
+
parent,
|
|
979
|
+
isMain,
|
|
980
|
+
options
|
|
981
|
+
);
|
|
982
|
+
};
|
|
983
|
+
return () => {
|
|
984
|
+
moduleWithResolver._resolveFilename = originalResolveFilename;
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
var GIRI_ALIAS_PREFIX = "$giri/";
|
|
988
|
+
var giriOutDir;
|
|
989
|
+
var giriResolverInstalled = false;
|
|
990
|
+
function ensureGiriAliasResolver(outDir) {
|
|
991
|
+
giriOutDir = outDir;
|
|
992
|
+
if (giriResolverInstalled) {
|
|
993
|
+
return;
|
|
1168
994
|
}
|
|
1169
|
-
|
|
995
|
+
giriResolverInstalled = true;
|
|
996
|
+
const moduleWithResolver = import_node_module.default;
|
|
997
|
+
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
|
998
|
+
moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
|
|
999
|
+
const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
|
|
1000
|
+
return originalResolveFilename.call(this, mapped, parent, isMain, options);
|
|
1001
|
+
};
|
|
1170
1002
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const loadShared = (file) => {
|
|
1177
|
-
if (!sharedCache.has(file)) {
|
|
1178
|
-
try {
|
|
1179
|
-
sharedCache.set(file, loadModule2(file));
|
|
1180
|
-
} catch {
|
|
1181
|
-
sharedCache.set(file, {});
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return sharedCache.get(file);
|
|
1003
|
+
function resolveGiriPaths(config, cwd = process.cwd()) {
|
|
1004
|
+
return {
|
|
1005
|
+
cwd: (0, import_node_path3.resolve)(cwd),
|
|
1006
|
+
routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
|
|
1007
|
+
outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
|
|
1185
1008
|
};
|
|
1009
|
+
}
|
|
1010
|
+
async function buildGiriApp(config, options = {}) {
|
|
1011
|
+
const paths = resolveGiriPaths(config, options.cwd);
|
|
1012
|
+
const routes = await scanRoutes(paths.routesDir);
|
|
1013
|
+
const app = config.adapter.createApp();
|
|
1014
|
+
ensureGiriAliasResolver(paths.outDir);
|
|
1015
|
+
const { unregister } = await safeRegister();
|
|
1016
|
+
const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
|
|
1186
1017
|
try {
|
|
1187
1018
|
for (const route of routes) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const input = readInput(routeModule);
|
|
1192
|
-
const security = collectSecurity(route, routeModule, loadShared);
|
|
1193
|
-
const hidden = collectHidden(route, routeModule, loadShared);
|
|
1194
|
-
if (input) {
|
|
1195
|
-
meta.input = input;
|
|
1196
|
-
}
|
|
1197
|
-
if (security) {
|
|
1198
|
-
meta.security = security;
|
|
1199
|
-
}
|
|
1200
|
-
if (hidden) {
|
|
1201
|
-
meta.hidden = true;
|
|
1202
|
-
}
|
|
1203
|
-
if (meta.input || meta.security || meta.hidden) {
|
|
1204
|
-
byFile.set(route.file, meta);
|
|
1205
|
-
}
|
|
1206
|
-
} catch {
|
|
1019
|
+
const routeModule = loadModule(route.file);
|
|
1020
|
+
if (typeof routeModule.handle !== "function") {
|
|
1021
|
+
throw new Error(`${route.file} must export a named handle function.`);
|
|
1207
1022
|
}
|
|
1023
|
+
const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
|
|
1024
|
+
(file) => normalizeMiddleware(loadModule(file).middleware, file)
|
|
1025
|
+
);
|
|
1026
|
+
const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
|
|
1027
|
+
config.adapter.register(app, {
|
|
1028
|
+
method: route.method,
|
|
1029
|
+
path: route.path,
|
|
1030
|
+
handle: routeModule.handle,
|
|
1031
|
+
middleware: [...folderMiddleware, ...verbMiddleware],
|
|
1032
|
+
input: routeInput(routeModule, route.file),
|
|
1033
|
+
services: options.services
|
|
1034
|
+
});
|
|
1208
1035
|
}
|
|
1209
1036
|
} finally {
|
|
1210
|
-
|
|
1037
|
+
unregisterAliasResolver();
|
|
1211
1038
|
unregister();
|
|
1212
1039
|
}
|
|
1213
|
-
return
|
|
1040
|
+
return { app, routes, paths };
|
|
1214
1041
|
}
|
|
1215
1042
|
|
|
1216
|
-
// src/generator/
|
|
1217
|
-
var
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1043
|
+
// src/generator/sync.ts
|
|
1044
|
+
var import_node_fs6 = require("fs");
|
|
1045
|
+
var import_promises3 = require("fs/promises");
|
|
1046
|
+
var import_node_path11 = require("path");
|
|
1047
|
+
|
|
1048
|
+
// src/generator/app-types.ts
|
|
1049
|
+
var import_node_fs4 = require("fs");
|
|
1050
|
+
var import_node_path5 = require("path");
|
|
1051
|
+
|
|
1052
|
+
// src/generator/util.ts
|
|
1053
|
+
var import_node_fs3 = require("fs");
|
|
1054
|
+
var import_promises2 = require("fs/promises");
|
|
1055
|
+
var import_node_path4 = require("path");
|
|
1056
|
+
var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
|
|
1057
|
+
function slash(path) {
|
|
1058
|
+
return path.split(import_node_path4.sep).join("/");
|
|
1059
|
+
}
|
|
1060
|
+
function importPath(fromFile, toFile) {
|
|
1061
|
+
let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
|
|
1062
|
+
if (!path.startsWith(".")) {
|
|
1063
|
+
path = `./${path}`;
|
|
1231
1064
|
}
|
|
1232
|
-
|
|
1233
|
-
await writeGenerated(file, lines.join("\n"));
|
|
1065
|
+
return path;
|
|
1234
1066
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1067
|
+
function relativeConfigPath(fromDir, toPath) {
|
|
1068
|
+
let path = slash((0, import_node_path4.relative)(fromDir, toPath));
|
|
1069
|
+
if (!path.startsWith(".")) {
|
|
1070
|
+
path = `./${path}`;
|
|
1071
|
+
}
|
|
1072
|
+
return path;
|
|
1073
|
+
}
|
|
1074
|
+
function moduleSpecifier(fromDir, target) {
|
|
1075
|
+
let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
1076
|
+
if (!path.startsWith(".")) {
|
|
1077
|
+
path = `./${path}`;
|
|
1078
|
+
}
|
|
1079
|
+
return path;
|
|
1080
|
+
}
|
|
1081
|
+
function typeFilePath(paths, routeDir) {
|
|
1082
|
+
const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
|
|
1083
|
+
return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
|
|
1084
|
+
}
|
|
1085
|
+
function assertSafeOutDir(paths) {
|
|
1086
|
+
const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
|
|
1087
|
+
if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
|
|
1088
|
+
throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
var writeCache = /* @__PURE__ */ new Map();
|
|
1092
|
+
async function writeGenerated(path, content) {
|
|
1093
|
+
if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
1097
|
+
await (0, import_promises2.writeFile)(path, content);
|
|
1098
|
+
writeCache.set(path, content);
|
|
1099
|
+
}
|
|
1100
|
+
async function writeJson(path, value) {
|
|
1101
|
+
await writeGenerated(path, `${JSON.stringify(value, null, 2)}
|
|
1102
|
+
`);
|
|
1103
|
+
}
|
|
1104
|
+
async function pruneDir(dir, keep) {
|
|
1105
|
+
let entries;
|
|
1106
|
+
try {
|
|
1107
|
+
entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
|
|
1108
|
+
} catch {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
for (const entry of entries) {
|
|
1112
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
1113
|
+
if (entry.isDirectory()) {
|
|
1114
|
+
await pruneDir(full, keep);
|
|
1115
|
+
await (0, import_promises2.rmdir)(full).catch(() => {
|
|
1116
|
+
});
|
|
1117
|
+
} else if (!keep.has(full)) {
|
|
1118
|
+
await (0, import_promises2.rm)(full, { force: true });
|
|
1119
|
+
writeCache.delete(full);
|
|
1257
1120
|
}
|
|
1258
1121
|
}
|
|
1259
|
-
return import_typescript.default.createProgram(routeFiles, options);
|
|
1260
1122
|
}
|
|
1261
1123
|
|
|
1262
|
-
// src/generator/
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
inProgress: /* @__PURE__ */ new Map(),
|
|
1273
|
-
usedDefs: /* @__PURE__ */ new Set(),
|
|
1274
|
-
warnings: []
|
|
1275
|
-
};
|
|
1124
|
+
// src/generator/app-types.ts
|
|
1125
|
+
var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
|
|
1126
|
+
function findMainFile(cwd) {
|
|
1127
|
+
for (const ext of MAIN_EXTENSIONS) {
|
|
1128
|
+
const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
|
|
1129
|
+
if ((0, import_node_fs4.existsSync)(file)) {
|
|
1130
|
+
return file;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return void 0;
|
|
1276
1134
|
}
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1135
|
+
function moduleSpecifier2(fromDir, target) {
|
|
1136
|
+
let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
1137
|
+
if (!path.startsWith(".")) {
|
|
1138
|
+
path = `./${path}`;
|
|
1139
|
+
}
|
|
1140
|
+
return path;
|
|
1279
1141
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1142
|
+
async function writeAppTypes(paths) {
|
|
1143
|
+
const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
|
|
1144
|
+
const mainFile = findMainFile(paths.cwd);
|
|
1145
|
+
if (!mainFile) {
|
|
1146
|
+
await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
|
|
1150
|
+
await writeGenerated(
|
|
1151
|
+
file,
|
|
1152
|
+
[
|
|
1153
|
+
GENERATED_HEADER,
|
|
1154
|
+
"declare global {",
|
|
1155
|
+
" namespace Giri {",
|
|
1156
|
+
" interface Register {",
|
|
1157
|
+
` app: typeof import(${JSON.stringify(spec)}) extends {`,
|
|
1158
|
+
" init: (...args: any[]) => infer R;",
|
|
1159
|
+
" }",
|
|
1160
|
+
" ? Awaited<R>",
|
|
1161
|
+
" : Record<string, unknown>;",
|
|
1162
|
+
" }",
|
|
1163
|
+
" }",
|
|
1164
|
+
"}",
|
|
1165
|
+
"export {};",
|
|
1166
|
+
""
|
|
1167
|
+
].join("\n")
|
|
1168
|
+
);
|
|
1282
1169
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1170
|
+
|
|
1171
|
+
// src/generator/manifest.ts
|
|
1172
|
+
var import_node_path6 = require("path");
|
|
1173
|
+
async function writeManifest(paths, routes, data = {}) {
|
|
1174
|
+
const manifest = {
|
|
1175
|
+
version: 1,
|
|
1176
|
+
routes: routes.map((route) => {
|
|
1177
|
+
const responses = data.responsesByFile?.get(route.file);
|
|
1178
|
+
const input = data.inputsByFile?.get(route.file);
|
|
1179
|
+
const security = data.securityByFile?.get(route.file);
|
|
1180
|
+
return {
|
|
1181
|
+
method: route.method,
|
|
1182
|
+
path: route.path,
|
|
1183
|
+
file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
|
|
1184
|
+
params: route.params,
|
|
1185
|
+
shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
|
|
1186
|
+
types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
|
|
1187
|
+
...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
|
|
1188
|
+
...input ? { input } : {},
|
|
1189
|
+
...security && security.security.length > 0 ? { security: security.security } : {},
|
|
1190
|
+
responses: responses?.responses ?? [],
|
|
1191
|
+
...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
|
|
1192
|
+
};
|
|
1193
|
+
})
|
|
1194
|
+
};
|
|
1195
|
+
await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
|
|
1286
1196
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1197
|
+
|
|
1198
|
+
// src/generator/openapi.ts
|
|
1199
|
+
var import_node_fs5 = require("fs");
|
|
1200
|
+
var import_node_path7 = require("path");
|
|
1201
|
+
var REASON = {
|
|
1202
|
+
200: "OK",
|
|
1203
|
+
201: "Created",
|
|
1204
|
+
202: "Accepted",
|
|
1205
|
+
204: "No Content",
|
|
1206
|
+
400: "Bad Request",
|
|
1207
|
+
401: "Unauthorized",
|
|
1208
|
+
403: "Forbidden",
|
|
1209
|
+
404: "Not Found",
|
|
1210
|
+
409: "Conflict",
|
|
1211
|
+
422: "Unprocessable Entity",
|
|
1212
|
+
500: "Internal Server Error"
|
|
1213
|
+
};
|
|
1214
|
+
function toOpenApiPath(path) {
|
|
1215
|
+
return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
|
|
1216
|
+
}
|
|
1217
|
+
function rewriteRefs(value) {
|
|
1218
|
+
if (Array.isArray(value)) {
|
|
1219
|
+
return value.map(rewriteRefs);
|
|
1220
|
+
}
|
|
1221
|
+
if (value && typeof value === "object") {
|
|
1222
|
+
const out = {};
|
|
1223
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1224
|
+
if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
|
|
1225
|
+
out.$ref = child.replace("#/$defs/", "#/components/schemas/");
|
|
1226
|
+
} else {
|
|
1227
|
+
out[key] = rewriteRefs(child);
|
|
1228
|
+
}
|
|
1296
1229
|
}
|
|
1230
|
+
return out;
|
|
1297
1231
|
}
|
|
1298
|
-
return
|
|
1232
|
+
return value;
|
|
1299
1233
|
}
|
|
1300
|
-
function
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1234
|
+
function mediaTypeFor(format) {
|
|
1235
|
+
return format === "text" ? "text/plain" : "application/json";
|
|
1236
|
+
}
|
|
1237
|
+
var BODY_MEDIA_TYPE = {
|
|
1238
|
+
json: "application/json",
|
|
1239
|
+
form: "multipart/form-data",
|
|
1240
|
+
urlencoded: "application/x-www-form-urlencoded",
|
|
1241
|
+
text: "text/plain"
|
|
1242
|
+
};
|
|
1243
|
+
function buildResponses(responses) {
|
|
1244
|
+
if (responses.length === 0) {
|
|
1245
|
+
return { default: { description: "Response" } };
|
|
1305
1246
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1247
|
+
const out = {};
|
|
1248
|
+
for (const response of responses) {
|
|
1249
|
+
const key = response.status === "default" ? "default" : String(response.status);
|
|
1250
|
+
const description = typeof response.status === "number" && REASON[response.status] || "Response";
|
|
1251
|
+
out[key] = {
|
|
1252
|
+
description,
|
|
1253
|
+
content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
|
|
1254
|
+
};
|
|
1309
1255
|
}
|
|
1310
|
-
return
|
|
1256
|
+
return out;
|
|
1311
1257
|
}
|
|
1312
|
-
function
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const name = symbol.getName();
|
|
1319
|
-
const propType = checker.getTypeOfSymbolAtLocation(symbol, ctx.location);
|
|
1320
|
-
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));
|
|
1321
|
-
properties[name] = walkType(propType, ctx);
|
|
1322
|
-
if (!optional) {
|
|
1323
|
-
required.push(name);
|
|
1258
|
+
function pathParameters(route) {
|
|
1259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1260
|
+
const params = [];
|
|
1261
|
+
for (const param of route.params) {
|
|
1262
|
+
if (seen.has(param.name)) {
|
|
1263
|
+
continue;
|
|
1324
1264
|
}
|
|
1265
|
+
seen.add(param.name);
|
|
1266
|
+
params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
|
|
1325
1267
|
}
|
|
1326
|
-
|
|
1327
|
-
if (Object.keys(properties).length > 0) {
|
|
1328
|
-
schema.properties = properties;
|
|
1329
|
-
}
|
|
1330
|
-
if (required.length > 0) {
|
|
1331
|
-
schema.required = required;
|
|
1332
|
-
}
|
|
1333
|
-
if (indexInfo) {
|
|
1334
|
-
schema.additionalProperties = walkType(indexInfo.type, ctx);
|
|
1335
|
-
} else if (Object.keys(properties).length > 0) {
|
|
1336
|
-
schema.additionalProperties = false;
|
|
1337
|
-
}
|
|
1338
|
-
return schema;
|
|
1268
|
+
return params;
|
|
1339
1269
|
}
|
|
1340
|
-
function
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (name && name !== "__type" && name !== "__object") {
|
|
1344
|
-
return name;
|
|
1270
|
+
function queryParameters(query) {
|
|
1271
|
+
if (!query || query.type !== "object" || typeof query.properties !== "object") {
|
|
1272
|
+
return [];
|
|
1345
1273
|
}
|
|
1346
|
-
|
|
1274
|
+
const properties = query.properties;
|
|
1275
|
+
const required = Array.isArray(query.required) ? query.required : [];
|
|
1276
|
+
return Object.entries(properties).map(([name, schema]) => ({
|
|
1277
|
+
name,
|
|
1278
|
+
in: "query",
|
|
1279
|
+
required: required.includes(name),
|
|
1280
|
+
schema: rewriteRefs(schema)
|
|
1281
|
+
}));
|
|
1347
1282
|
}
|
|
1348
|
-
function
|
|
1349
|
-
const
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1357
|
-
if (checker.isTupleType(type)) {
|
|
1358
|
-
const elements = checker.getTypeArguments(type);
|
|
1359
|
-
return { type: "array", items: elements.map((element) => walkType(element, ctx)) };
|
|
1360
|
-
}
|
|
1361
|
-
const id = typeId(type);
|
|
1362
|
-
const existing = ctx.inProgress.get(id);
|
|
1363
|
-
if (existing) {
|
|
1364
|
-
ctx.usedDefs.add(existing);
|
|
1365
|
-
return { $ref: `#/$defs/${existing}` };
|
|
1366
|
-
}
|
|
1367
|
-
const name = defName(type);
|
|
1368
|
-
ctx.inProgress.set(id, name);
|
|
1369
|
-
const schema = buildObjectSchema(type, ctx);
|
|
1370
|
-
ctx.inProgress.delete(id);
|
|
1371
|
-
if (ctx.usedDefs.has(name)) {
|
|
1372
|
-
ctx.defs[name] = schema;
|
|
1373
|
-
return { $ref: `#/$defs/${name}` };
|
|
1283
|
+
function readProjectInfo(cwd) {
|
|
1284
|
+
const file = (0, import_node_path7.join)(cwd, "package.json");
|
|
1285
|
+
if ((0, import_node_fs5.existsSync)(file)) {
|
|
1286
|
+
try {
|
|
1287
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
|
|
1288
|
+
return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1374
1291
|
}
|
|
1375
|
-
return
|
|
1292
|
+
return { title: "giri API", version: "0.0.0" };
|
|
1376
1293
|
}
|
|
1377
|
-
function
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1294
|
+
function buildOpenApiDocument(paths, routes, data = {}) {
|
|
1295
|
+
const documentPaths = {};
|
|
1296
|
+
const schemas = {};
|
|
1297
|
+
const securitySchemes = {};
|
|
1298
|
+
for (const route of routes) {
|
|
1299
|
+
if (data.hiddenFiles?.has(route.file)) {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const responses = data.responsesByFile?.get(route.file);
|
|
1303
|
+
const input = data.inputsByFile?.get(route.file);
|
|
1304
|
+
const security = data.securityByFile?.get(route.file);
|
|
1305
|
+
for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
|
|
1306
|
+
schemas[name] = rewriteRefs(schema);
|
|
1307
|
+
}
|
|
1308
|
+
const operation = { responses: buildResponses(responses?.responses ?? []) };
|
|
1309
|
+
const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
|
|
1310
|
+
if (parameters.length > 0) {
|
|
1311
|
+
operation.parameters = parameters;
|
|
1312
|
+
}
|
|
1313
|
+
if (input?.body) {
|
|
1314
|
+
const content = {};
|
|
1315
|
+
for (const [contentType, schema] of Object.entries(input.body)) {
|
|
1316
|
+
content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
|
|
1317
|
+
schema: rewriteRefs(schema)
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
if (Object.keys(content).length > 0) {
|
|
1321
|
+
operation.requestBody = { required: true, content };
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (security && security.security.length > 0) {
|
|
1325
|
+
operation.security = security.security;
|
|
1326
|
+
}
|
|
1327
|
+
if (security) {
|
|
1328
|
+
Object.assign(securitySchemes, security.securitySchemes);
|
|
1329
|
+
}
|
|
1330
|
+
const openApiPath = toOpenApiPath(route.path);
|
|
1331
|
+
const pathItem = documentPaths[openApiPath] ?? {};
|
|
1332
|
+
pathItem[route.method.toLowerCase()] = operation;
|
|
1333
|
+
documentPaths[openApiPath] = pathItem;
|
|
1384
1334
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1335
|
+
const document = {
|
|
1336
|
+
openapi: "3.1.0",
|
|
1337
|
+
info: readProjectInfo(paths.cwd),
|
|
1338
|
+
paths: documentPaths
|
|
1339
|
+
};
|
|
1340
|
+
const components = {};
|
|
1341
|
+
if (Object.keys(schemas).length > 0) {
|
|
1342
|
+
components.schemas = schemas;
|
|
1387
1343
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
return { type: "string" };
|
|
1344
|
+
if (Object.keys(securitySchemes).length > 0) {
|
|
1345
|
+
components.securitySchemes = securitySchemes;
|
|
1391
1346
|
}
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1347
|
+
if (Object.keys(components).length > 0) {
|
|
1348
|
+
document.components = components;
|
|
1394
1349
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1350
|
+
return document;
|
|
1351
|
+
}
|
|
1352
|
+
async function writeOpenApi(paths, routes, data = {}) {
|
|
1353
|
+
await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/generator/param-types.ts
|
|
1357
|
+
var import_node_path8 = require("path");
|
|
1358
|
+
function paramsType(params) {
|
|
1359
|
+
if (params.length === 0) {
|
|
1360
|
+
return "{}";
|
|
1397
1361
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1362
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (const param of params) {
|
|
1364
|
+
unique.set(param.name, param);
|
|
1400
1365
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1366
|
+
const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
|
|
1367
|
+
return `{
|
|
1368
|
+
${fields}
|
|
1369
|
+
}`;
|
|
1370
|
+
}
|
|
1371
|
+
function varsType(typesDir, sharedFiles) {
|
|
1372
|
+
if (sharedFiles.length === 0) {
|
|
1373
|
+
return "{}";
|
|
1403
1374
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1375
|
+
return sharedFiles.map((file) => {
|
|
1376
|
+
const spec = JSON.stringify(moduleSpecifier(typesDir, file));
|
|
1377
|
+
return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
|
|
1378
|
+
}).join("\n & ");
|
|
1379
|
+
}
|
|
1380
|
+
function methodExports(typesDir, verbs) {
|
|
1381
|
+
return verbs.map(({ method, file }) => {
|
|
1382
|
+
const spec = JSON.stringify(moduleSpecifier(typesDir, file));
|
|
1383
|
+
const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
|
|
1384
|
+
const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
|
|
1385
|
+
return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async function writeParamTypes(paths, folders) {
|
|
1389
|
+
for (const { dir, params, sharedFiles, verbs } of folders) {
|
|
1390
|
+
const file = typeFilePath(paths, dir);
|
|
1391
|
+
const typesDir = (0, import_node_path8.dirname)(file);
|
|
1392
|
+
const lines = [
|
|
1393
|
+
GENERATED_HEADER,
|
|
1394
|
+
`export type Params = ${paramsType(params)};`,
|
|
1395
|
+
"export type RouteParams = Params;",
|
|
1396
|
+
`type Vars = ${varsType(typesDir, sharedFiles)};`,
|
|
1397
|
+
"export type Middleware<Injects extends Record<string, unknown> = {}> =",
|
|
1398
|
+
' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
|
|
1399
|
+
'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
|
|
1400
|
+
' import("@boon4681/giri").Handle<Params, Input, Vars>;'
|
|
1401
|
+
];
|
|
1402
|
+
if (verbs.length > 0) {
|
|
1403
|
+
lines.push(...methodExports(typesDir, verbs));
|
|
1404
|
+
}
|
|
1405
|
+
lines.push("");
|
|
1406
|
+
await writeGenerated(file, lines.join("\n"));
|
|
1406
1407
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/generator/inputs.ts
|
|
1411
|
+
function sanitize(schema) {
|
|
1412
|
+
const { $schema, ...rest } = schema;
|
|
1413
|
+
void $schema;
|
|
1414
|
+
return rest;
|
|
1415
|
+
}
|
|
1416
|
+
function inputToJsonSchema(schema) {
|
|
1417
|
+
if (!isGiriInputSchema(schema)) {
|
|
1418
|
+
return void 0;
|
|
1409
1419
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1420
|
+
return sanitize(schema.toJsonSchema());
|
|
1421
|
+
}
|
|
1422
|
+
function bodyToJsonSchemas(value) {
|
|
1423
|
+
if (!isGiriBodySchema(value)) {
|
|
1424
|
+
return void 0;
|
|
1412
1425
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1426
|
+
const out = {};
|
|
1427
|
+
for (const [contentType, schema] of Object.entries(value.contents)) {
|
|
1428
|
+
const json = inputToJsonSchema(schema);
|
|
1429
|
+
if (json) {
|
|
1430
|
+
out[contentType] = json;
|
|
1431
|
+
}
|
|
1415
1432
|
}
|
|
1416
|
-
return
|
|
1433
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1417
1434
|
}
|
|
1418
1435
|
|
|
1419
|
-
// src/generator/
|
|
1420
|
-
function
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
1429
|
-
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))) {
|
|
1430
|
-
found = declaration.initializer;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1436
|
+
// src/generator/route-meta.ts
|
|
1437
|
+
function loadModule2(file) {
|
|
1438
|
+
const resolved = require.resolve(file);
|
|
1439
|
+
delete require.cache[resolved];
|
|
1440
|
+
return require(resolved);
|
|
1441
|
+
}
|
|
1442
|
+
function interopDefault2(value) {
|
|
1443
|
+
if (value && typeof value === "object" && "default" in value) {
|
|
1444
|
+
return value.default;
|
|
1434
1445
|
}
|
|
1435
|
-
return
|
|
1446
|
+
return value;
|
|
1436
1447
|
}
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1448
|
+
function middlewareFunctions(value) {
|
|
1449
|
+
const exported = interopDefault2(value);
|
|
1450
|
+
if (typeof exported === "function") {
|
|
1451
|
+
return [exported];
|
|
1440
1452
|
}
|
|
1441
|
-
if (
|
|
1442
|
-
return
|
|
1453
|
+
if (Array.isArray(exported)) {
|
|
1454
|
+
return exported.filter((fn) => typeof fn === "function");
|
|
1443
1455
|
}
|
|
1444
|
-
|
|
1445
|
-
const visit = (node) => {
|
|
1446
|
-
if (import_typescript3.default.isFunctionDeclaration(node) || import_typescript3.default.isFunctionExpression(node) || import_typescript3.default.isArrowFunction(node)) {
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
if (import_typescript3.default.isReturnStatement(node) && node.expression) {
|
|
1450
|
-
expressions.push(node.expression);
|
|
1451
|
-
}
|
|
1452
|
-
import_typescript3.default.forEachChild(node, visit);
|
|
1453
|
-
};
|
|
1454
|
-
import_typescript3.default.forEachChild(fn.body, visit);
|
|
1455
|
-
return expressions;
|
|
1456
|
-
}
|
|
1457
|
-
function propertyType(checker, type, name, location) {
|
|
1458
|
-
const symbol = checker.getPropertyOfType(type, name);
|
|
1459
|
-
return symbol ? checker.getTypeOfSymbolAtLocation(symbol, location) : void 0;
|
|
1460
|
-
}
|
|
1461
|
-
function isTypedResponse2(checker, type) {
|
|
1462
|
-
return Boolean(
|
|
1463
|
-
checker.getPropertyOfType(type, "data") && checker.getPropertyOfType(type, "status") && checker.getPropertyOfType(type, "format")
|
|
1464
|
-
);
|
|
1456
|
+
return [];
|
|
1465
1457
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1458
|
+
function readInput(routeModule) {
|
|
1459
|
+
const input = {};
|
|
1460
|
+
const body = bodyToJsonSchemas(routeModule.body);
|
|
1461
|
+
const query = inputToJsonSchema(routeModule.query);
|
|
1462
|
+
if (body) {
|
|
1463
|
+
input.body = body;
|
|
1469
1464
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return void 0;
|
|
1465
|
+
if (query) {
|
|
1466
|
+
input.query = query;
|
|
1473
1467
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1468
|
+
return input.body || input.query ? input : void 0;
|
|
1469
|
+
}
|
|
1470
|
+
function hiddenFrom(value) {
|
|
1471
|
+
if (value === false) {
|
|
1472
|
+
return true;
|
|
1476
1473
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
return void 0;
|
|
1474
|
+
if (value === true) {
|
|
1475
|
+
return false;
|
|
1480
1476
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const statusType = checker.getTypeAtLocation(statusArg);
|
|
1484
|
-
status = statusType.isNumberLiteral() ? statusType.value : "default";
|
|
1477
|
+
if (value && typeof value === "object" && "hidden" in value) {
|
|
1478
|
+
return Boolean(value.hidden);
|
|
1485
1479
|
}
|
|
1486
|
-
return
|
|
1480
|
+
return void 0;
|
|
1487
1481
|
}
|
|
1488
|
-
function
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1482
|
+
function collectHidden(route, routeModule, loadShared) {
|
|
1483
|
+
let hidden = false;
|
|
1484
|
+
for (const file of route.sharedFiles) {
|
|
1485
|
+
const opinion = hiddenFrom(loadShared(file).openapi);
|
|
1486
|
+
if (opinion !== void 0) {
|
|
1487
|
+
hidden = opinion;
|
|
1488
|
+
}
|
|
1494
1489
|
}
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
return { status, format, data: dataType };
|
|
1498
|
-
}
|
|
1499
|
-
function constituents(type) {
|
|
1500
|
-
return type.isUnion() ? type.types : [type];
|
|
1490
|
+
const verb = hiddenFrom(routeModule.openapi);
|
|
1491
|
+
return verb ?? hidden;
|
|
1501
1492
|
}
|
|
1502
|
-
function
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
return result;
|
|
1493
|
+
function collectSecurity(route, routeModule, loadShared) {
|
|
1494
|
+
const skipInherited = Boolean(
|
|
1495
|
+
routeModule.config?.skipInherited
|
|
1496
|
+
);
|
|
1497
|
+
const middleware = [];
|
|
1498
|
+
if (!skipInherited) {
|
|
1499
|
+
for (const file of route.sharedFiles) {
|
|
1500
|
+
middleware.push(...middlewareFunctions(loadShared(file).middleware));
|
|
1501
|
+
}
|
|
1512
1502
|
}
|
|
1513
|
-
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
record(fromCall);
|
|
1525
|
-
continue;
|
|
1503
|
+
middleware.push(...middlewareFunctions(routeModule.middleware));
|
|
1504
|
+
const security = [];
|
|
1505
|
+
const securitySchemes = {};
|
|
1506
|
+
for (const fn of middleware) {
|
|
1507
|
+
const openapi = fn.openapi;
|
|
1508
|
+
if (openapi?.security) {
|
|
1509
|
+
for (const requirement of openapi.security) {
|
|
1510
|
+
if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
|
|
1511
|
+
security.push(requirement);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1526
1514
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1515
|
+
if (openapi?.securitySchemes) {
|
|
1516
|
+
Object.assign(securitySchemes, openapi.securitySchemes);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
|
|
1520
|
+
}
|
|
1521
|
+
async function extractRouteMeta(config, paths, routes) {
|
|
1522
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1523
|
+
const { unregister } = await safeRegister();
|
|
1524
|
+
const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
|
|
1525
|
+
const sharedCache = /* @__PURE__ */ new Map();
|
|
1526
|
+
const loadShared = (file) => {
|
|
1527
|
+
if (!sharedCache.has(file)) {
|
|
1528
|
+
try {
|
|
1529
|
+
sharedCache.set(file, loadModule2(file));
|
|
1530
|
+
} catch {
|
|
1531
|
+
sharedCache.set(file, {});
|
|
1533
1532
|
}
|
|
1534
1533
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1534
|
+
return sharedCache.get(file);
|
|
1535
|
+
};
|
|
1536
|
+
try {
|
|
1537
|
+
for (const route of routes) {
|
|
1538
|
+
try {
|
|
1539
|
+
const routeModule = loadModule2(route.file);
|
|
1540
|
+
const meta = {};
|
|
1541
|
+
const input = readInput(routeModule);
|
|
1542
|
+
const security = collectSecurity(route, routeModule, loadShared);
|
|
1543
|
+
const hidden = collectHidden(route, routeModule, loadShared);
|
|
1544
|
+
if (input) {
|
|
1545
|
+
meta.input = input;
|
|
1546
|
+
}
|
|
1547
|
+
if (security) {
|
|
1548
|
+
meta.security = security;
|
|
1549
|
+
}
|
|
1550
|
+
if (hidden) {
|
|
1551
|
+
meta.hidden = true;
|
|
1552
|
+
}
|
|
1553
|
+
if (meta.input || meta.security || meta.hidden) {
|
|
1554
|
+
byFile.set(route.file, meta);
|
|
1555
|
+
}
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1537
1558
|
}
|
|
1559
|
+
} finally {
|
|
1560
|
+
unregisterAlias();
|
|
1561
|
+
unregister();
|
|
1538
1562
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1563
|
+
return byFile;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/generator/route-types.ts
|
|
1567
|
+
var import_node_path9 = require("path");
|
|
1568
|
+
async function writeRouteTypes(paths, routes) {
|
|
1569
|
+
const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
|
|
1570
|
+
const lines = [
|
|
1571
|
+
GENERATED_HEADER,
|
|
1572
|
+
"export interface RouteParams {"
|
|
1573
|
+
];
|
|
1574
|
+
for (const route of routes) {
|
|
1575
|
+
const typeFile = typeFilePath(paths, route.routeDir);
|
|
1576
|
+
lines.push(
|
|
1577
|
+
` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
|
|
1578
|
+
importPath(file, typeFile)
|
|
1579
|
+
)}).Params;`
|
|
1580
|
+
);
|
|
1542
1581
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
result.$defs = ctx.defs;
|
|
1546
|
-
return result;
|
|
1582
|
+
lines.push("}", "");
|
|
1583
|
+
await writeGenerated(file, lines.join("\n"));
|
|
1547
1584
|
}
|
|
1548
1585
|
|
|
1549
1586
|
// src/generator/tsconfig.ts
|
|
@@ -1602,20 +1639,21 @@ async function typeFolders(paths, routes) {
|
|
|
1602
1639
|
verbs: verbsByDir.get(slash(dir)) ?? []
|
|
1603
1640
|
}));
|
|
1604
1641
|
}
|
|
1605
|
-
function extractResponses(paths, routes) {
|
|
1642
|
+
async function extractResponses(paths, routes) {
|
|
1606
1643
|
const byFile = /* @__PURE__ */ new Map();
|
|
1607
1644
|
if (routes.length === 0) {
|
|
1608
1645
|
return byFile;
|
|
1609
1646
|
}
|
|
1610
1647
|
try {
|
|
1648
|
+
const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
|
|
1611
1649
|
const files = [...new Set(routes.map((route) => route.file))];
|
|
1612
1650
|
const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
|
|
1613
|
-
const program =
|
|
1651
|
+
const program = createSchemaProgram2(
|
|
1614
1652
|
paths,
|
|
1615
1653
|
(0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
|
|
1616
1654
|
);
|
|
1617
1655
|
for (const file of files) {
|
|
1618
|
-
byFile.set(file,
|
|
1656
|
+
byFile.set(file, extractRouteResponses2(program, file));
|
|
1619
1657
|
}
|
|
1620
1658
|
} catch (error) {
|
|
1621
1659
|
console.warn(`giri: skipped response schema generation (${error.message}).`);
|
|
@@ -1657,7 +1695,7 @@ async function syncProject(config, options = {}) {
|
|
|
1657
1695
|
await writeRouteTypes(paths, routes);
|
|
1658
1696
|
await writeAppTypes(paths);
|
|
1659
1697
|
await writeTsConfig(paths, config);
|
|
1660
|
-
const responsesByFile = extractResponses(paths, routes);
|
|
1698
|
+
const responsesByFile = await extractResponses(paths, routes);
|
|
1661
1699
|
const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
|
|
1662
1700
|
const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
|
|
1663
1701
|
await writeManifest(paths, routes, data);
|