Commit Graph
14 Commits
Author SHA1 Message Date
Claude 057162e45b 🔖 fix: sync package-lock, and stamp the version fallbacks from the build
Junie's review on #16 caught the lock, and it was worse than reported:
package-lock.json said 0.3.0, so it had already missed the 0.4.0 release. It
does not ship (npm excludes it from tarballs, and `files` lists only dist, src,
FRAMEWORKS.md and CHANGELOG.md) and `npm ci` never complained because it only
diffs dependencies, not the project's own version — which is exactly why it
drifted two releases without anyone noticing. Regenerated with
`npm install --package-lock-only`.

The other half was the two version strings in docs/index.html: the brand badge
and the "It is still 0.x" line. Both are no-JavaScript fallbacks — page.js
overwrites them from meta.js — so they are right in a browser and stale in a
text reader or a scraper. Bumping them by hand is what failed on 0.4.0 and
again here, and it is the "release facts hand-bumped beside their generator"
finding from the code review.

So build-docs.mjs now stamps them, next to the meta.js it already writes. The
docs-sync gate turns a forgotten bump into a CI failure instead of a review
comment. Both regexes are asserted: renaming the markup fails the build with
the pattern that stopped matching, rather than silently stamping nothing —
verified by renaming the id and watching it exit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-20 16:34:49 +00:00
Claude f56d2811cb 📦 docs: cereale is on npm, and release.yml moves to trusted publishing
0.4.0 published, so the three places that said it had not stopped being true:
the README install section, the landing page's install panel, and the "what
cereale is not" item. All three now say `npm install cereale`, and the last
becomes a limitation that is actually still true — it is 0.x, where a minor
bump is allowed to break you.

An npm version badge joins the row, tinted the same brand brown as the rest.
It was held back deliberately while the package did not exist, because a
badge that renders "not found" is worse than no badge.

page.js loses the install-shell substitution: it rewrote the tarball filename
in a code block that no longer exists.

--- release.yml: trusted publishing ---

npm exchanges the workflow's short-lived GitHub identity token for a publish
token scoped to this package, so no long-lived npm token has to exist. The
header documents exactly what to enter on npmjs.com, including the two fields
npm checks against the OIDC claims and refuses on mismatch: the workflow
filename must match this file, and Environment must stay blank while the job
declares none.

The find that matters: Node 22 bundles npm 10.x, which has no OIDC code at
all. Verified by unpacking the CLI — lib/utils/oidc.js is absent in 11.4.2 and
present in 11.5.0. Since npm's OIDC step is deliberately non-throwing, an old
CLI would have skipped trusted publishing in silence and fallen back to token
auth while appearing to work. So the workflow raises npm and then asserts the
version, rather than assuming it.

NPM_TOKEN stays as a fallback for the same non-throwing reason: this can land
before the registry side is configured, and nothing breaks. Delete the secret
once a real run shows OIDC working.

--provenance stays explicit. Under OIDC npm enables it itself for a public
repo, but only when the flag is left at its default (config.isDefault check in
oidc.js), so passing it just skips that auto-enable and lands in the same
place — while remaining the only thing that produces an attestation on the
token path.

actionlint clean; the version guard tested against 10.9.7, 11.4.2, 11.5.0 and
12.0.2; the page re-rendered with no errors and no "not on npm" text left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-20 16:10:42 +00:00
Claude 4b910f19bf 🔍 fix: apply the code review — two visible regressions, and the gates behind them
Nine review angles, fourteen verified findings, all but the byte-table
generator applied. The two that mattered most were regressions of mine:

- a:hover repainted button-styled anchors, and in dark theme --accent-text
  equals --accent-solid — hovering the hero CTA drew its label in its own
  background colour. Verified invisible before (computed color == computed
  background) and distinct after: 10.39:1 dark, 6.90:1 light. The buttons and
  the skip link now re-assert their label colours on hover.
