@boon4681/giri 0.0.1 → 0.0.2-alpha-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -17
- package/dist/adapters/hono.d.ts +26 -3
- package/dist/adapters/hono.js +99 -7
- package/dist/adapters/hono.js.map +1 -1
- package/dist/cli.js +1197 -1119
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1368 -1285
- package/dist/index.js.map +1 -1
- package/dist/{types-BrUMxh5u.d.ts → types-C4J1_iLZ.d.ts} +24 -3
- 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,1588 @@ 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
|
+
var pendingResponseBrand = /* @__PURE__ */ Symbol("giri.pending-response");
|
|
429
|
+
function getPending(context) {
|
|
430
|
+
return context[pendingResponseBrand];
|
|
431
|
+
}
|
|
432
|
+
function createTypedResponse(data, status, format, headers) {
|
|
433
|
+
return {
|
|
434
|
+
[typedResponseBrand]: { data, status, format },
|
|
435
|
+
data,
|
|
436
|
+
status,
|
|
437
|
+
format,
|
|
438
|
+
headers
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function isTypedResponse(value) {
|
|
442
|
+
return Boolean(value && typeof value === "object" && typedResponseBrand in value);
|
|
443
|
+
}
|
|
444
|
+
function createContext(options) {
|
|
445
|
+
const url = new URL(options.request.url);
|
|
446
|
+
const store = /* @__PURE__ */ new Map();
|
|
447
|
+
const validated = options.validated ?? {};
|
|
448
|
+
const pending = { headers: new Headers() };
|
|
449
|
+
const defaultStatus = () => pending.status ?? 200;
|
|
450
|
+
const context = {
|
|
451
|
+
params: options.params ?? {},
|
|
452
|
+
app: options.app ?? {},
|
|
453
|
+
req: {
|
|
454
|
+
raw: options.request,
|
|
455
|
+
url,
|
|
456
|
+
method: options.request.method,
|
|
457
|
+
header: (name) => options.request.headers.get(name),
|
|
458
|
+
json: () => options.request.json(),
|
|
459
|
+
text: () => options.request.text(),
|
|
460
|
+
arrayBuffer: () => options.request.arrayBuffer(),
|
|
461
|
+
formData: () => options.request.formData(),
|
|
462
|
+
valid: (key) => {
|
|
463
|
+
if (!(key in validated)) {
|
|
464
|
+
throw new Error(`No validated ${String(key)} data is available for this route.`);
|
|
465
|
+
}
|
|
466
|
+
return validated[key];
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
set: (key, value) => {
|
|
470
|
+
store.set(key, value);
|
|
471
|
+
},
|
|
472
|
+
get: (key) => store.get(key),
|
|
473
|
+
json: (data, status, headers) => createTypedResponse(data, status ?? defaultStatus(), "json", headers),
|
|
474
|
+
text: (text, status, headers) => createTypedResponse(text, status ?? defaultStatus(), "text", headers),
|
|
475
|
+
html: (html, status, headers) => createTypedResponse(html, status ?? defaultStatus(), "html", headers),
|
|
476
|
+
body: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
|
|
477
|
+
newResponse: (data, status, headers) => new Response(data, { status: status ?? defaultStatus(), headers }),
|
|
478
|
+
redirect: (location, status) => new Response(null, { status: status ?? 302, headers: { Location: location } }),
|
|
479
|
+
notFound: () => new Response("404 Not Found", { status: 404 }),
|
|
480
|
+
header: (name, value, options2) => {
|
|
481
|
+
if (value === void 0) {
|
|
482
|
+
pending.headers.delete(name);
|
|
483
|
+
} else if (options2?.append) {
|
|
484
|
+
pending.headers.append(name, value);
|
|
485
|
+
} else {
|
|
486
|
+
pending.headers.set(name, value);
|
|
547
487
|
}
|
|
488
|
+
},
|
|
489
|
+
status: (code) => {
|
|
490
|
+
pending.status = code;
|
|
548
491
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
function assertBodySchema(value, file) {
|
|
554
|
-
if (!isGiriBodySchema(value)) {
|
|
555
|
-
throw new Error(
|
|
556
|
-
`${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
492
|
+
};
|
|
493
|
+
context[nativeContextBrand] = options.native;
|
|
494
|
+
context[pendingResponseBrand] = pending;
|
|
495
|
+
return context;
|
|
559
496
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
497
|
+
function typedResponseToResponse(response) {
|
|
498
|
+
const headers = new Headers(response.headers);
|
|
499
|
+
if (response.format === "json" && !headers.has("content-type")) {
|
|
500
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
565
501
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const input = {};
|
|
569
|
-
if (routeModule.body !== void 0) {
|
|
570
|
-
assertBodySchema(routeModule.body, file);
|
|
571
|
-
input.body = routeModule.body;
|
|
502
|
+
if (response.format === "text" && !headers.has("content-type")) {
|
|
503
|
+
headers.set("content-type", "text/plain; charset=utf-8");
|
|
572
504
|
}
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
input.query = routeModule.query;
|
|
505
|
+
if (response.format === "html" && !headers.has("content-type")) {
|
|
506
|
+
headers.set("content-type", "text/html; charset=utf-8");
|
|
576
507
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
function resolveAliasTarget(cwd, target, capture = "") {
|
|
583
|
-
const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
|
|
584
|
-
return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
|
|
508
|
+
const body = BODYLESS_STATUS.has(response.status) ? null : response.format === "json" ? JSON.stringify(response.data) : String(response.data);
|
|
509
|
+
return new Response(body, {
|
|
510
|
+
status: response.status,
|
|
511
|
+
headers
|
|
512
|
+
});
|
|
585
513
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
return void 0;
|
|
593
|
-
}
|
|
594
|
-
if (request === key) {
|
|
595
|
-
return "";
|
|
514
|
+
function toResponse(response, context) {
|
|
515
|
+
const base = isTypedResponse(response) ? typedResponseToResponse(response) : response;
|
|
516
|
+
const pending = context ? getPending(context) : void 0;
|
|
517
|
+
if (!pending) {
|
|
518
|
+
return base;
|
|
596
519
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
520
|
+
let hasPending = false;
|
|
521
|
+
pending.headers.forEach(() => {
|
|
522
|
+
hasPending = true;
|
|
523
|
+
});
|
|
524
|
+
if (!hasPending) {
|
|
525
|
+
return base;
|
|
600
526
|
}
|
|
601
|
-
|
|
527
|
+
const headers = new Headers(base.headers);
|
|
528
|
+
pending.headers.forEach((value, key) => {
|
|
529
|
+
if (!headers.has(key)) {
|
|
530
|
+
headers.set(key, value);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return new Response(base.body, { status: base.status, statusText: base.statusText, headers });
|
|
602
534
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
535
|
+
async function composeMiddleware(middleware, handle, context) {
|
|
536
|
+
let index = -1;
|
|
537
|
+
let result;
|
|
538
|
+
const dispatch = async (i) => {
|
|
539
|
+
if (i <= index) {
|
|
540
|
+
throw new Error("next() called multiple times in giri middleware.");
|
|
608
541
|
}
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
|
|
542
|
+
index = i;
|
|
543
|
+
if (i === middleware.length) {
|
|
544
|
+
result = await handle(context);
|
|
545
|
+
return result;
|
|
612
546
|
}
|
|
613
|
-
|
|
547
|
+
const returned = await middleware[i](context, () => dispatch(i + 1));
|
|
548
|
+
if (returned !== void 0) {
|
|
549
|
+
result = returned;
|
|
550
|
+
return returned;
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
};
|
|
554
|
+
await dispatch(0);
|
|
555
|
+
if (result === void 0) {
|
|
556
|
+
throw new Error("Route completed without returning a response.");
|
|
614
557
|
}
|
|
615
|
-
return
|
|
558
|
+
return result;
|
|
616
559
|
}
|
|
617
|
-
function
|
|
618
|
-
if (
|
|
619
|
-
return
|
|
620
|
-
};
|
|
560
|
+
function defineMiddleware(optionsOrMiddleware, maybeMiddleware) {
|
|
561
|
+
if (typeof optionsOrMiddleware === "function") {
|
|
562
|
+
return optionsOrMiddleware;
|
|
621
563
|
}
|
|
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;
|
|
564
|
+
if (!maybeMiddleware) {
|
|
565
|
+
throw new Error("defineMiddleware(options, middleware) requires a middleware function.");
|
|
644
566
|
}
|
|
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
|
-
};
|
|
567
|
+
maybeMiddleware.openapi = optionsOrMiddleware.openapi;
|
|
568
|
+
return maybeMiddleware;
|
|
659
569
|
}
|
|
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 };
|
|
570
|
+
function stack(...middleware) {
|
|
571
|
+
return middleware;
|
|
691
572
|
}
|
|
692
573
|
|
|
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;
|
|
574
|
+
// src/validation.ts
|
|
575
|
+
function defineInputSchema(schema) {
|
|
576
|
+
return { [inputSchemaBrand]: true, ...schema };
|
|
723
577
|
}
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
return path;
|
|
578
|
+
function isGiriInputSchema(value) {
|
|
579
|
+
return Boolean(
|
|
580
|
+
value && typeof value === "object" && value[inputSchemaBrand] === true
|
|
581
|
+
);
|
|
730
582
|
}
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
|
|
583
|
+
function defineBodySchema(contents) {
|
|
584
|
+
return { [bodySchemaBrand]: true, contents };
|
|
734
585
|
}
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
586
|
+
function isGiriBodySchema(value) {
|
|
587
|
+
return Boolean(
|
|
588
|
+
value && typeof value === "object" && value[bodySchemaBrand] === true
|
|
589
|
+
);
|
|
740
590
|
}
|
|
741
|
-
var
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
591
|
+
var MIME_TO_CONTENT_TYPE = {
|
|
592
|
+
"application/json": "json",
|
|
593
|
+
"multipart/form-data": "form",
|
|
594
|
+
"application/x-www-form-urlencoded": "urlencoded",
|
|
595
|
+
"text/plain": "text"
|
|
596
|
+
};
|
|
597
|
+
function contentTypeFromHeader(header) {
|
|
598
|
+
if (!header) {
|
|
599
|
+
return void 0;
|
|
745
600
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
writeCache.set(path, content);
|
|
601
|
+
const mime = header.split(";", 1)[0].trim().toLowerCase();
|
|
602
|
+
return MIME_TO_CONTENT_TYPE[mime];
|
|
749
603
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
604
|
+
function formDataObject(form) {
|
|
605
|
+
const result = {};
|
|
606
|
+
form.forEach((value, key) => {
|
|
607
|
+
const current = result[key];
|
|
608
|
+
if (current === void 0) {
|
|
609
|
+
result[key] = value;
|
|
610
|
+
} else if (Array.isArray(current)) {
|
|
611
|
+
current.push(value);
|
|
612
|
+
} else {
|
|
613
|
+
result[key] = [current, value];
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
return result;
|
|
753
617
|
}
|
|
754
|
-
async function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
} catch {
|
|
759
|
-
return;
|
|
618
|
+
async function readRawBody(request, contentType) {
|
|
619
|
+
const cloned = request.clone();
|
|
620
|
+
if (contentType === "json") {
|
|
621
|
+
return cloned.json();
|
|
760
622
|
}
|
|
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
|
-
}
|
|
623
|
+
if (contentType === "text") {
|
|
624
|
+
return cloned.text();
|
|
771
625
|
}
|
|
626
|
+
return formDataObject(await cloned.formData());
|
|
772
627
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
628
|
+
function queryObject(url) {
|
|
629
|
+
const result = {};
|
|
630
|
+
for (const [key, value] of url.searchParams) {
|
|
631
|
+
const current = result[key];
|
|
632
|
+
if (current === void 0) {
|
|
633
|
+
result[key] = value;
|
|
634
|
+
} else if (Array.isArray(current)) {
|
|
635
|
+
current.push(value);
|
|
636
|
+
} else {
|
|
637
|
+
result[key] = [current, value];
|
|
781
638
|
}
|
|
782
639
|
}
|
|
783
|
-
return
|
|
640
|
+
return result;
|
|
784
641
|
}
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
642
|
+
async function runValidation(schema, value, label) {
|
|
643
|
+
if (!isGiriInputSchema(schema)) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`giri: ${label} schema must be wrapped with a validator, e.g. \`export const ${label} = zod(...)\` from @boon4681/giri/validators/zod.`
|
|
646
|
+
);
|
|
789
647
|
}
|
|
790
|
-
return
|
|
648
|
+
return schema.validate(value);
|
|
791
649
|
}
|
|
792
|
-
async function
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
await
|
|
797
|
-
|
|
650
|
+
async function prepareRequestInput(request, input) {
|
|
651
|
+
const validated = {};
|
|
652
|
+
if (input?.query) {
|
|
653
|
+
const query = queryObject(new URL(request.url));
|
|
654
|
+
const result = await runValidation(input.query, query, "query");
|
|
655
|
+
if (!result.ok) {
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
response: createTypedResponse(
|
|
659
|
+
{ message: "Invalid query parameters.", issues: result.issues },
|
|
660
|
+
400,
|
|
661
|
+
"json"
|
|
662
|
+
)
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
validated.query = result.value;
|
|
798
666
|
}
|
|
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);
|
|
667
|
+
if (input?.body) {
|
|
668
|
+
const contents = input.body.contents;
|
|
669
|
+
const declared = Object.keys(contents);
|
|
670
|
+
const requested = contentTypeFromHeader(request.headers.get("content-type"));
|
|
671
|
+
const chosen = requested && contents[requested] ? requested : contents.json ? "json" : void 0;
|
|
672
|
+
if (!chosen) {
|
|
830
673
|
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 } : {}
|
|
674
|
+
ok: false,
|
|
675
|
+
response: createTypedResponse(
|
|
676
|
+
{ message: "Unsupported media type.", issues: { accepted: declared } },
|
|
677
|
+
415,
|
|
678
|
+
"json"
|
|
679
|
+
)
|
|
842
680
|
};
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
681
|
+
}
|
|
682
|
+
let rawBody;
|
|
683
|
+
try {
|
|
684
|
+
rawBody = await readRawBody(request, chosen);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
response: createTypedResponse(
|
|
689
|
+
{ message: "Invalid request body.", issues: error },
|
|
690
|
+
400,
|
|
691
|
+
"json"
|
|
692
|
+
)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const result = await runValidation(contents[chosen], rawBody, "body");
|
|
696
|
+
if (!result.ok) {
|
|
697
|
+
return {
|
|
698
|
+
ok: false,
|
|
699
|
+
response: createTypedResponse(
|
|
700
|
+
{ message: "Invalid request body.", issues: result.issues },
|
|
701
|
+
400,
|
|
702
|
+
"json"
|
|
703
|
+
)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;
|
|
707
|
+
}
|
|
708
|
+
return { ok: true, validated };
|
|
846
709
|
}
|
|
847
710
|
|
|
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
|
-
|
|
711
|
+
// src/app.ts
|
|
712
|
+
var import_node_module = __toESM(require("module"));
|
|
713
|
+
var import_node_path3 = require("path");
|
|
714
|
+
|
|
715
|
+
// src/loader/loader.ts
|
|
716
|
+
var import_prompts = require("@clack/prompts");
|
|
717
|
+
var import_node_fs = require("fs");
|
|
718
|
+
var import_node_path = require("path");
|
|
719
|
+
var import_node_process = require("process");
|
|
720
|
+
|
|
721
|
+
// src/config/schema.ts
|
|
722
|
+
var import_typebox = require("@sinclair/typebox");
|
|
723
|
+
var configSchema = import_typebox.Type.Object({
|
|
724
|
+
adapter: import_typebox.Type.Any(),
|
|
725
|
+
alias: import_typebox.Type.Optional(import_typebox.Type.Record(
|
|
726
|
+
import_typebox.Type.String(),
|
|
727
|
+
import_typebox.Type.Union([import_typebox.Type.String(), import_typebox.Type.Array(import_typebox.Type.String())])
|
|
728
|
+
)),
|
|
729
|
+
outDir: import_typebox.Type.Optional(import_typebox.Type.String()),
|
|
730
|
+
server: import_typebox.Type.Optional(import_typebox.Type.Object({
|
|
731
|
+
port: import_typebox.Type.Optional(import_typebox.Type.Number()),
|
|
732
|
+
hostname: import_typebox.Type.Optional(import_typebox.Type.String())
|
|
733
|
+
}, { additionalProperties: false })),
|
|
734
|
+
errorSchema: import_typebox.Type.Optional(import_typebox.Type.Any())
|
|
735
|
+
}, { additionalProperties: false });
|
|
736
|
+
|
|
737
|
+
// src/loader/loader.ts
|
|
738
|
+
var import_value = require("@sinclair/typebox/value");
|
|
739
|
+
var assertES5 = async (unregister) => {
|
|
740
|
+
try {
|
|
741
|
+
init_es5();
|
|
742
|
+
} catch (e) {
|
|
743
|
+
if ("errors" in e && Array.isArray(e.errors) && e.errors.length > 0) {
|
|
744
|
+
const es5Error = e.errors.filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
|
|
745
|
+
if (es5Error) {
|
|
746
|
+
import_prompts.log.error(
|
|
747
|
+
`Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`
|
|
748
|
+
);
|
|
749
|
+
(0, import_node_process.exit)(1);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
import_prompts.log.error(e);
|
|
753
|
+
(0, import_node_process.exit)(1);
|
|
870
754
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
755
|
+
};
|
|
756
|
+
var safeRegister = async () => {
|
|
757
|
+
const { register } = await import("esbuild-register/dist/node");
|
|
758
|
+
let res;
|
|
759
|
+
try {
|
|
760
|
+
res = register({
|
|
761
|
+
format: "cjs",
|
|
762
|
+
loader: "ts"
|
|
763
|
+
});
|
|
764
|
+
} catch {
|
|
765
|
+
res = {
|
|
766
|
+
unregister: () => {
|
|
878
767
|
}
|
|
879
|
-
}
|
|
880
|
-
return out;
|
|
768
|
+
};
|
|
881
769
|
}
|
|
882
|
-
|
|
770
|
+
await assertES5(res.unregister);
|
|
771
|
+
return res;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/routes.ts
|
|
775
|
+
var import_node_fs2 = require("fs");
|
|
776
|
+
var import_promises = require("fs/promises");
|
|
777
|
+
var import_node_path2 = require("path");
|
|
778
|
+
var import_tinyglobby = require("tinyglobby");
|
|
779
|
+
var METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
780
|
+
var METHOD_FROM_FILE = new Map(
|
|
781
|
+
METHOD_ORDER.map((method) => [`+${method.toLowerCase()}`, method])
|
|
782
|
+
);
|
|
783
|
+
function normalizeSlashes(path) {
|
|
784
|
+
return path.split(import_node_path2.sep).join("/");
|
|
883
785
|
}
|
|
884
|
-
function
|
|
885
|
-
return
|
|
786
|
+
function isRouteSourceFile(fileName) {
|
|
787
|
+
return /\.(?:[cm]?[jt]s|[jt]sx)$/.test(fileName) && !fileName.endsWith(".d.ts");
|
|
886
788
|
}
|
|
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
|
-
};
|
|
789
|
+
function methodFromFile(fileName) {
|
|
790
|
+
if (!isRouteSourceFile(fileName)) {
|
|
791
|
+
return void 0;
|
|
905
792
|
}
|
|
906
|
-
|
|
793
|
+
const stem = fileName.replace(/\.(?:[cm]?[jt]s|[jt]sx)$/, "").toLowerCase();
|
|
794
|
+
return METHOD_FROM_FILE.get(stem);
|
|
907
795
|
}
|
|
908
|
-
function
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
continue;
|
|
796
|
+
function sharedFileIn(dir) {
|
|
797
|
+
for (const ext of ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]) {
|
|
798
|
+
const file = (0, import_node_path2.join)(dir, `+shared.${ext}`);
|
|
799
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
800
|
+
return file;
|
|
914
801
|
}
|
|
915
|
-
seen.add(param.name);
|
|
916
|
-
params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
|
|
917
802
|
}
|
|
918
|
-
return
|
|
803
|
+
return void 0;
|
|
919
804
|
}
|
|
920
|
-
function
|
|
921
|
-
|
|
805
|
+
function physicalRouteSegments(routesDir, routeDir) {
|
|
806
|
+
const rel = (0, import_node_path2.relative)(routesDir, routeDir);
|
|
807
|
+
if (!rel) {
|
|
922
808
|
return [];
|
|
923
809
|
}
|
|
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
|
-
}));
|
|
810
|
+
return normalizeSlashes(rel).split("/").filter(Boolean);
|
|
932
811
|
}
|
|
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
|
-
}
|
|
812
|
+
function urlSegment(segment) {
|
|
813
|
+
if (/^\(.+\)$/.test(segment)) {
|
|
814
|
+
return {};
|
|
941
815
|
}
|
|
942
|
-
|
|
816
|
+
const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
|
|
817
|
+
if (catchAll) {
|
|
818
|
+
const name = catchAll[1];
|
|
819
|
+
return {
|
|
820
|
+
value: `:${name}{.*}`,
|
|
821
|
+
param: { name, catchAll: true }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const param = /^\[(.+)\]$/.exec(segment);
|
|
825
|
+
if (param) {
|
|
826
|
+
const name = param[1];
|
|
827
|
+
return {
|
|
828
|
+
value: `:${name}`,
|
|
829
|
+
param: { name, catchAll: false }
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
return { value: segment };
|
|
943
833
|
}
|
|
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;
|
|
834
|
+
function pathFromSegments(segments) {
|
|
835
|
+
const pathSegments = [];
|
|
836
|
+
const params = [];
|
|
837
|
+
for (const segment of segments) {
|
|
838
|
+
const converted = urlSegment(segment);
|
|
839
|
+
if (converted.value) {
|
|
840
|
+
pathSegments.push(converted.value);
|
|
976
841
|
}
|
|
977
|
-
if (
|
|
978
|
-
|
|
842
|
+
if (converted.param) {
|
|
843
|
+
params.push(converted.param);
|
|
979
844
|
}
|
|
980
|
-
const openApiPath = toOpenApiPath(route.path);
|
|
981
|
-
const pathItem = documentPaths[openApiPath] ?? {};
|
|
982
|
-
pathItem[route.method.toLowerCase()] = operation;
|
|
983
|
-
documentPaths[openApiPath] = pathItem;
|
|
984
845
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
paths: documentPaths
|
|
846
|
+
return {
|
|
847
|
+
path: pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "/",
|
|
848
|
+
params
|
|
989
849
|
};
|
|
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
850
|
}
|
|
1021
|
-
function
|
|
1022
|
-
if (
|
|
1023
|
-
return
|
|
851
|
+
async function scanRouteFolders(routesDir) {
|
|
852
|
+
if (!(0, import_node_fs2.existsSync)(routesDir)) {
|
|
853
|
+
return [];
|
|
1024
854
|
}
|
|
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));
|
|
855
|
+
const folders = [routesDir];
|
|
856
|
+
const walk = async (dir) => {
|
|
857
|
+
for (const entry of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
|
|
858
|
+
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
859
|
+
const full = (0, import_node_path2.join)(dir, entry.name);
|
|
860
|
+
folders.push(full);
|
|
861
|
+
await walk(full);
|
|
862
|
+
}
|
|
1054
863
|
}
|
|
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;
|
|
864
|
+
};
|
|
865
|
+
await walk(routesDir);
|
|
866
|
+
return folders;
|
|
1065
867
|
}
|
|
1066
|
-
function
|
|
1067
|
-
|
|
1068
|
-
|
|
868
|
+
function routeParamsForDir(routesDir, dir) {
|
|
869
|
+
return pathFromSegments(physicalRouteSegments(routesDir, dir)).params;
|
|
870
|
+
}
|
|
871
|
+
function sharedFilesForDir(routesDir, dir) {
|
|
872
|
+
const segments = physicalRouteSegments(routesDir, dir);
|
|
873
|
+
const dirs = [routesDir];
|
|
874
|
+
let current = routesDir;
|
|
875
|
+
for (const segment of segments) {
|
|
876
|
+
current = (0, import_node_path2.join)(current, segment);
|
|
877
|
+
dirs.push(current);
|
|
1069
878
|
}
|
|
1070
|
-
return
|
|
879
|
+
return dirs.map(sharedFileIn).filter((file) => Boolean(file));
|
|
1071
880
|
}
|
|
1072
|
-
function
|
|
1073
|
-
if (!
|
|
1074
|
-
return
|
|
881
|
+
async function scanRoutes(routesDir) {
|
|
882
|
+
if (!(0, import_node_fs2.existsSync)(routesDir)) {
|
|
883
|
+
return [];
|
|
1075
884
|
}
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
885
|
+
const files = await (0, import_tinyglobby.glob)("**/+*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}", {
|
|
886
|
+
cwd: routesDir,
|
|
887
|
+
absolute: true,
|
|
888
|
+
onlyFiles: true
|
|
889
|
+
});
|
|
890
|
+
const routes = [];
|
|
891
|
+
for (const file of files) {
|
|
892
|
+
const method = methodFromFile((0, import_node_path2.basename)(file));
|
|
893
|
+
if (!method) {
|
|
894
|
+
continue;
|
|
1081
895
|
}
|
|
896
|
+
const routeDir = (0, import_node_path2.dirname)(file);
|
|
897
|
+
const routeSegments = physicalRouteSegments(routesDir, routeDir);
|
|
898
|
+
const { path, params } = pathFromSegments(routeSegments);
|
|
899
|
+
routes.push({
|
|
900
|
+
method,
|
|
901
|
+
path,
|
|
902
|
+
file,
|
|
903
|
+
routeDir,
|
|
904
|
+
routeSegments,
|
|
905
|
+
params,
|
|
906
|
+
sharedFiles: sharedFilesForDir(routesDir, routeDir)
|
|
907
|
+
});
|
|
1082
908
|
}
|
|
1083
|
-
return
|
|
909
|
+
return routes.sort((left, right) => {
|
|
910
|
+
const pathOrder = left.path.localeCompare(right.path);
|
|
911
|
+
if (pathOrder !== 0) {
|
|
912
|
+
return pathOrder;
|
|
913
|
+
}
|
|
914
|
+
return METHOD_ORDER.indexOf(left.method) - METHOD_ORDER.indexOf(right.method);
|
|
915
|
+
});
|
|
1084
916
|
}
|
|
1085
917
|
|
|
1086
|
-
// src/
|
|
1087
|
-
function
|
|
918
|
+
// src/app.ts
|
|
919
|
+
function loadModule(file) {
|
|
1088
920
|
const resolved = require.resolve(file);
|
|
1089
921
|
delete require.cache[resolved];
|
|
1090
922
|
return require(resolved);
|
|
1091
923
|
}
|
|
1092
|
-
function
|
|
924
|
+
function interopDefault(value) {
|
|
1093
925
|
if (value && typeof value === "object" && "default" in value) {
|
|
1094
926
|
return value.default;
|
|
1095
927
|
}
|
|
1096
928
|
return value;
|
|
1097
929
|
}
|
|
1098
|
-
function
|
|
1099
|
-
const exported =
|
|
930
|
+
function normalizeMiddleware(value, file) {
|
|
931
|
+
const exported = interopDefault(value);
|
|
932
|
+
if (exported === void 0) {
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
1100
935
|
if (typeof exported === "function") {
|
|
1101
936
|
return [exported];
|
|
1102
937
|
}
|
|
1103
938
|
if (Array.isArray(exported)) {
|
|
1104
|
-
|
|
939
|
+
for (const middleware of exported) {
|
|
940
|
+
if (typeof middleware !== "function") {
|
|
941
|
+
throw new Error(`Middleware export in ${file} must contain only functions.`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return exported;
|
|
1105
945
|
}
|
|
1106
|
-
|
|
946
|
+
throw new Error(`Middleware export in ${file} must be a function or an array of functions.`);
|
|
1107
947
|
}
|
|
1108
|
-
function
|
|
948
|
+
function assertBodySchema(value, file) {
|
|
949
|
+
if (!isGiriBodySchema(value)) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`${file}: "body" must be wrapped with a validator, e.g. \`export const body = zod.body({ json: ... })\` from @boon4681/giri/validators/zod.`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function assertQuerySchema(value, file) {
|
|
956
|
+
if (!isGiriInputSchema(value)) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`${file}: "query" must be wrapped with a validator, e.g. \`export const query = zod.query(...)\` from @boon4681/giri/validators/zod.`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function routeInput(routeModule, file) {
|
|
1109
963
|
const input = {};
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
input.body = body;
|
|
964
|
+
if (routeModule.body !== void 0) {
|
|
965
|
+
assertBodySchema(routeModule.body, file);
|
|
966
|
+
input.body = routeModule.body;
|
|
1114
967
|
}
|
|
1115
|
-
if (query) {
|
|
1116
|
-
|
|
968
|
+
if (routeModule.query !== void 0) {
|
|
969
|
+
assertQuerySchema(routeModule.query, file);
|
|
970
|
+
input.query = routeModule.query;
|
|
1117
971
|
}
|
|
1118
972
|
return input.body || input.query ? input : void 0;
|
|
1119
973
|
}
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
|
|
974
|
+
function aliasValues(value) {
|
|
975
|
+
return Array.isArray(value) ? value : [value];
|
|
976
|
+
}
|
|
977
|
+
function resolveAliasTarget(cwd, target, capture = "") {
|
|
978
|
+
const replaced = target.includes("*") ? target.replaceAll("*", capture) : target;
|
|
979
|
+
return (0, import_node_path3.isAbsolute)(replaced) ? replaced : (0, import_node_path3.resolve)(cwd, replaced);
|
|
980
|
+
}
|
|
981
|
+
function matchAlias(request, key) {
|
|
982
|
+
if (key.includes("*")) {
|
|
983
|
+
const [prefix2, suffix = ""] = key.split("*");
|
|
984
|
+
if (request.startsWith(prefix2) && request.endsWith(suffix)) {
|
|
985
|
+
return request.slice(prefix2.length, request.length - suffix.length);
|
|
986
|
+
}
|
|
987
|
+
return void 0;
|
|
1123
988
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
return
|
|
989
|
+
if (request === key) {
|
|
990
|
+
return "";
|
|
1126
991
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
992
|
+
const prefix = `${key}/`;
|
|
993
|
+
if (request.startsWith(prefix)) {
|
|
994
|
+
return request.slice(prefix.length);
|
|
1129
995
|
}
|
|
1130
996
|
return void 0;
|
|
1131
997
|
}
|
|
1132
|
-
function
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
998
|
+
function resolveAliasRequest(request, alias, cwd) {
|
|
999
|
+
for (const [key, value] of Object.entries(alias ?? {})) {
|
|
1000
|
+
const capture = matchAlias(request, key);
|
|
1001
|
+
if (capture === void 0) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const [target] = aliasValues(value);
|
|
1005
|
+
if (!target) {
|
|
1006
|
+
continue;
|
|
1138
1007
|
}
|
|
1008
|
+
return resolveAliasTarget(cwd, target, capture);
|
|
1139
1009
|
}
|
|
1140
|
-
|
|
1141
|
-
return verb ?? hidden;
|
|
1010
|
+
return void 0;
|
|
1142
1011
|
}
|
|
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
|
-
}
|
|
1012
|
+
function registerAliasResolver(alias, cwd) {
|
|
1013
|
+
if (!alias || Object.keys(alias).length === 0) {
|
|
1014
|
+
return () => {
|
|
1015
|
+
};
|
|
1152
1016
|
}
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1017
|
+
const moduleWithResolver = import_node_module.default;
|
|
1018
|
+
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
|
1019
|
+
moduleWithResolver._resolveFilename = function resolveWithGiriAlias(request, parent, isMain, options) {
|
|
1020
|
+
return originalResolveFilename.call(
|
|
1021
|
+
this,
|
|
1022
|
+
resolveAliasRequest(request, alias, cwd) ?? request,
|
|
1023
|
+
parent,
|
|
1024
|
+
isMain,
|
|
1025
|
+
options
|
|
1026
|
+
);
|
|
1027
|
+
};
|
|
1028
|
+
return () => {
|
|
1029
|
+
moduleWithResolver._resolveFilename = originalResolveFilename;
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
var GIRI_ALIAS_PREFIX = "$giri/";
|
|
1033
|
+
var giriOutDir;
|
|
1034
|
+
var giriResolverInstalled = false;
|
|
1035
|
+
function ensureGiriAliasResolver(outDir) {
|
|
1036
|
+
giriOutDir = outDir;
|
|
1037
|
+
if (giriResolverInstalled) {
|
|
1038
|
+
return;
|
|
1168
1039
|
}
|
|
1169
|
-
|
|
1040
|
+
giriResolverInstalled = true;
|
|
1041
|
+
const moduleWithResolver = import_node_module.default;
|
|
1042
|
+
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
|
1043
|
+
moduleWithResolver._resolveFilename = function resolveWithGiriInternalAlias(request, parent, isMain, options) {
|
|
1044
|
+
const mapped = typeof request === "string" && request.startsWith(GIRI_ALIAS_PREFIX) && giriOutDir ? (0, import_node_path3.join)(giriOutDir, request.slice(GIRI_ALIAS_PREFIX.length)) : request;
|
|
1045
|
+
return originalResolveFilename.call(this, mapped, parent, isMain, options);
|
|
1046
|
+
};
|
|
1170
1047
|
}
|
|
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);
|
|
1048
|
+
function resolveGiriPaths(config, cwd = process.cwd()) {
|
|
1049
|
+
return {
|
|
1050
|
+
cwd: (0, import_node_path3.resolve)(cwd),
|
|
1051
|
+
routesDir: (0, import_node_path3.resolve)(cwd, "src/routes"),
|
|
1052
|
+
outDir: (0, import_node_path3.resolve)(cwd, config.outDir ?? ".giri")
|
|
1185
1053
|
};
|
|
1054
|
+
}
|
|
1055
|
+
async function buildGiriApp(config, options = {}) {
|
|
1056
|
+
const paths = resolveGiriPaths(config, options.cwd);
|
|
1057
|
+
const routes = await scanRoutes(paths.routesDir);
|
|
1058
|
+
const app = config.adapter.createApp();
|
|
1059
|
+
ensureGiriAliasResolver(paths.outDir);
|
|
1060
|
+
const { unregister } = await safeRegister();
|
|
1061
|
+
const unregisterAliasResolver = registerAliasResolver(config.alias, paths.cwd);
|
|
1186
1062
|
try {
|
|
1187
1063
|
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 {
|
|
1064
|
+
const routeModule = loadModule(route.file);
|
|
1065
|
+
if (typeof routeModule.handle !== "function") {
|
|
1066
|
+
throw new Error(`${route.file} must export a named handle function.`);
|
|
1207
1067
|
}
|
|
1068
|
+
const folderMiddleware = routeModule.config?.skipInherited ? [] : route.sharedFiles.flatMap(
|
|
1069
|
+
(file) => normalizeMiddleware(loadModule(file).middleware, file)
|
|
1070
|
+
);
|
|
1071
|
+
const verbMiddleware = normalizeMiddleware(routeModule.middleware, route.file);
|
|
1072
|
+
config.adapter.register(app, {
|
|
1073
|
+
method: route.method,
|
|
1074
|
+
path: route.path,
|
|
1075
|
+
handle: routeModule.handle,
|
|
1076
|
+
middleware: [...folderMiddleware, ...verbMiddleware],
|
|
1077
|
+
input: routeInput(routeModule, route.file),
|
|
1078
|
+
services: options.services
|
|
1079
|
+
});
|
|
1208
1080
|
}
|
|
1209
1081
|
} finally {
|
|
1210
|
-
|
|
1082
|
+
unregisterAliasResolver();
|
|
1211
1083
|
unregister();
|
|
1212
1084
|
}
|
|
1213
|
-
return
|
|
1085
|
+
return { app, routes, paths };
|
|
1214
1086
|
}
|
|
1215
1087
|
|
|
1216
|
-
// src/generator/
|
|
1217
|
-
var
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1088
|
+
// src/generator/sync.ts
|
|
1089
|
+
var import_node_fs6 = require("fs");
|
|
1090
|
+
var import_promises3 = require("fs/promises");
|
|
1091
|
+
var import_node_path11 = require("path");
|
|
1092
|
+
|
|
1093
|
+
// src/generator/app-types.ts
|
|
1094
|
+
var import_node_fs4 = require("fs");
|
|
1095
|
+
var import_node_path5 = require("path");
|
|
1096
|
+
|
|
1097
|
+
// src/generator/util.ts
|
|
1098
|
+
var import_node_fs3 = require("fs");
|
|
1099
|
+
var import_promises2 = require("fs/promises");
|
|
1100
|
+
var import_node_path4 = require("path");
|
|
1101
|
+
var GENERATED_HEADER = "// Generated by giri sync. Do not edit.";
|
|
1102
|
+
function slash(path) {
|
|
1103
|
+
return path.split(import_node_path4.sep).join("/");
|
|
1104
|
+
}
|
|
1105
|
+
function importPath(fromFile, toFile) {
|
|
1106
|
+
let path = slash((0, import_node_path4.relative)((0, import_node_path4.dirname)(fromFile), toFile)).replace(/\.d\.ts$/, "");
|
|
1107
|
+
if (!path.startsWith(".")) {
|
|
1108
|
+
path = `./${path}`;
|
|
1231
1109
|
}
|
|
1232
|
-
|
|
1233
|
-
await writeGenerated(file, lines.join("\n"));
|
|
1110
|
+
return path;
|
|
1234
1111
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1112
|
+
function relativeConfigPath(fromDir, toPath) {
|
|
1113
|
+
let path = slash((0, import_node_path4.relative)(fromDir, toPath));
|
|
1114
|
+
if (!path.startsWith(".")) {
|
|
1115
|
+
path = `./${path}`;
|
|
1116
|
+
}
|
|
1117
|
+
return path;
|
|
1118
|
+
}
|
|
1119
|
+
function moduleSpecifier(fromDir, target) {
|
|
1120
|
+
let path = slash((0, import_node_path4.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
1121
|
+
if (!path.startsWith(".")) {
|
|
1122
|
+
path = `./${path}`;
|
|
1123
|
+
}
|
|
1124
|
+
return path;
|
|
1125
|
+
}
|
|
1126
|
+
function typeFilePath(paths, routeDir) {
|
|
1127
|
+
const sourceDir = (0, import_node_path4.relative)(paths.cwd, routeDir);
|
|
1128
|
+
return (0, import_node_path4.join)(paths.outDir, "types", sourceDir, "$types.d.ts");
|
|
1129
|
+
}
|
|
1130
|
+
function assertSafeOutDir(paths) {
|
|
1131
|
+
const rel = (0, import_node_path4.relative)(paths.cwd, paths.outDir);
|
|
1132
|
+
if (!rel || rel.startsWith("..") || rel.includes(`..${import_node_path4.sep}`)) {
|
|
1133
|
+
throw new Error(`Refusing to sync outside the project root: ${paths.outDir}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
var writeCache = /* @__PURE__ */ new Map();
|
|
1137
|
+
async function writeGenerated(path, content) {
|
|
1138
|
+
if (writeCache.get(path) === content && (0, import_node_fs3.existsSync)(path)) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
1142
|
+
await (0, import_promises2.writeFile)(path, content);
|
|
1143
|
+
writeCache.set(path, content);
|
|
1144
|
+
}
|
|
1145
|
+
async function writeJson(path, value) {
|
|
1146
|
+
await writeGenerated(path, `${JSON.stringify(value, null, 2)}
|
|
1147
|
+
`);
|
|
1148
|
+
}
|
|
1149
|
+
async function pruneDir(dir, keep) {
|
|
1150
|
+
let entries;
|
|
1151
|
+
try {
|
|
1152
|
+
entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
|
|
1153
|
+
} catch {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
for (const entry of entries) {
|
|
1157
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
1158
|
+
if (entry.isDirectory()) {
|
|
1159
|
+
await pruneDir(full, keep);
|
|
1160
|
+
await (0, import_promises2.rmdir)(full).catch(() => {
|
|
1161
|
+
});
|
|
1162
|
+
} else if (!keep.has(full)) {
|
|
1163
|
+
await (0, import_promises2.rm)(full, { force: true });
|
|
1164
|
+
writeCache.delete(full);
|
|
1257
1165
|
}
|
|
1258
1166
|
}
|
|
1259
|
-
return import_typescript.default.createProgram(routeFiles, options);
|
|
1260
1167
|
}
|
|
1261
1168
|
|
|
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
|
-
};
|
|
1169
|
+
// src/generator/app-types.ts
|
|
1170
|
+
var MAIN_EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
|
|
1171
|
+
function findMainFile(cwd) {
|
|
1172
|
+
for (const ext of MAIN_EXTENSIONS) {
|
|
1173
|
+
const file = (0, import_node_path5.join)(cwd, "src", `main.${ext}`);
|
|
1174
|
+
if ((0, import_node_fs4.existsSync)(file)) {
|
|
1175
|
+
return file;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return void 0;
|
|
1276
1179
|
}
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1180
|
+
function moduleSpecifier2(fromDir, target) {
|
|
1181
|
+
let path = slash((0, import_node_path5.relative)(fromDir, target)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
1182
|
+
if (!path.startsWith(".")) {
|
|
1183
|
+
path = `./${path}`;
|
|
1184
|
+
}
|
|
1185
|
+
return path;
|
|
1279
1186
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1187
|
+
async function writeAppTypes(paths) {
|
|
1188
|
+
const file = (0, import_node_path5.join)(paths.outDir, "types", "app.d.ts");
|
|
1189
|
+
const mainFile = findMainFile(paths.cwd);
|
|
1190
|
+
if (!mainFile) {
|
|
1191
|
+
await writeGenerated(file, [GENERATED_HEADER, "export {};", ""].join("\n"));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const spec = moduleSpecifier2((0, import_node_path5.join)(paths.outDir, "types"), mainFile);
|
|
1195
|
+
await writeGenerated(
|
|
1196
|
+
file,
|
|
1197
|
+
[
|
|
1198
|
+
GENERATED_HEADER,
|
|
1199
|
+
"declare global {",
|
|
1200
|
+
" namespace Giri {",
|
|
1201
|
+
" interface Register {",
|
|
1202
|
+
` app: typeof import(${JSON.stringify(spec)}) extends {`,
|
|
1203
|
+
" init: (...args: any[]) => infer R;",
|
|
1204
|
+
" }",
|
|
1205
|
+
" ? Awaited<R>",
|
|
1206
|
+
" : Record<string, unknown>;",
|
|
1207
|
+
" }",
|
|
1208
|
+
" }",
|
|
1209
|
+
"}",
|
|
1210
|
+
"export {};",
|
|
1211
|
+
""
|
|
1212
|
+
].join("\n")
|
|
1213
|
+
);
|
|
1282
1214
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1215
|
+
|
|
1216
|
+
// src/generator/manifest.ts
|
|
1217
|
+
var import_node_path6 = require("path");
|
|
1218
|
+
async function writeManifest(paths, routes, data = {}) {
|
|
1219
|
+
const manifest = {
|
|
1220
|
+
version: 1,
|
|
1221
|
+
routes: routes.map((route) => {
|
|
1222
|
+
const responses = data.responsesByFile?.get(route.file);
|
|
1223
|
+
const input = data.inputsByFile?.get(route.file);
|
|
1224
|
+
const security = data.securityByFile?.get(route.file);
|
|
1225
|
+
return {
|
|
1226
|
+
method: route.method,
|
|
1227
|
+
path: route.path,
|
|
1228
|
+
file: slash((0, import_node_path6.relative)(paths.cwd, route.file)),
|
|
1229
|
+
params: route.params,
|
|
1230
|
+
shared: route.sharedFiles.map((file) => slash((0, import_node_path6.relative)(paths.cwd, file))),
|
|
1231
|
+
types: slash((0, import_node_path6.relative)(paths.cwd, typeFilePath(paths, route.routeDir))),
|
|
1232
|
+
...data.hiddenFiles?.has(route.file) ? { hidden: true } : {},
|
|
1233
|
+
...input ? { input } : {},
|
|
1234
|
+
...security && security.security.length > 0 ? { security: security.security } : {},
|
|
1235
|
+
responses: responses?.responses ?? [],
|
|
1236
|
+
...responses && Object.keys(responses.$defs).length > 0 ? { $defs: responses.$defs } : {}
|
|
1237
|
+
};
|
|
1238
|
+
})
|
|
1239
|
+
};
|
|
1240
|
+
await writeJson((0, import_node_path6.join)(paths.outDir, "manifest.json"), manifest);
|
|
1286
1241
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1242
|
+
|
|
1243
|
+
// src/generator/openapi.ts
|
|
1244
|
+
var import_node_fs5 = require("fs");
|
|
1245
|
+
var import_node_path7 = require("path");
|
|
1246
|
+
var REASON = {
|
|
1247
|
+
200: "OK",
|
|
1248
|
+
201: "Created",
|
|
1249
|
+
202: "Accepted",
|
|
1250
|
+
204: "No Content",
|
|
1251
|
+
400: "Bad Request",
|
|
1252
|
+
401: "Unauthorized",
|
|
1253
|
+
403: "Forbidden",
|
|
1254
|
+
404: "Not Found",
|
|
1255
|
+
409: "Conflict",
|
|
1256
|
+
422: "Unprocessable Entity",
|
|
1257
|
+
500: "Internal Server Error"
|
|
1258
|
+
};
|
|
1259
|
+
function toOpenApiPath(path) {
|
|
1260
|
+
return path.replace(/:([A-Za-z0-9_]+)(?:\{[^}]*\})?/g, "{$1}");
|
|
1261
|
+
}
|
|
1262
|
+
function rewriteRefs(value) {
|
|
1263
|
+
if (Array.isArray(value)) {
|
|
1264
|
+
return value.map(rewriteRefs);
|
|
1265
|
+
}
|
|
1266
|
+
if (value && typeof value === "object") {
|
|
1267
|
+
const out = {};
|
|
1268
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1269
|
+
if (key === "$ref" && typeof child === "string" && child.startsWith("#/$defs/")) {
|
|
1270
|
+
out.$ref = child.replace("#/$defs/", "#/components/schemas/");
|
|
1271
|
+
} else {
|
|
1272
|
+
out[key] = rewriteRefs(child);
|
|
1273
|
+
}
|
|
1296
1274
|
}
|
|
1275
|
+
return out;
|
|
1297
1276
|
}
|
|
1298
|
-
return
|
|
1277
|
+
return value;
|
|
1299
1278
|
}
|
|
1300
|
-
function
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1279
|
+
function mediaTypeFor(format) {
|
|
1280
|
+
return format === "text" ? "text/plain" : "application/json";
|
|
1281
|
+
}
|
|
1282
|
+
var BODY_MEDIA_TYPE = {
|
|
1283
|
+
json: "application/json",
|
|
1284
|
+
form: "multipart/form-data",
|
|
1285
|
+
urlencoded: "application/x-www-form-urlencoded",
|
|
1286
|
+
text: "text/plain"
|
|
1287
|
+
};
|
|
1288
|
+
function buildResponses(responses) {
|
|
1289
|
+
if (responses.length === 0) {
|
|
1290
|
+
return { default: { description: "Response" } };
|
|
1305
1291
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1292
|
+
const out = {};
|
|
1293
|
+
for (const response of responses) {
|
|
1294
|
+
const key = response.status === "default" ? "default" : String(response.status);
|
|
1295
|
+
const description = typeof response.status === "number" && REASON[response.status] || "Response";
|
|
1296
|
+
out[key] = {
|
|
1297
|
+
description,
|
|
1298
|
+
content: { [mediaTypeFor(response.format)]: { schema: rewriteRefs(response.schema) } }
|
|
1299
|
+
};
|
|
1309
1300
|
}
|
|
1310
|
-
return
|
|
1301
|
+
return out;
|
|
1311
1302
|
}
|
|
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);
|
|
1303
|
+
function pathParameters(route) {
|
|
1304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1305
|
+
const params = [];
|
|
1306
|
+
for (const param of route.params) {
|
|
1307
|
+
if (seen.has(param.name)) {
|
|
1308
|
+
continue;
|
|
1324
1309
|
}
|
|
1310
|
+
seen.add(param.name);
|
|
1311
|
+
params.push({ name: param.name, in: "path", required: true, schema: { type: "string" } });
|
|
1325
1312
|
}
|
|
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;
|
|
1313
|
+
return params;
|
|
1339
1314
|
}
|
|
1340
|
-
function
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (name && name !== "__type" && name !== "__object") {
|
|
1344
|
-
return name;
|
|
1315
|
+
function queryParameters(query) {
|
|
1316
|
+
if (!query || query.type !== "object" || typeof query.properties !== "object") {
|
|
1317
|
+
return [];
|
|
1345
1318
|
}
|
|
1346
|
-
|
|
1319
|
+
const properties = query.properties;
|
|
1320
|
+
const required = Array.isArray(query.required) ? query.required : [];
|
|
1321
|
+
return Object.entries(properties).map(([name, schema]) => ({
|
|
1322
|
+
name,
|
|
1323
|
+
in: "query",
|
|
1324
|
+
required: required.includes(name),
|
|
1325
|
+
schema: rewriteRefs(schema)
|
|
1326
|
+
}));
|
|
1347
1327
|
}
|
|
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}` };
|
|
1328
|
+
function readProjectInfo(cwd) {
|
|
1329
|
+
const file = (0, import_node_path7.join)(cwd, "package.json");
|
|
1330
|
+
if ((0, import_node_fs5.existsSync)(file)) {
|
|
1331
|
+
try {
|
|
1332
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
|
|
1333
|
+
return { title: pkg.name ?? "giri API", version: pkg.version ?? "0.0.0" };
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1374
1336
|
}
|
|
1375
|
-
return
|
|
1337
|
+
return { title: "giri API", version: "0.0.0" };
|
|
1376
1338
|
}
|
|
1377
|
-
function
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1339
|
+
function buildOpenApiDocument(paths, routes, data = {}) {
|
|
1340
|
+
const documentPaths = {};
|
|
1341
|
+
const schemas = {};
|
|
1342
|
+
const securitySchemes = {};
|
|
1343
|
+
for (const route of routes) {
|
|
1344
|
+
if (data.hiddenFiles?.has(route.file)) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const responses = data.responsesByFile?.get(route.file);
|
|
1348
|
+
const input = data.inputsByFile?.get(route.file);
|
|
1349
|
+
const security = data.securityByFile?.get(route.file);
|
|
1350
|
+
for (const [name, schema] of Object.entries(responses?.$defs ?? {})) {
|
|
1351
|
+
schemas[name] = rewriteRefs(schema);
|
|
1352
|
+
}
|
|
1353
|
+
const operation = { responses: buildResponses(responses?.responses ?? []) };
|
|
1354
|
+
const parameters = [...pathParameters(route), ...queryParameters(input?.query)];
|
|
1355
|
+
if (parameters.length > 0) {
|
|
1356
|
+
operation.parameters = parameters;
|
|
1357
|
+
}
|
|
1358
|
+
if (input?.body) {
|
|
1359
|
+
const content = {};
|
|
1360
|
+
for (const [contentType, schema] of Object.entries(input.body)) {
|
|
1361
|
+
content[BODY_MEDIA_TYPE[contentType] ?? contentType] = {
|
|
1362
|
+
schema: rewriteRefs(schema)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (Object.keys(content).length > 0) {
|
|
1366
|
+
operation.requestBody = { required: true, content };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (security && security.security.length > 0) {
|
|
1370
|
+
operation.security = security.security;
|
|
1371
|
+
}
|
|
1372
|
+
if (security) {
|
|
1373
|
+
Object.assign(securitySchemes, security.securitySchemes);
|
|
1374
|
+
}
|
|
1375
|
+
const openApiPath = toOpenApiPath(route.path);
|
|
1376
|
+
const pathItem = documentPaths[openApiPath] ?? {};
|
|
1377
|
+
pathItem[route.method.toLowerCase()] = operation;
|
|
1378
|
+
documentPaths[openApiPath] = pathItem;
|
|
1384
1379
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1380
|
+
const document = {
|
|
1381
|
+
openapi: "3.1.0",
|
|
1382
|
+
info: readProjectInfo(paths.cwd),
|
|
1383
|
+
paths: documentPaths
|
|
1384
|
+
};
|
|
1385
|
+
const components = {};
|
|
1386
|
+
if (Object.keys(schemas).length > 0) {
|
|
1387
|
+
components.schemas = schemas;
|
|
1387
1388
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
return { type: "string" };
|
|
1389
|
+
if (Object.keys(securitySchemes).length > 0) {
|
|
1390
|
+
components.securitySchemes = securitySchemes;
|
|
1391
1391
|
}
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1392
|
+
if (Object.keys(components).length > 0) {
|
|
1393
|
+
document.components = components;
|
|
1394
1394
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1395
|
+
return document;
|
|
1396
|
+
}
|
|
1397
|
+
async function writeOpenApi(paths, routes, data = {}) {
|
|
1398
|
+
await writeJson((0, import_node_path7.join)(paths.outDir, "openapi.json"), buildOpenApiDocument(paths, routes, data));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/generator/param-types.ts
|
|
1402
|
+
var import_node_path8 = require("path");
|
|
1403
|
+
function paramsType(params) {
|
|
1404
|
+
if (params.length === 0) {
|
|
1405
|
+
return "{}";
|
|
1397
1406
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1407
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1408
|
+
for (const param of params) {
|
|
1409
|
+
unique.set(param.name, param);
|
|
1400
1410
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1411
|
+
const fields = [...unique.values()].map((param) => ` ${JSON.stringify(param.name)}: string;`).join("\n");
|
|
1412
|
+
return `{
|
|
1413
|
+
${fields}
|
|
1414
|
+
}`;
|
|
1415
|
+
}
|
|
1416
|
+
function varsType(typesDir, sharedFiles) {
|
|
1417
|
+
if (sharedFiles.length === 0) {
|
|
1418
|
+
return "{}";
|
|
1403
1419
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1420
|
+
return sharedFiles.map((file) => {
|
|
1421
|
+
const spec = JSON.stringify(moduleSpecifier(typesDir, file));
|
|
1422
|
+
return `(typeof import(${spec}) extends { middleware: infer M } ? import("@boon4681/giri").InferStackVars<M> : {})`;
|
|
1423
|
+
}).join("\n & ");
|
|
1424
|
+
}
|
|
1425
|
+
function methodExports(typesDir, verbs) {
|
|
1426
|
+
return verbs.map(({ method, file }) => {
|
|
1427
|
+
const spec = JSON.stringify(moduleSpecifier(typesDir, file));
|
|
1428
|
+
const input = `import("@boon4681/giri").RouteInputOf<typeof import(${spec})>`;
|
|
1429
|
+
const vars = `Vars & import("@boon4681/giri").MiddlewareVarsOf<typeof import(${spec})>`;
|
|
1430
|
+
return `export type ${method} = import("@boon4681/giri").Handle<Params, ${input}, ${vars}>;`;
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
async function writeParamTypes(paths, folders) {
|
|
1434
|
+
for (const { dir, params, sharedFiles, verbs } of folders) {
|
|
1435
|
+
const file = typeFilePath(paths, dir);
|
|
1436
|
+
const typesDir = (0, import_node_path8.dirname)(file);
|
|
1437
|
+
const lines = [
|
|
1438
|
+
GENERATED_HEADER,
|
|
1439
|
+
`export type Params = ${paramsType(params)};`,
|
|
1440
|
+
"export type RouteParams = Params;",
|
|
1441
|
+
`type Vars = ${varsType(typesDir, sharedFiles)};`,
|
|
1442
|
+
"export type Middleware<Injects extends Record<string, unknown> = {}> =",
|
|
1443
|
+
' import("@boon4681/giri").Middleware<Params, import("@boon4681/giri").ValidatedInput, Injects>;',
|
|
1444
|
+
'export type Handle<Input extends import("@boon4681/giri").ValidatedInput = import("@boon4681/giri").ValidatedInput> =',
|
|
1445
|
+
' import("@boon4681/giri").Handle<Params, Input, Vars>;'
|
|
1446
|
+
];
|
|
1447
|
+
if (verbs.length > 0) {
|
|
1448
|
+
lines.push(...methodExports(typesDir, verbs));
|
|
1449
|
+
}
|
|
1450
|
+
lines.push("");
|
|
1451
|
+
await writeGenerated(file, lines.join("\n"));
|
|
1406
1452
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/generator/inputs.ts
|
|
1456
|
+
function sanitize(schema) {
|
|
1457
|
+
const { $schema, ...rest } = schema;
|
|
1458
|
+
void $schema;
|
|
1459
|
+
return rest;
|
|
1460
|
+
}
|
|
1461
|
+
function inputToJsonSchema(schema) {
|
|
1462
|
+
if (!isGiriInputSchema(schema)) {
|
|
1463
|
+
return void 0;
|
|
1409
1464
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1465
|
+
return sanitize(schema.toJsonSchema());
|
|
1466
|
+
}
|
|
1467
|
+
function bodyToJsonSchemas(value) {
|
|
1468
|
+
if (!isGiriBodySchema(value)) {
|
|
1469
|
+
return void 0;
|
|
1412
1470
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1471
|
+
const out = {};
|
|
1472
|
+
for (const [contentType, schema] of Object.entries(value.contents)) {
|
|
1473
|
+
const json = inputToJsonSchema(schema);
|
|
1474
|
+
if (json) {
|
|
1475
|
+
out[contentType] = json;
|
|
1476
|
+
}
|
|
1415
1477
|
}
|
|
1416
|
-
return
|
|
1478
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1417
1479
|
}
|
|
1418
1480
|
|
|
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
|
-
}
|
|
1481
|
+
// src/generator/route-meta.ts
|
|
1482
|
+
function loadModule2(file) {
|
|
1483
|
+
const resolved = require.resolve(file);
|
|
1484
|
+
delete require.cache[resolved];
|
|
1485
|
+
return require(resolved);
|
|
1486
|
+
}
|
|
1487
|
+
function interopDefault2(value) {
|
|
1488
|
+
if (value && typeof value === "object" && "default" in value) {
|
|
1489
|
+
return value.default;
|
|
1434
1490
|
}
|
|
1435
|
-
return
|
|
1491
|
+
return value;
|
|
1436
1492
|
}
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1493
|
+
function middlewareFunctions(value) {
|
|
1494
|
+
const exported = interopDefault2(value);
|
|
1495
|
+
if (typeof exported === "function") {
|
|
1496
|
+
return [exported];
|
|
1440
1497
|
}
|
|
1441
|
-
if (
|
|
1442
|
-
return
|
|
1498
|
+
if (Array.isArray(exported)) {
|
|
1499
|
+
return exported.filter((fn) => typeof fn === "function");
|
|
1443
1500
|
}
|
|
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
|
-
);
|
|
1501
|
+
return [];
|
|
1465
1502
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1503
|
+
function readInput(routeModule) {
|
|
1504
|
+
const input = {};
|
|
1505
|
+
const body = bodyToJsonSchemas(routeModule.body);
|
|
1506
|
+
const query = inputToJsonSchema(routeModule.query);
|
|
1507
|
+
if (body) {
|
|
1508
|
+
input.body = body;
|
|
1469
1509
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return void 0;
|
|
1510
|
+
if (query) {
|
|
1511
|
+
input.query = query;
|
|
1473
1512
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1513
|
+
return input.body || input.query ? input : void 0;
|
|
1514
|
+
}
|
|
1515
|
+
function hiddenFrom(value) {
|
|
1516
|
+
if (value === false) {
|
|
1517
|
+
return true;
|
|
1476
1518
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
return void 0;
|
|
1519
|
+
if (value === true) {
|
|
1520
|
+
return false;
|
|
1480
1521
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const statusType = checker.getTypeAtLocation(statusArg);
|
|
1484
|
-
status = statusType.isNumberLiteral() ? statusType.value : "default";
|
|
1522
|
+
if (value && typeof value === "object" && "hidden" in value) {
|
|
1523
|
+
return Boolean(value.hidden);
|
|
1485
1524
|
}
|
|
1486
|
-
return
|
|
1525
|
+
return void 0;
|
|
1487
1526
|
}
|
|
1488
|
-
function
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1527
|
+
function collectHidden(route, routeModule, loadShared) {
|
|
1528
|
+
let hidden = false;
|
|
1529
|
+
for (const file of route.sharedFiles) {
|
|
1530
|
+
const opinion = hiddenFrom(loadShared(file).openapi);
|
|
1531
|
+
if (opinion !== void 0) {
|
|
1532
|
+
hidden = opinion;
|
|
1533
|
+
}
|
|
1494
1534
|
}
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
return { status, format, data: dataType };
|
|
1498
|
-
}
|
|
1499
|
-
function constituents(type) {
|
|
1500
|
-
return type.isUnion() ? type.types : [type];
|
|
1535
|
+
const verb = hiddenFrom(routeModule.openapi);
|
|
1536
|
+
return verb ?? hidden;
|
|
1501
1537
|
}
|
|
1502
|
-
function
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
return result;
|
|
1538
|
+
function collectSecurity(route, routeModule, loadShared) {
|
|
1539
|
+
const skipInherited = Boolean(
|
|
1540
|
+
routeModule.config?.skipInherited
|
|
1541
|
+
);
|
|
1542
|
+
const middleware = [];
|
|
1543
|
+
if (!skipInherited) {
|
|
1544
|
+
for (const file of route.sharedFiles) {
|
|
1545
|
+
middleware.push(...middlewareFunctions(loadShared(file).middleware));
|
|
1546
|
+
}
|
|
1512
1547
|
}
|
|
1513
|
-
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
record(fromCall);
|
|
1525
|
-
continue;
|
|
1548
|
+
middleware.push(...middlewareFunctions(routeModule.middleware));
|
|
1549
|
+
const security = [];
|
|
1550
|
+
const securitySchemes = {};
|
|
1551
|
+
for (const fn of middleware) {
|
|
1552
|
+
const openapi = fn.openapi;
|
|
1553
|
+
if (openapi?.security) {
|
|
1554
|
+
for (const requirement of openapi.security) {
|
|
1555
|
+
if (!security.some((seen) => JSON.stringify(seen) === JSON.stringify(requirement))) {
|
|
1556
|
+
security.push(requirement);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1526
1559
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1560
|
+
if (openapi?.securitySchemes) {
|
|
1561
|
+
Object.assign(securitySchemes, openapi.securitySchemes);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return security.length > 0 || Object.keys(securitySchemes).length > 0 ? { security, securitySchemes } : void 0;
|
|
1565
|
+
}
|
|
1566
|
+
async function extractRouteMeta(config, paths, routes) {
|
|
1567
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1568
|
+
const { unregister } = await safeRegister();
|
|
1569
|
+
const unregisterAlias = registerAliasResolver(config.alias, paths.cwd);
|
|
1570
|
+
const sharedCache = /* @__PURE__ */ new Map();
|
|
1571
|
+
const loadShared = (file) => {
|
|
1572
|
+
if (!sharedCache.has(file)) {
|
|
1573
|
+
try {
|
|
1574
|
+
sharedCache.set(file, loadModule2(file));
|
|
1575
|
+
} catch {
|
|
1576
|
+
sharedCache.set(file, {});
|
|
1533
1577
|
}
|
|
1534
1578
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1579
|
+
return sharedCache.get(file);
|
|
1580
|
+
};
|
|
1581
|
+
try {
|
|
1582
|
+
for (const route of routes) {
|
|
1583
|
+
try {
|
|
1584
|
+
const routeModule = loadModule2(route.file);
|
|
1585
|
+
const meta = {};
|
|
1586
|
+
const input = readInput(routeModule);
|
|
1587
|
+
const security = collectSecurity(route, routeModule, loadShared);
|
|
1588
|
+
const hidden = collectHidden(route, routeModule, loadShared);
|
|
1589
|
+
if (input) {
|
|
1590
|
+
meta.input = input;
|
|
1591
|
+
}
|
|
1592
|
+
if (security) {
|
|
1593
|
+
meta.security = security;
|
|
1594
|
+
}
|
|
1595
|
+
if (hidden) {
|
|
1596
|
+
meta.hidden = true;
|
|
1597
|
+
}
|
|
1598
|
+
if (meta.input || meta.security || meta.hidden) {
|
|
1599
|
+
byFile.set(route.file, meta);
|
|
1600
|
+
}
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1537
1603
|
}
|
|
1604
|
+
} finally {
|
|
1605
|
+
unregisterAlias();
|
|
1606
|
+
unregister();
|
|
1538
1607
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1608
|
+
return byFile;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/generator/route-types.ts
|
|
1612
|
+
var import_node_path9 = require("path");
|
|
1613
|
+
async function writeRouteTypes(paths, routes) {
|
|
1614
|
+
const file = (0, import_node_path9.join)(paths.outDir, "routes.d.ts");
|
|
1615
|
+
const lines = [
|
|
1616
|
+
GENERATED_HEADER,
|
|
1617
|
+
"export interface RouteParams {"
|
|
1618
|
+
];
|
|
1619
|
+
for (const route of routes) {
|
|
1620
|
+
const typeFile = typeFilePath(paths, route.routeDir);
|
|
1621
|
+
lines.push(
|
|
1622
|
+
` ${JSON.stringify(`${route.method} ${route.path}`)}: import(${JSON.stringify(
|
|
1623
|
+
importPath(file, typeFile)
|
|
1624
|
+
)}).Params;`
|
|
1625
|
+
);
|
|
1542
1626
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
result.$defs = ctx.defs;
|
|
1546
|
-
return result;
|
|
1627
|
+
lines.push("}", "");
|
|
1628
|
+
await writeGenerated(file, lines.join("\n"));
|
|
1547
1629
|
}
|
|
1548
1630
|
|
|
1549
1631
|
// src/generator/tsconfig.ts
|
|
@@ -1602,20 +1684,21 @@ async function typeFolders(paths, routes) {
|
|
|
1602
1684
|
verbs: verbsByDir.get(slash(dir)) ?? []
|
|
1603
1685
|
}));
|
|
1604
1686
|
}
|
|
1605
|
-
function extractResponses(paths, routes) {
|
|
1687
|
+
async function extractResponses(paths, routes) {
|
|
1606
1688
|
const byFile = /* @__PURE__ */ new Map();
|
|
1607
1689
|
if (routes.length === 0) {
|
|
1608
1690
|
return byFile;
|
|
1609
1691
|
}
|
|
1610
1692
|
try {
|
|
1693
|
+
const { createSchemaProgram: createSchemaProgram2, extractRouteResponses: extractRouteResponses2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
|
|
1611
1694
|
const files = [...new Set(routes.map((route) => route.file))];
|
|
1612
1695
|
const appTypes = (0, import_node_path11.join)(paths.outDir, "types", "app.d.ts");
|
|
1613
|
-
const program =
|
|
1696
|
+
const program = createSchemaProgram2(
|
|
1614
1697
|
paths,
|
|
1615
1698
|
(0, import_node_fs6.existsSync)(appTypes) ? [...files, appTypes] : files
|
|
1616
1699
|
);
|
|
1617
1700
|
for (const file of files) {
|
|
1618
|
-
byFile.set(file,
|
|
1701
|
+
byFile.set(file, extractRouteResponses2(program, file));
|
|
1619
1702
|
}
|
|
1620
1703
|
} catch (error) {
|
|
1621
1704
|
console.warn(`giri: skipped response schema generation (${error.message}).`);
|
|
@@ -1657,7 +1740,7 @@ async function syncProject(config, options = {}) {
|
|
|
1657
1740
|
await writeRouteTypes(paths, routes);
|
|
1658
1741
|
await writeAppTypes(paths);
|
|
1659
1742
|
await writeTsConfig(paths, config);
|
|
1660
|
-
const responsesByFile = extractResponses(paths, routes);
|
|
1743
|
+
const responsesByFile = await extractResponses(paths, routes);
|
|
1661
1744
|
const { inputsByFile, securityByFile, hiddenFiles } = await extractMeta(config, paths, routes);
|
|
1662
1745
|
const data = { responsesByFile, inputsByFile, securityByFile, hiddenFiles };
|
|
1663
1746
|
await writeManifest(paths, routes, data);
|