Shared frontend service packages monorepo under the @script-development npm scope.
- Language: TypeScript ^6.0 (strict mode,
verbatimModuleSyntax) - Build: tsdown (Rolldown/oxc) — dual ESM + CJS output
- Test: vitest 4 (100% coverage threshold) + Stryker (90% mutation threshold)
- Lint: oxlint (explicit config at
.oxlintrc.json) - Format: oxfmt
- Package lint: publint + attw (Are The Types Wrong) —
lint:pkgenforces fail-on-any-advisory viascripts/lint-pkg.mjs(suggestions, warnings, and errors all treat as fatal — publint CLI default and--strictboth exit 0 on suggestions). Motivated by enforcement queue #33 + the PR #35git+prefix regression that silently drifted across 10 packages because the unenforced gate only printed the suggestion. The same wrapper also assertsengines.nodepresence across the root manifest + all workspace packages — closes enforcement queue #31 (drift-prevention gate, deployed 2026-05-12; declarations themselves landed 2026-04-22 via commit0605d99). Presence-only check; the value (>=24.0.0today) is not validated — value alignment is a separate doctrine question tracked alongside the CInode-version. - Publish: OIDC Trusted Publishing to public npm registry (no stored tokens)
- CI: 8-gate pipeline: audit → format → lint → build → typecheck → lint:pkg → coverage → mutation
fs-http is the war-room reference implementation of Doctrine #8 (Architectural Principle #8, library-author extension — see war-room CLAUDE.md ## Architectural Principles §8, 2026-04-22):
Library-author extension (2026-04-22) — Shared HTTP factory packages (e.g.,
@script-development/fs-http) must expose a compliant timeout surface: a default, a required option, or a documented contract plus consumer-level enforcement. Inheriting framework defaults at the library layer silently propagates the violation to every consumer territory.
fs-http exposes a 5-axis timeout surface:
- Default:
30000ms applied if no override is provided. - Service-wide option:
createHttpService(baseURL, { timeout: number }). - Per-request override: standard axios
timeoutconfig on individual calls. - Opt-out:
timeout: 0disables the timeout (use sparingly). - Constant export:
DEFAULT_TIMEOUT_MSis barrel-exported for consumers that need to reference the default explicitly.
Consumer territories must apply per-call timeouts at instantiation OR rely on the 30000 ms default. See docs/packages/http.md#timeout for usage.
| Package | Vue | Description |
|---|---|---|
| fs-http | No | HTTP service factory with middleware architecture |
| fs-storage | No | localStorage service factory with prefix namespacing |
| fs-helpers | No | Tree-shakeable utilities: deep copy, type guards, case conversion |
| fs-theme | Yes | Reactive dark/light mode with storage persistence |
| fs-loading | Yes | Loading state service with HTTP middleware |
| fs-adapter-store | Yes | Reactive adapter-store pattern with CRUD resource adapters |
| fs-cached-adapter-store | Yes | Hash-bumping cache wrapper around fs-adapter-store; middleware-driven invalidation with prime() bootstrap; no retrieveAll/retrieveById on the public surface |
| fs-toast | Yes | Component-agnostic toast queue (FIFO) |
| fs-dialog | Yes | Component-agnostic dialog stack (LIFO) with error middleware |
| fs-translation | Yes | Type-safe reactive i18n with dot-notation keys |
| fs-router | Yes | Type-safe router service factory with CRUD navigation, middleware pipeline, and custom components for Vue Router |
- Factory pattern: All packages export
createXxxService()factory functions returning plain service objects. No classes, no singletons. - Single entry point: Each package has
src/index.tsas the sole barrel export. Named exports only, no defaults. - Peer dependencies: Vue-dependent packages declare
vueas a peer dep. Inter-package dependencies are peers too. - Loose coupling: Prefer structural typing (duck types) over direct package imports where possible.
fs-theme'sThemeStorageContractis the exemplar. - Test environment: Browser-dependent tests use
// @vitest-environment happy-domfile-level comments. - Identical build config: All packages share the same
tsdown.config.tsstructure. - No direct axios imports in dependent packages. Route
AxiosResponse/AxiosRequestConfig/ sibling types throughfs-http's re-exports (e.g.Parameters<ResponseMiddlewareFunc>[0]for response types). Directimport type {AxiosResponse} from 'axios'breaks rolldown'sd.ctsemission on dual-bundle packages — caught duringfs-cached-adapter-storescaffold 2026-05-13. - Transport-surface discipline. Every
fs-httptransport method must inherit option-honoring from theaxios.create()instance. Adding a new transport path that uses nativefetch(or any non-axios transport) requires a deliberate audit against the fullHttpServiceOptionsmatrix —headers,withCredentials,withXSRFToken,smartCredentials,timeout, plus the per-callAxiosRequestConfigoverride surface. The Library-Config-Honor Surface Audit (Sapper M3 + Surveyor M3, 2026-05-15) is the standing checklist. The pre-1.0streamRequestfunction violated this rule on four axes (queue #22 streamRequest portion + queue #64 XSRF + Surveyor M3 F-1 headers + F-2 timeout) and was removed in 0.4.0 with zero realized consumer impact. If a future streaming use case emerges, the right design is either axios'sresponseType: 'stream'mode via the standard methods (inherits all options for free) or a deliberatecreateStreamHttpServicefactory designed against the option-honoring matrix from the start — not a re-add of an axios-bypassing transport.
Two packages share an internal direct-dep on string-ts: fs-helpers (deepCamelKeys, deepSnakeKeys, DeepSnakeKeys type) and fs-translation (replaceAll). Symbols are disjoint, npm dedupes the dep in consumer node_modules when ranges align, and tsdown externalizes string-ts in both bundles — consumers using both packages do not ship duplicate copies.
Discipline: when bumping string-ts in either package, bump it in the other in the same PR. Range drift across the two consumers (e.g. one on ^2.x, the other on ^3.x) breaks consumer dedupe.
While packages remain pre-1.0, npm caret semantics treat every minor bump as breaking (^0.1.0 matches only 0.1.x). Each fs-http minor bump cascades into peer-range widenings on fs-loading, fs-adapter-store, and fs-cached-adapter-store. The cascade is mechanical, not avoidable on npm.
Per-bump checklist:
- Grep all
packages/*/package.jsonfor the bumped package's name. - For every match in
dependencies/devDependencies/peerDependencies, widen the range (e.g.^0.1.0→^0.1.0 || ^0.2.0). - Patch-bump the affected sibling packages — the peer-range widening is observable in published metadata and deserves its own version.
- Regenerate
package-lock.jsonand verify everynode_modules/@script-development/*resolves to the workspace ("resolved": "packages/*","link": true). No nested registry copies anywhere in the lock. - CI passing
npm ciis necessary but not sufficient — inspect the lock for nested copies after every cross-minor bump.
Cascade peers as of 2026-05-13:
- An
fs-httpminor bump cascades to:fs-loading,fs-adapter-store,fs-cached-adapter-store. - An
fs-adapter-storeminor bump cascades to:fs-cached-adapter-store. - An
fs-storageminor bump cascades to:fs-adapter-store,fs-cached-adapter-store.
This tax disappears once packages reach 1.0. The workspace:* protocol is not an option on npm (npm 11+ rejects it as EUNSUPPORTEDPROTOCOL); it is a pnpm/yarn feature.
| Command | Purpose |
|---|---|
npm run build |
Build all packages (tsdown) |
npm run typecheck |
Type-check all packages (requires build first) |
npm test |
Run all tests |
npm run test:coverage |
Run tests with coverage (100% threshold) |
npm run test:mutation |
Run Stryker mutation testing (90% threshold) |
npm run lint |
Lint with oxlint |
npm run format:check |
Check formatting with oxfmt |
npm run format |
Fix formatting with oxfmt |
npm run lint:pkg |
Run publint + attw on all packages |
npm audit |
Check for dependency vulnerabilities |
Build before typecheck. Cross-package type resolution requires built .d.mts files. The CI pipeline enforces this order.
Lint configuration lives at .oxlintrc.json (repo-root, no per-package overrides). The explicit config declares three defaults so rule additions/removals land as a deliberate diff rather than silent upstream drift when oxlint bumps:
- Plugins:
typescript,unicorn,oxc— the three plugins enabled by oxlint's own defaults. - Categories:
correctness: "error"— all 107 Correctness rules fail CI (waswarn, so violations were silently tolerated pre-config). perf,suspicious,pedantic,style,restriction,nursery: unset — library posture is Correctness-only, opt-in per-rule for anything else.
To add a rule, set it in the rules object (e.g. "perf/no-accumulating-spread": "error"). To disable a default, set it to "off". To opt into a whole category, add it to categories (be deliberate — pedantic has false positives, nursery is unstable). See npx oxlint --rules for the full catalog with default-on/off markers.
- Create
packages/{name}/withpackage.json,tsconfig.json,tsdown.config.ts,vitest.config.ts - Name it
@script-development/fs-{name} - Use
defineProjectfromvitest/configin the vitest config - Add 100% coverage threshold and 90% mutation threshold
- Bump version in the new package's
package.json(manual — no changeset.mdfiles)
Distilled operational rules from cross-project Architecture Decision Records. Canonical source: adrs.script.nl. This section is maintained by the War Room — do not edit directly. Last synced: 2026-04-17
- Published here as
fs-adapter-store. This territory is the canonical home of the pattern. - Preserve the reactive adapter-store contract:
createAdapterStoreModule()factory returning a module withresourceAdapterfor CRUD plus typedAdapted<T>/NewAdapted<T>records. - Changes to the pattern's surface (function signatures, exported types) are breaking for every consumer — treat them as major version decisions and coordinate with consumer territories (kendo, BIO).
- War Room ADRs are canonical at
adrs.script.nl. Projections (this section) are distilled into territory CLAUDE.md by the War Room. - Do not amend projections in this file directly. Propose amendments through the war room; the update propagates here.
- fs-packages is a full territory under the war room (not exempt like BIO).
- Kendo, BIO, and Entreezuil mock only
@script-development/fs-httpwhen running page integration tests. fs-http is the mock target; its public API (createHttpService, middleware hooks,isAxiosError) is the contract consumers depend on. - Do not introduce breaking changes to fs-http's public API without coordinating with consumer territories' mock-server infrastructure.
The following cross-project ADRs do not apply to fs-packages because it has no Laravel/PHP backend, no HTTP API surface, no database, and no app-UI:
- ADR-0001 Audit Logging — Laravel/DB-backed; N/A.
- ADR-0002 Cascade Deletion & Selective Soft Deletes — Laravel/DB-backed; N/A.
- ADR-0009 Unified ResourceData Pattern — Laravel JSON response shape; N/A.
- ADR-0011 Action Class Architecture — PHP
final readonlyaction classes; N/A. - ADR-0012 FormRequest → DTO Flow — Laravel request pipeline; N/A.
- ADR-0014 Domain-Driven Frontend Structure — App-level vertical slices by business domain; fs-packages is horizontal library infrastructure, not an app. N/A.
- ADR-0016 Config Attribute Injection — PHP
#[Config]attribute; N/A. - ADR-0019 Explicit Model Hydration — Eloquent model hydration; N/A.
Kendo-only or territory-scoped ADRs (0003, 0004, 0006, 0008, 0018) do not apply cross-territory.
ADR-0005 (Spy System), ADR-0007 (Soldiers + Briefings), ADR-0010 (Squad System) govern war room operations, not territory code. No projection required.