Added the frontend in web/, following its code conventions, with linting and generated backend types
web/CODE_CONVENTIONS.md covers the Vite, SolidJS, Tailwind and TypeScript frontend: tooling, files, imports, exports, naming, TypeScript, formatting, Solid's reactivity, styling, the backend's types, API calls, errors, security, formatting values, comments, accessibility, storage, dependencies and tests. npm run check (strict tsc -b, ESLint and Prettier) passes, and npm run build builds it. ESLint (typescript-eslint with type information, eslint-plugin-solid, simple-import-sort, eslint-comments, and rules for the naming, type import, export type and promise conventions) and Prettier (with the Tailwind class plugin) check what they can. Tool versions are pinned exactly. Imports between folders use @/, which works in Vite as well as TypeScript. The backend's types are generated from its OpenAPI description into src/types/api.ts by npm run generate:api, which turns swag's x-nullable into null types first; src/types/Opportunities.ts names them for the app. The app works against the backend's API: it fetches every page of /api/opportunities with each opportunity's marks, the classifications to filter by from /api/classifications, sets and clears marks (hidden among them) with the CSRF token, and shows the OCDS tender statuses. Its lists' requests are aborted when reloaded or no longer needed. The dev server proxies /api and /ws to job-search serve, by default at localhost:9393. web/go.mod keeps web/ out of the Go module, as an npm package in node_modules ships Go code. The page stays up to date over the backend's WebSocket (src/api/events.ts, with hand-written event types in src/types/Events.ts), reconnecting with a growing delay. ClassificationsUpdated changes the filter's classifications in place and OpportunityMarksUpdated sets an opportunity's marks (unless this page is still saving its own change). OpportunitiesUpdated catches the list up: it asks for the opportunities changed after the last change batch it has seen (changed_after) and merges them in, reloading the whole list only when the backend doesn't know the batch. A reconnect catches up the same way and reloads the classifications.
This commit is contained in:
1 parent
9905ffb4cb
commit
1832ea7437
42 files changed
+8019
No files matched your search
@@ -0,0 +1,56 @@
|
||||
// Generates src/types/api.ts, the TypeScript types of the backend's API, from
|
||||
// its OpenAPI 3.1 description:
|
||||
//
|
||||
// npm run generate:api [-- <path or URL of openapi.json>]
|
||||
//
|
||||
// The default is the backend's generated file, ../internal/api/openapi/
|
||||
// swagger.json (run go generate ./... in the repository first); a running
|
||||
// server's http://localhost:9393/api/openapi.json works too.
|
||||
//
|
||||
// swag marks a Go field that can be null (a pointer, or a slice that can be
|
||||
// nil) with the x-nullable extension, which openapi-typescript ignores, so
|
||||
// those properties are rewritten into OpenAPI 3.1's own form first.
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import type { OpenAPI3 } from 'openapi-typescript';
|
||||
import openapiTS, { astToString } from 'openapi-typescript';
|
||||
|
||||
const defaultSource = '../internal/api/openapi/swagger.json';
|
||||
const output = 'src/types/api.ts';
|
||||
|
||||
const header = `// Code generated by npm run generate:api from the backend's OpenAPI
|
||||
// description. DO NOT EDIT: change the Go types or handlers, then
|
||||
// regenerate (see scripts/generateApiTypes.ts).
|
||||
|
||||
`;
|
||||
|
||||
type Schema = Record<string, unknown>;
|
||||
|
||||
/** Rewrites every x-nullable property of the description's schemas so it allows null. */
|
||||
function allowNulls(description: OpenAPI3): void {
|
||||
const schemas = (description.components?.schemas ?? {}) as Record<string, Schema>;
|
||||
for (const schema of Object.values(schemas)) {
|
||||
const properties = (schema.properties ?? {}) as Record<string, Schema>;
|
||||
for (const [name, property] of Object.entries(properties)) {
|
||||
if (property['x-nullable'] !== true) continue;
|
||||
delete property['x-nullable'];
|
||||
properties[name] =
|
||||
typeof property.type === 'string' && property.type !== 'array'
|
||||
? { ...property, type: [property.type, 'null'] }
|
||||
: { oneOf: [property, { type: 'null' }] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readDescription(source: string): Promise<OpenAPI3> {
|
||||
const text = /^https?:\/\//.test(source)
|
||||
? await (await fetch(source)).text()
|
||||
: await readFile(source, 'utf8');
|
||||
return JSON.parse(text) as OpenAPI3;
|
||||
}
|
||||
|
||||
const source = process.argv[2] ?? defaultSource;
|
||||
const description = await readDescription(source);
|
||||
allowNulls(description);
|
||||
const types = astToString(await openapiTS(description));
|
||||
await writeFile(output, header + types);
|
||||
console.log(`Wrote ${output} from ${source}`);
|
||||
Reference in new issue
Block a user