- Footer links had lost every non-hover affordance: the text-decoration:none
  carve-out plus body-coloured links left a 2.37:1 shade difference as the
  only cue. The carve-out is deleted — .nav-links a and .brand already
  declare none themselves — and the accent rule is back on the footer.

Also on the page: #ref-filter, the one text input, moves to --border-ui (it
still had the 1.68:1 border the token's own comment calls decorative);
focusable code surfaces get the --accent-on-code ring at -2px offset, inside
the .code overflow clip; #output .out-err drops #ff8095, the last surviving
colour of the deleted indigo palette; the two rgba(255,106,126) washes become
color-mix over --err-line so a grep for the token finds them.

The theme machinery loses a whole block: the dark media query is guarded with
:not([data-theme="light"]), so an explicit light toggle falls through to the
bare :root palette and the 21-token hand-copy in [data-theme="light"] is
gone. Verified in all four system/toggle combinations.

The page stops contradicting the repo: it claimed cereale/min "cannot
tree-shake — nothing left to shake" while src/treeshake.test.ts proves the
opposite on every run. Corrected here and in FRAMEWORKS.md, with the measured
figures (1,837 vs 1,996 bytes for one decorator). The release facts the page
was hand-bumping — both tgz names, "0.4.0 lives in the repository", the
sixty-eight — now fill from meta.js/the bundle like the version badge always
has.

The workflows close three holes:

- The docs sync gate was blind to NEW untracked build outputs (git diff does
  not report them; demonstrated). Both workflows now run check:docs-sync, one
  shared script that fails on anything porcelain reports — which also ends
  the copy-paste divergence between them.
- pages.yml deploys on CI succeeding on main (workflow_run) instead of on the
  push itself, so a deploy implies green tests, not just in-sync docs. The
  deploy job refuses refs other than main, closing the workflow_dispatch
  any-branch deploy, and the build job drops pages/id-token — npm postinstall
  scripts no longer run alongside an OIDC grant.
- The Junie action is pinned to the commit behind v1.7.4 rather than the tag,
  which is the immutability the previous comment promised but a mutable ref
  cannot deliver.

Verified: every fix confirmed in a rendered browser in both themes; 72
contrast pairs still pass; no overflow at 20 widths; 268 tests, build,
check:types, check:docs, actionlint all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-17 16:23:46 +00:00
Claude c13aff9f2b 🌾 docs: give the page the identity its name implies
The library is called cereale and the page was generic dark-developer-docs
with an emoji in the brand mark. Three directions were drafted and scored by
separate judges on identity, legibility and craft; this is the disciplined
one with the two grafts that fixed its own weaknesses.

Palette. Indigo on off-white becomes rye-brown on oat paper, warm in both
themes. The accent is deliberately NOT wheat-gold: gold is both the cliche
and a collision with --warn, which already owns amber, and two amber families
would make "our brand" and "needs your attention" the same colour.

One ornament, used three times. A 2px mark repeated every 9px: as a rule
above each section head, stood on end as the stalk beside each note, and in
place of the three fake macOS traffic lights on every code panel — which were
the most generic pixels on the page. The brand emoji and the emoji favicon
both go, replaced by the same figure drawn as a five-rect SVG: an ear of grain
reduced to its skeleton, which is the section rule stood upright. An emoji is
a different picture on every OS, so it is the one brand element you do not
control.

Grafted from the two directions that lost:

- Ruled links. On a palette this warm, --accent-text and --text-muted sit
  1.14:1 apart, so colour alone loses a link in prose. Links now keep body
  colour and carry a 2px accent rule, which satisfies 1.4.1 outright.
- --accent-on-code. #editor:focus drew its ring in --accent against the
  always-dark slab: 2.86:1, a live 1.4.11 failure on the shipped page. The
  new single-valued token measures 7.03:1.
- --border-ui, for icon-only and outlined controls, which were on
  --border-strong at 1.60:1. Now 3.54:1.
