Skip to content

Commit f29f526

Browse files
committed
improvements
0 parents  commit f29f526

File tree

9 files changed

+1518
-0
lines changed

9 files changed

+1518
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.wrangler

ADR.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
To provide NPM compatibility for installation, a package manager like JSR would need to implement these key registry API endpoints:
2+
3+
1. Package metadata endpoint: `GET /[package-name]`
4+
5+
- Returns package information including versions, dependencies, and dist information
6+
- Critical for npm to resolve the package and its dependencies
7+
8+
2. Package version endpoint: `GET /[package-name]/[version]`
9+
10+
- Returns metadata for a specific version of the package
11+
12+
3. Package tarball endpoint: `GET /[package-name]/-/[package-name]-[version].tgz`
13+
14+
- Provides the actual tarball file for download
15+
- This is where npm retrieves the package content from
16+
17+
These endpoints form the minimal set required to allow npm install commands to work properly. The registry must respond with proper headers and tarball content that matches npm's expectations for format and structure.
18+
19+
Would you like more details about any specific endpoint implementation or other aspects of npm compatibility?

TODO.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# oapis package manager
2+
3+
## TODO: Make it a package manager
4+
5+
1. use `generateTypescript` correctly
6+
2. use swcapi for stripping types
7+
3. use https://www.npmjs.com/package/@gera2ld/tarjs for tar file creation
8+
4. test and see if installation works using `npm config set @oapis:registry https://npm.oapis.org` and `npm i @oapis/{domain}`
9+
10+
GOAL: Use this for any API I made. If that works:
11+
12+
- also auto-generate `.d.ts`
13+
- use `.d.ts` to generate docs as well
14+
- expose HTML at https://oapis.org/{domain}/{operationId}
15+
- make it deno-compatible as well, including for deno's URL-import feature.
16+
- make it browser-compatible as well (script src="https://js.oapis.org/{domain}/{operationId}.js")
17+
- add versioning and caching, ensuring to check the openapi.json for a changed version if needed. if needed also auto-bump version number (likely not needed).

