Compare commits

..

No commits in common. "main" and "v0.4.1" have entirely different histories.
main ... v0.4.1

5 changed files with 11 additions and 85 deletions

View file

@ -48,55 +48,12 @@ for path, methods in paths.items():
# breaks strict consumers like Orval's Zod generator. Strip empty arrays —
# absence of the keyword has the same semantics.
schemas = spec.get("components", {}).get("schemas", {})
stripped_required = 0
stripped = 0
for sch in schemas.values():
if isinstance(sch, dict) and sch.get("required") == []:
del sch["required"]
stripped_required += 1
# oatpp 1.3 emits `"type": "Any"` for `oatpp::Any` / `oatpp::Fields` fields.
# `Any` is not a valid OpenAPI 3.0 type — the spec uses an empty schema `{}`
# (or `nullable: true` with no type) to mean "any value". Replace `Any` with
# an empty schema; recurse so we catch nested cases (object property,
# array item, etc.).
fixed_any = 0
def fix_any(node):
global fixed_any
if isinstance(node, dict):
if node.get("type") == "Any":
del node["type"]
fixed_any += 1
for v in node.values():
fix_any(v)
elif isinstance(node, list):
for item in node:
fix_any(item)
fix_any(spec)
# oatpp 1.3 sometimes emits the same path parameter twice (same name + same
# `in: path`). OpenAPI 3.0 rejects duplicate parameters. Dedupe by (name, in)
# preserving first occurrence.
deduped_params = 0
for path_item in paths.values():
if not isinstance(path_item, dict):
continue
for op in path_item.values():
if not isinstance(op, dict) or "parameters" not in op:
continue
seen = set()
unique = []
for p in op["parameters"]:
key = (p.get("name"), p.get("in"))
if key in seen:
deduped_params += 1
continue
seen.add(key)
unique.append(p)
op["parameters"] = unique
stripped += 1
SRC.write_text(json.dumps(spec, indent=2))
report = [f"{len(paths)} paths"]
if stripped_required: report.append(f"stripped empty `required` from {stripped_required} schemas")
if fixed_any: report.append(f"replaced {fixed_any} invalid `type: Any` with any-schema")
if deduped_params: report.append(f"deduped {deduped_params} duplicate parameter(s)")
print(f" postprocessed {SRC}" + ", ".join(report))
print(f" postprocessed {SRC}{len(paths)} paths"
+ (f", stripped empty `required` from {stripped} schemas" if stripped else ""))

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@uschuster/webapp-scaffold",
"version": "0.5.0",
"version": "0.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@uschuster/webapp-scaffold",
"version": "0.5.0",
"version": "0.3.6",
"license": "UNLICENSED",
"bin": {
"webapp-scaffold-fetch-openapi": "bin/fetch-openapi.sh",

View file

@ -1,6 +1,6 @@
{
"name": "@uschuster/webapp-scaffold",
"version": "0.5.0",
"version": "0.4.1",
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
"type": "module",
"bin": {

View file

@ -173,23 +173,4 @@ describe('createCoreFetch — request shape', () => {
await cf('/v1/x');
expect(captured).toBe('https://api.example.com/v1/x');
});
it('baseUrl getter is resolved at request time, not factory time', async () => {
const captured: string[] = [];
let dynamicBase = 'https://first.example.com';
const cf = createCoreFetch({
baseUrl: () => dynamicBase,
fetchImpl: ((url: string) => {
captured.push(url);
return Promise.resolve(jsonResponse({}));
}) as unknown as typeof fetch,
});
await cf('/v1/x');
dynamicBase = 'https://second.example.com/';
await cf('/v1/y');
expect(captured).toEqual([
'https://first.example.com/v1/x',
'https://second.example.com/v1/y',
]);
});
});

View file

@ -28,16 +28,8 @@ export interface CoreFetchRequest {
}
export interface CoreFetchOptions {
/**
* Prefix for every request; trailing slash stripped at resolve time.
*
* Accepts a string OR a getter so consumers that derive the base from
* `import.meta.env.BASE_URL` (or any other late-bound source) can pass
* the getter form. The getter is invoked at request time, never at
* factory-construction time which keeps orval's CJS bundling of the
* mutator file from blowing up on top-level `import.meta` references.
*/
baseUrl?: string | (() => string);
/** Prefix for every request; strip trailing slash. */
baseUrl?: string;
/** `'body'` returns the parsed body (default); `'wrapped'` returns { data, status, headers }. */
responseShape?: ResponseShape;
@ -105,11 +97,7 @@ export type CoreFetch = CoreFetchWrapped & CoreFetchBody;
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
// Lazy base resolution — see CoreFetchOptions.baseUrl JSDoc.
const resolveBase = (): string => {
const raw = typeof opts.baseUrl === 'function' ? opts.baseUrl() : (opts.baseUrl ?? '');
return raw.replace(/\/$/, '');
};
const base = (opts.baseUrl ?? '').replace(/\/$/, '');
// Resolve fetch at call time, not construction time, so tests that
// `vi.stubGlobal('fetch', ...)` after module import see the stub.
const callFetch: typeof fetch = opts.fetchImpl
@ -147,7 +135,7 @@ export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
let r: Response;
try {
r = await callFetch(`${resolveBase()}${url}`, {
r = await callFetch(`${base}${url}`, {
credentials: 'include',
...init,
headers,