- --err-line/--err-text, replacing three loose literals.

Two bugs found by rendering rather than by reading. The header overflowed the
viewport by 63px across the whole 641-767px band, hidden rather than fixed by
body{overflow-x:hidden}; the Size link added in the previous commit widened
that to 109px. The nav-link breakpoint moves 640px to 860px, which is where
they actually fit.

Verified: 72 foreground/background pairs computed in both themes, none below
4.5:1 for text or 3:1 for non-text; no horizontal overflow at any of 20
widths from 320 to 1920; no page errors and no external requests in either
theme; the playground still compiles and runs; markup parses with nothing
unclosed; check:docs passes and the generated files stay in sync.

Not taken: recolouring --t-decorator to the brand. It is the best identity
idea in the set — the decorators are what the library is — but the six --t-*
tokens are a shared vocabulary, page.js emits their class names, and the
playground highlights code the reader pastes in. One token, easy to revisit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-07 13:42:46 +00:00
Claude 1720f2274d 📄 docs: bring the landing page up to 0.4.0
The page still described 0.3.0. Two of the stale strings were in a shell
block a visitor is meant to copy — `npm pack # → cereale-0.3.0.tgz` then
`npm install ../cereale/cereale-0.3.0.tgz`, which is an ENOENT for anyone
following the install instructions today.

Corrected: five version strings (the badge fallback, the pack/install pair,
the "lives in the repository" claim, and the diagnosability paragraph, which
becomes past tense rather than 0.4.0); seven GitHub URLs using the
redirect-only capitalised org name, which the framework table added in 0.4.0
had already stopped doing; NestJS pinned to 11.1 and Bun to 1.3, matching
FRAMEWORKS.md, where every other row was already pinned; `tsc` given its 5.2
floor, which the page had never stated anywhere.

Added, all of it absent before:

- A bundle-size section with the measured table across esbuild, rollup and
  webpack, the independence of the serializer and deserializer, and why the
  regression was invisible to a single-bundler measurement.
- `cereale/min` in the install section, with the import-map form and the
  caveat that leads FRAMEWORKS.md — if you have a bundler, do not use it.
- Hero: a third shipped format, and the size fact that the 68-decorator chip
  invites.
- The verification date on the framework claim; "the three ✓ rows" made
  unambiguous now that the section has a second table.

Every figure is quoted from CHANGELOG.md or FRAMEWORKS.md rather than
recalled. check:docs passes, the generated files stay in sync, and the markup
parses with no unclosed or mismatched tags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-07 13:35:17 +00:00
Claude b4f0657d09 📦 feat: add cereale/min, and a verified framework guide
**cereale/min** — the library flattened into one minified ES module,
25.5 KB / 8.6 KB gzipped, for import maps, <script type="module">, Deno
and Workers. Built from dist/esm/index.js, so the decorator lowering and
the ES2025 target are tsc's; esbuild only flattens and minifies.

It is an addition rather than a replacement, and the measurement is the
reason. Bundled through esbuild the flat and per-module builds land within
2 bytes of each other; through rollup + terser the flat one is 165 bytes
smaller; unused decorators tree-shake out of both. With the size argument a
wash, per-module stays the default import for the one thing it does better
— readable stack traces without source maps. (My first pass at that
measurement reported "shaken" for every symbol because both rollup builds
had failed and grep was reading missing files as absence. The check now
asserts the bundle is non-empty and that a *used* symbol is present, so it
can tell a real result from a broken harness.)

**FRAMEWORKS.md** — a recipe per framework, each one run before it was
written, with the versions and date verified against.

Angular works, which was not obvious: the CLI scaffolds
experimentalDecorators: true, but ngtsc erases @Component and @Injectable
into static properties rather than leaning on TypeScript's decorator emit.
Flip the flag and both systems coexist. Verified with ngc on Angular 21.2
with strictTemplates — templates still type-check and a wrong cereale rule
is still TS1240 inside the Angular build.

