@koszewscy/sk-react-toolkit (0.2.1)
Installation
@koszewscy:registry=https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/npm install @koszewscy/sk-react-toolkit@0.2.1"@koszewscy/sk-react-toolkit": "0.2.1"About this package
sk-react-toolkit
The shared code of the web tools built as a Fastify server in TypeScript and a React application with MUI built by Vite, such as azure-platform's manage-ui and sql-query-webui. It is TypeScript source: Node 26 runs the server helpers, and the application's Vite build compiles the web code.
Using it
The package is @koszewscy/sk-react-toolkit, published to the npm registry of Gitea,
https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/. An application depends on a
published version and imports @koszewscy/sk-react-toolkit/server, /web and /vite. Its
.npmrc names the registry of the scope:
@koszewscy:registry=https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/
{ "dependencies": { "@koszewscy/sk-react-toolkit": "^0.2.0" } }
The web application adds the toolkit's resolve options and font plugin to its vite.config.ts, so
React, MUI and Emotion come from its own node_modules:
import { font, fontChoice, TOOLKIT_RESOLVE } from '@koszewscy/sk-react-toolkit/vite'
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => ({
plugins: [react(), font(fontChoice(loadEnv(mode, import.meta.dirname)))],
resolve: TOOLKIT_RESOLVE,
}))
To change the toolkit and an application together, npm link --no-save replaces the installed
package with the checkout, the TypeScript source, in each package of the application, whatever
version it names. The applications run it for both their packages as npm run dev-link:
{ "scripts": { "dev-link": "npm link --no-save --no-fund ../sk-react-toolkit && cd web && npm link --no-save --no-fund ../../sk-react-toolkit" } }
package.json and the lock files keep the published version; npm ci or npm install replaces
the link with it again, and npm run dev-link links the checkout again. A change of the toolkit
reaches the released application through a release of the toolkit and an update of the
dependency.
The peer dependencies are the application's: fastify and @fastify/static for the server
helpers; react, react-dom, @mui/material, @mui/icons-material and Emotion for the web
code; vite for the Vite helpers.
sk-react-toolkit/server
| Export | Purpose |
|---|---|
HttpError |
an error answered with its status and { error }, with problems and a detail message for a refused write |
strict(properties) |
the JSON schema of an object with exactly these properties, all required |
errorHandler(log) |
Fastify's error handler: { error }, or { error, problems, message }; unexpected 5xx errors logged with their stack |
notFoundHandler(webDir, buildHint) |
404 for /api paths, index.html for the others, 503 with the hint while the frontend is not built |
createLog, timestampsOn, QUIET, Log |
the log of a server, with optional timestamps and requests; QUIET for tests |
fail(message) |
prints the error in red and ends the process with status 2 |
gitVersion(dir, variable) |
git describe of the checkout, else the environment variable, else dev |
exitOnSignals(close) |
closes the server on Ctrl-C and SIGTERM, ending the process after 5 seconds at most |
readJsonObject(path, options) |
a JSON object file and its version, the SHA-256 of its bytes; optionally {} for a missing file |
encryptionKey(), encrypt(text, key), decrypt(encrypted, key) |
the key of the secrets and AES-256-GCM encryption; see Credential store |
CredentialStore, credentialProblems, credentialValues, registerCredentialRoutes, POSTGRESQL_KIND |
the credential store; see Credential store |
sk-react-toolkit/web
| Export | Purpose |
|---|---|
request, ApiError, Validation |
a JSON call to the backend, its error with the problems of a refused write, and the answer of a validate endpoint |
appTheme(colorSchemes?), renderApp(options) |
the theme (light, dark and system, Regular font, small text fields) and the startup that retries the first call while showing Connecting |
REGULAR, MONOSPACED, verbatim |
the font styles and the attributes of inputs the browser must not change |
Card |
a centered panel with a title and a message |
ThemeMenu |
the button cycling light, dark and system theme |
SwitcherMenu, SwitcherItem |
a button naming the item chosen, whose menu chooses another, such as a page or a connection |
TextInput |
a text field with clear and show/hide buttons |
RowDialog, useValidation |
a dialog that edits a value validated by the server as it changes, and the validation hook |
createBrowserSettings(keys, event) |
switches kept in localStorage and followed across tabs |
CredentialsButton |
the dialog of a credential store; see Credential store |
sk-react-toolkit/vite
| Export | Purpose |
|---|---|
font(choice), fontChoice(env), FONTS |
the font plugin of VITE_FONT: cdn (Google Fonts links, the default), bundled (@fontsource/roboto, a dependency of the application) or none |
TOOLKIT_RESOLVE |
the resolve options that keep one copy of React, MUI and Emotion |
Credential store
A credential store is a list of credentials in a key of the application's config.json, all of
the one kind the application gives the store, such as a database connection or a service
principal; an application with several kinds has a store, a key, for each. Each entry has a
name, unique and not one of the store's reserved names, and the fields of the store's kind:
{
"pg_connections": [
{
"name": "state",
"host": "db.example.com",
"port": 5432,
"database": "platform",
"user": "tofu",
"ssl_mode": "verify-full",
"password": "<encrypted>"
}
]
}
A kind (CredentialKind) has a label for one credential and names its fields
(CredentialField), each by its key in config.json,
with a label, a help text, a type, text, port, guid or choice with its choices, whether
it is required, and whether it is secret. POSTGRESQL_KIND is the kind of a PostgreSQL
connection: host, port, database, user, ssl_mode (disable, require or
verify-full) and a secret password.
A secret field is encrypted with AES-256-GCM, stored as base64 of a random IV, the ciphertext and
the authentication tag. The key is the SHA-256 of ENCRYPTION_KEY, else of the content of the
file ENCRYPTION_KEY_FILE names, without its final newline. Without either, no secret can be
saved or used; a secret encrypted with another key cannot be decrypted and has to be entered
again.
CredentialStore describes a store: the path of config.json, the key, the kind, the reserved
names and whether a missing config.json reads as {}.
credentialProblems(value, store): the problems of the key's value by<key>.<index>.<field>, for the application's own check ofconfig.json.credentialValues(store, name): a credential with its secrets decrypted; HttpError 404 for a name that does not exist, 400 without a key or for a secret the key does not decrypt.registerCredentialRoutes(app, store, prefix, log):GET <prefix>/read,POST <prefix>/validateandPOST <prefix>/write. Read answers the credentials,CredentialProfile, withvalues(the fields that are not secret, as text),secrets(null for each secret, never answered) andstored(which secrets are kept), the kind, the reserved names, the path ofconfig.json, its version and whether the server has a key. A write takes the version read and the credentials; a secret is null to keep the secret of the credential namedoriginal,''for none, or the new secret, which is encrypted. A file that changed since the version read is 409, credentials with problems 422; the other keys ofconfig.jsonare kept.
CredentialsButton (stores, title, icon, note, onSaved) opens the credentials of the
stores in a dialog. stores lists the prefix of each store's routes and the label of its tab; the
tabs are shown when there are several stores, which share one config.json. Each tab has a list at
the left with Add and Remove, and the fields of the credential selected, as the store's kind names
them; a tab with a problem has a red label. A secret shows only whether it is kept: Set or Change
replaces it and Remove drops it. Save writes each store that changed, each write with the version
the previous one answers.
Development
npm install installs the peer dependencies as development dependencies; npm run typecheck
and npm run lint check the code. npm run build (build.mjs) builds the published package in
dist/: the code compiled to JavaScript with its declarations, README.md, CHANGELOG.md and a
package.json whose exports name the compiled files.
Releases
./release.mjs commits, tags the version of the newest CHANGELOG.md entry and pushes main with
the tag; -n prints the git commands instead. The version of a release of the code, x.y.z, is
that of package.json, so a release raises both. A tag x.y.z-n releases changes that are not
code changes; its version is already published, so its run publishes nothing.
The Release workflow, .gitea/workflows/release.yml, builds and publishes the package on Gitea
Actions for each tag and on demand: it installs the dependencies, typechecks and lints the code,
builds the package and publishes dist/ to the npm registry of the owner CI_OWNER on the server
CI_URL, with CI_TOKEN, a token with the package scope; all three are secrets of the
repository. A version already published is skipped, as the registry refuses the same version
again.
Dependencies
Peer Dependencies
| ID | Version |
|---|---|
| @emotion/react | ^11.14.0 |
| @emotion/styled | ^11.14.1 |
| @fastify/static | ^10.1.7 |
| @mui/icons-material | ^9.5.0 |
| @mui/material | ^9.5.0 |
| fastify | ^5.12.7 |
| react | ^19.3.1 |
| react-dom | ^19.3.1 |
| vite | ^8.3.5 |