generateTypescript.ts

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { OpenapiDocument, Operation, Parameter, JSONSchema } from "./types";
2+
3+
function getBaseUrl(openapiUrl: string, server?: { url: string }): string {
4+
// Try to get base URL from servers array first
5+
if (server) {
6+
try {
7+
// Check if it's a valid URL
8+
new URL(server.url);
9+
return server.url.replace(/\/$/, ""); // Remove trailing slash if present
10+
} catch (e) {
11+
// If not a valid URL, it might be a relative path - ignore
12+
}
13+
}
14+
15+
// Fallback to openapiUrl origin
16+
try {
17+
const url = new URL(openapiUrl);
18+
return url.origin;
19+
} catch (e) {
20+
throw new Error(
21+
"Unable to determine base URL from either servers or openapiUrl",
22+
);
23+
}
24+
}
25+
26+
function jsonSchemaToTS(schema: JSONSchema, indentLevel = 1): string {
27+
if (!schema) return "any";
28+
29+
const indent = " ".repeat(indentLevel);
30+
31+
if (schema.$ref) {
32+
// Handle references - in a full implementation, you'd resolve these
33+
return schema.$ref.split("/").pop() || "any";
34+
}
35+
36+
if (schema.enum) {
37+
return schema.enum
38+
.map((value) => (typeof value === "string" ? `"${value}"` : value))
39+
.join(" | ");
40+
}
41+
42+
if (schema.oneOf) {
43+
return schema.oneOf.map((s) => jsonSchemaToTS(s, indentLevel)).join(" | ");
44+
}
45+
46+
if (schema.anyOf) {
47+
return schema.anyOf.map((s) => jsonSchemaToTS(s, indentLevel)).join(" | ");
48+
}
49+
50+
if (schema.allOf) {
51+
return schema.allOf.map((s) => jsonSchemaToTS(s, indentLevel)).join(" & ");
52+
}
53+
54+
switch (schema.type) {
55+
case "object": {
56+
if (!schema.properties) return "{ [key: string]: any }";
57+
58+
const requiredProps = schema.required || [];
59+
const properties = Object.entries(schema.properties)
60+
.map(([key, prop]) => {
61+
const isRequired = requiredProps.includes(key);
62+
const typeStr = jsonSchemaToTS(prop, indentLevel + 1);
63+
const description = prop.description
64+
? `\n${indent}/** ${prop.description} */\n${indent}`
65+
: "";
66+
return `${description}"${key}"${isRequired ? "" : "?"}: ${typeStr};`;
67+
})
68+
.join(`\n${indent}`);
69+
70+
return `{\n${indent}${properties}\n${indent.slice(2)}}`;
71+
}
72+
73+
case "array":
74+
return `Array<${jsonSchemaToTS(schema.items || {}, indentLevel)}>`;
75+
76+
case "string":
77+
return schema.nullable ? "string | null" : "string";
78+
79+
case "number":
80+
case "integer":
81+
return schema.nullable ? "number | null" : "number";
82+
83+
case "boolean":
84+
return schema.nullable ? "boolean | null" : "boolean";
85+
86+
case "null":
87+
return "null";
88+
89+
default:
90+
return "any";
91+
}
92+
}
93+
94+
export function generateTypeScript(
95+
openapi: OpenapiDocument,
96+
method: string,
97+
operation: Operation,
98+
path: string,
99+
openapiUrl: string,
100+
isExported?: boolean,
101+
): string {
102+
const baseUrl = getBaseUrl(
103+
openapiUrl,
104+
operation.servers?.[0] || openapi.servers?.[0],
105+
);
106+
107+
// Generate parameter types
108+
const parameters: Parameter[] = operation.parameters || [];
109+
110+
const headerParams = parameters.filter((p) => p.in === "header");
111+
const queryParams = parameters.filter((p) => p.in === "query");
112+
const pathParams = parameters.filter((p) => p.in === "path");
113+
const requestBody =
114+
operation.requestBody?.content?.["application/json"]?.schema;
115+
116+
// Build request type properties
117+
const requestProperties: string[] = [];
118+
119+
if (headerParams.length) {
120+
requestProperties.push(
121+
`headers: ${jsonSchemaToTS({
122+
type: "object",
123+
required: headerParams.filter((p) => p.required).map((p) => p.name),
124+
properties: Object.fromEntries(
125+
headerParams.map((p) => [
126+
p.name,
127+
{ ...p.schema, description: p.description },
128+
]),
129+
),
130+
})}`,
131+
);
132+
}
133+
134+
if (queryParams.length) {
135+
requestProperties.push(
136+
`query: ${jsonSchemaToTS({
137+
type: "object",
138+
required: queryParams.filter((p) => p.required).map((p) => p.name),
139+
properties: Object.fromEntries(
140+
queryParams.map((p) => [
141+
p.name,
142+
{ ...p.schema, description: p.description },
143+
]),
144+
),
145+
})}`,
146+
);
147+
}
148+
149+
if (pathParams.length) {
150+
requestProperties.push(
151+
`path: ${jsonSchemaToTS({
152+
type: "object",
153+
required: pathParams.map((p) => p.name), // All path parameters are required
154+
properties: Object.fromEntries(
155+
pathParams.map((p) => [
156+
p.name,
157+
{ ...p.schema, description: p.description },
158+
]),
159+
),
160+
})}`,
161+
);
162+
}
163+
164+
if (requestBody) {
165+
requestProperties.push(`body: ${jsonSchemaToTS(requestBody)}`);
166+
}
167+
168+
// Generate response type
169+
const successResponse = operation.responses?.["200"];
170+
const mediaTypes = successResponse?.content
171+
? Object.keys(successResponse.content)
172+
: [];
173+
const isJson = mediaTypes.includes("application/json");
174+
const responseBody =
175+
successResponse?.content?.[isJson ? "application/json" : mediaTypes[0]]
176+
?.schema;
177+
178+
const responseHeaders = successResponse?.headers;
179+
180+
// Generate response headers type with proper typing for known headers
181+
let responseHeadersType = "Record<string, string>";
182+
if (responseHeaders) {
183+
const headerProperties = Object.entries(responseHeaders).reduce(
184+
(acc, [name, header]: [string, any]) => {
185+
const headerName = name.toLowerCase(); // HTTP headers are case-insensitive
186+
const schema = header.schema || { type: "string" };
187+
const description = header.description
188+
? `/** ${header.description} */\n `
189+
: "";
190+
return (
191+
acc + `${description}"${headerName}": ${jsonSchemaToTS(schema)};\n `
192+
);
193+
},
194+
"",
195+
);
196+
197+
responseHeadersType = `{
198+
${headerProperties}
199+
// Allow additional string headers
200+
[key: string]: string | number | boolean | undefined;
201+
}`;
202+
}
203+
204+
const code = `
205+
/**
206+
* Generated Types for ${path}
207+
*/
208+
209+
${
210+
requestProperties.length
211+
? `type RequestType = {
212+
${requestProperties.join(";\n ")};
213+
};`
214+
: "type RequestType = Record<string, never>;"
215+
}
216+
217+
type ResponseType = {
218+
status: number;
219+
headers: ${responseHeadersType};
220+
body: ${responseBody ? jsonSchemaToTS(responseBody) : "void"};
221+
};
222+
223+
/**
224+
* Makes a request to ${path}
225+
* @param request The request parameters
226+
* @returns A promise that resolves to the response
227+
*/
228+
${
229+
isExported ? "export default" : ""
230+
} async function makeRequest(request: RequestType): Promise<ResponseType> {
231+
// Build URL with path parameters
232+
let url = "${baseUrl}${path}";
233+
${
234+
pathParams.length
235+
? `
236+
Object.entries(request.path).forEach(([key, value]) => {
237+
url = url.replace(\`{\${key}\`, encodeURIComponent(String(value)));
238+
});`
239+
: ""
240+
}
241+
242+
${
243+
queryParams.length
244+
? `
245+
// Add query parameters
246+
const queryParams = new URLSearchParams();
247+
Object.entries(request.query).forEach(([key, value]) => {
248+
if (value !== undefined && value !== null) {
249+
queryParams.append(key, String(value));
250+
}
251+
});
252+
const queryString = queryParams.toString();
253+
if (queryString) {
254+
url += "?" + queryString;
255+
}`
256+
: ""
257+
}
258+
259+
// Make the request
260+
const response = await fetch(url, {
261+
method: "${method.toUpperCase()}",
262+
${headerParams.length ? "headers: request.headers," : ""}
263+
${requestBody ? "body: JSON.stringify(request.body)," : ""}
264+
});
265+
266+
// Parse response
267+
const responseBody = ${
268+
isJson ? "await response.json()" : "await response.text()"
269+
};
270+
271+
// Convert headers to typed object
272+
const responseHeaders: Record<string, string> = {};
273+
response.headers.forEach((value, key) => {
274+
responseHeaders[key.toLowerCase()] = value;
275+
});
276+
277+
return {
278+
status: response.status,
279+
headers: responseHeaders as ResponseType['headers'],
280+
body: responseBody,
281+
};
282+
}
283+
`;
284+
285+
return code;
286+
}

0 commit comments

Comments
 (0)