Next.js cannot work inline, structurally: it derives both the SWC parser's
decorator support and the transform mode from the one flag, so on gives
legacy emit and off makes @ a syntax error. NestJS cannot either — its DI
needs design:type from emitDecoratorMetadata.

Both have the same answer: keep the cereale classes in a package compiled
by tsc and import the built output. Verified inside a program with BOTH
legacy flags on, alongside @Injectable() — mapping and validation work, and
the compile-time guarantee still holds where the rules are written.

Also verified: Bun 1.3 needs no configuration, and a real Vite 8 build with
the plugin works where the same build without it silently leaves decorator
syntax in the bundle.

Version 0.4.0: cereale/min is a new public entry point, and cutting a minor
keeps the existing v0.3.0 tag meaningful instead of force-moving it onto a
commit it was never cut from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 16:00:26 +00:00
Claude fd42675d3d 🔗 docs: link the live site, now that Pages is confirmed
Pages serves docs/ from main, so the page rebuilt in #6 is live at
avalon-vanguard.github.io/cereale. The README pointed at the local file
because the URL could not be verified from here; it now links the site and
keeps the local instructions as the fallback. package.json homepage moves
there too — npm renders it as the package's headline link, and a live
playground is a better landing spot than an anchor inside the README.

Adds canonical and Open Graph tags. No og:image: a preview card with a
broken image is worse than one without, and there is no artwork yet.

check-docs.mjs flagged the canonical link as a remote subresource, which it
is not — the browser never fetches it. Rather than exempt the URL, the
check now looks at rel and only flags the relations that actually fetch or
connect. Verified it still catches a CDN stylesheet, a preconnect and a
script src; a check that cannot tell a declaration from a request is one
that gets switched off the first time it is wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:33:55 +00:00
Claude 478f852b42 🔒 fix!: refuse names that no longer reach their property
A rename did not actually take effect. @JsonProperty stopped the old name
from being *mapped*, but not from being *accepted*: unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key fell through to the
unknown-key policy, and the default `allow` copied it onto the instance
untouched.

The value therefore landed on a declared property having skipped everything
declared for it:

  @JsonProperty('home_address') @JsonType(() => Addr) @ValidateNested()
  address!: Addr;

  toInstanceSync(Order, { address: { city: 'Paris' } })
    -> address is a plain object, instanceof Addr === false
    -> validateSync() returns []          <- nothing complains

A payload aimed at the previous version of a class was accepted in part, in
silence. Three routes led to the same hole, and all three are now closed:

- the property key of a field renamed with @JsonProperty
- the raw key of a field a naming strategy renders differently
  (`firstName` under snake_case)
- the property key of a field that is both renamed and @JsonReadOnly, which
  was still settable under its own key

Refused, not swallowed. A stale name is a mismatch with whatever produced
the payload, not a deliberate refusal like @JsonReadOnly, so it is kept in
its own map rather than lumped into `blocked`: under unknownKeys: 'error'
it is still reported, and the report now names the property it was reaching
for and what that property is called now.

  "ref" is not a JSON name for Order: property "ref" is mapped to
  "order_ref". Send that name, or add @JsonAlias("ref") to keep
  accepting this one.

@JsonAlias still keeps an old name working, and a key that some *other*
property legitimately answers to is still mapped to that property — both
asserted. The resolution happens once when the name map is built, which is
memoized per class and naming strategy, so deserialization is unchanged at
~45µs for 50 nested orders.

The behaviour this replaces was pinned by tests two commits ago, pending
this decision; those tests now assert the fix, and six more cover the
naming-strategy, read-only, alias and key-collision cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 14:42:44 +00:00
Claude 057f008ac0 🔍 fix: correct the last three findings from the page review
Of 26 findings raised across five auditors, 23 were refuted on a second
pass. These three survived.

**A renamed property's old key is still writable.** The README, the landing
page and two doc comments all said that once a property carries
@JsonProperty its original name "is no longer accepted on input". It is no
longer *mapped* — but it is not rejected either. Unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key falls through to the
unknown-key policy, and the default `allow` copies it onto the instance
untouched. Reproduced against dist:

  @JsonProperty('home_address') @JsonType(() => Addr) @ValidateNested()
  address!: Addr;

  toInstanceSync(Order, { address: { city: 'Paris' } })
    -> address is a plain object, instanceof Addr === false
    -> validateSync() returns []          <- nothing complains
    -> round-trips out as home_address    <- silently accepted

Blocking the old key would fix it, but would also swallow the
`unknownKeys: 'error'` report a strict caller gets today, which is arguably
the more useful signal. That is a judgement call the library has not made,
so this commit states the behaviour accurately everywhere it was stated
wrongly and pins it with five tests covering the default, `strip`, `error`
and the @JsonAlias fix — so it cannot drift either way while the question
is open.

**@IsNotEmpty and @IsEmpty are not complements.** `[]` and `{}` pass BOTH:
isNotEmpty checks only null/undefined/'' while isEmpty also treats empty
arrays and objects as empty. Listed one line apart as "must not be empty" /
"must be empty", they invited exactly the wrong inference.

**unknownKeys is deserialization-only**, in a group whose blurb says these
apply per call or via configure().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 10:33:45 +00:00
Claude 8c9aea3062 📐 fix: correct what the adversarial review of the page found
Five auditors read the finished page against the source; a second pass
tried to refute each finding. What survived:

**The esbuild row was wrong, and dangerously so.** It said esbuild takes
"the same settings via tsconfigRaw" as tsc. It does not: esbuild lowers
standard decorators only when its own *top-level* `target` is below
`esnext`. A `target` inside `tsconfigRaw` sets the
`useDefineForClassFields` default and nothing else. I ran it — the
decorator survives verbatim and the module throws SyntaxError on import,
which is the exact silent passthrough the section blames on oxc. The
repo's own vite plugin and toolchain test always passed `target`
top-level, so the executed matrix never backed the advice the docs gave.
Both halves are now asserted in src/toolchain.test.ts.

**"This table is executed by a test" did not cover the oxc row** — the
only ✗, and the row the whole section is built around. It cannot be:
oxc ships as a native binary with no standalone transform API, which the
test file already said in a comment. Fixed on the page and in the README.

Reference corrections, each verified against the source:
- fromRequest has no …Sync twin; the group blurb claimed every entry did
- @IsNotIn does not narrow its field, unlike its five neighbours
- @MinDate/@MaxDate take a Date as well as a thunk
- @Validate has three parameters, not two; defineRule has four
- getConfig() and resetConfig() were missing from a group rendered under
  the heading "Everything cereale exports"

And on the page itself: the vite.config.ts snippet never imported
defineConfig, so pasting it failed; and the plugin note omitted that
.tsx is excluded by default, which would drop a reader straight back
into the 0-test hole the section exists to describe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 10:12:29 +00:00
Claude c828f5cfe9 🌾 feat: rebuild the landing page, and stop it from rotting again
The old page had been quietly broken for some time. It loaded
@babel/standalone from an **unpinned** CDN URL, which rolled over to Babel
8 and dropped the `proposal-class-properties` plugin the page asked for, so
Babel.transform threw before it ever reached the decorators — and the
decorator config it passed was `{ legacy: true }`, which 0.2.0 had already
made wrong. Nothing on the page said so. The copy was still selling the
0.1.0 pitch ("Spring-like"), listed about half the decorators, showed
`npm install cereale` for a package the registry returns 404 for, and
claimed "Zero overhead" against a README that publishes the real
microsecond costs.

The rebuild is one self-contained page: hand-written CSS, no Tailwind CDN,
no CodeMirror, and a vendored compiler pinned by package.json. It loads
nothing from the network. The playground runs the real bundled library
across six examples, all verified in a headless browser. The reference
covers all 68 decorators and the full API, counted from the bundle at
runtime so it cannot drift.

The hero's compiler error is not typed into the HTML. scripts/build-docs.mjs
compiles the snippets with the real tsc and writes the verbatim diagnostics
into docs/diagnostics.js, failing the build if a snippet the page calls a
compile error ever compiles — and two snippets that must compile guard
against the harness passing vacuously.

Three guards keep it honest, all wired into CI:
- check:docs fails on any remote subresource
- build:docs + git diff fails if docs/ is stale against src/
- check:types compiles a consumer against dist/ with no DOM lib, no
  @types/node and no skipLibCheck

That last one found a real packaging defect: `fromRequest` was declared as
taking the global `Request`, so cereale's own published .d.ts raised
"Cannot find name 'Request'" in any project whose lib and types did not
happen to supply it — inside a dependency, in code they may never call, and
unfixable from the outside. It now takes a structural JsonBody, which a
Request still satisfies. The library's own type tests had been hiding it by
enabling both DOM and skipLibCheck.

An adversarial review of the finished page caught four more: the lede
claimed *every* rule is type-checked (@IsDefined and @IsNotIn deliberately
are not), the guarantee section was wrong about the mechanism (a legacy
decorator does get design:type under emitDecoratorMetadata — the real claim
is about its type signature), one sample called a Movie method on a Media[]
and did not compile, and "nested objects come back as real classes" omitted
that you have to declare them. WCAG contrast was measured rather than
eyeballed: seven real failures fixed in the two themes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 10:03:16 +00:00
Claude 83ad2a289d 📝 docs: bring README, demo and playground in line with the code; release 0.1.0
The README described a library that did not exist in places. It showed
@ValidateNested({ each: true }), which did not compile; told users to enable
emitDecoratorMetadata, which the library never reads; and documented none of the
mapping API. Its Quick Start now runs verbatim — verified by compiling and
executing it against the local source.

- README: document field-name mapping, access control, options, error helpers
  and the 30 new validators; drop the emitDecoratorMetadata instruction; add a
  Notes and Limitations section covering circular references, validate() on
  plain objects, and the fact that @JsonProperty stops the original name from
  being accepted unless you add @JsonAlias
- CHANGELOG.md: new, covering 0.1.0
- example.ts: rewritten as a tour of the current API — read-only ids, write-only
  secrets, renamed fields, conditional validation, flattened errors, and a
  base-class rule reaching a subclass
- docs: the playground hand-listed its symbol table in three parallel places and
  exposed IsEmail, which is not an export. It now derives scope from the bundle,
  so new decorators work there as soon as they ship. Bundle regenerated
- version 0.1.0

136 tests, 97% statement and 100% function coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-03 23:53:09 +00:00
Enzo Marioni fa75147e4e ✨ feat: implement core Cereale library for JSON mapping and validation
- 🎨 add Spring-like decorators (@JsonSerialize, @JsonDeserialize, etc.)
- ⚙️ implement JsonMapper and metadata storage for transformations
- 🧪 add comprehensive test suite using Vitest
- 📝 add README, CONTRIBUTING, and API documentation
- 👷 setup GitHub Actions CI workflow
- 🔧 configure TypeScript and project settings
2026-04-18 19:33:19 +02:00
Enzo Marioni 7fe7e8c7c3 ✨ feat: implement core Optimus library for JSON mapping and validation
- 🎨 add Spring-like decorators (@JsonSerialize, @JsonDeserialize, etc.)
- ⚙️ implement JsonMapper and metadata storage for transformations
- 🧪 add comprehensive test suite using Vitest
- 📝 add README, CONTRIBUTING, and API documentation
- 👷 setup GitHub Actions CI workflow
- 🔧 configure TypeScript and project settings
2026-04-11 16:58:12 +02:00