@fohte/storybook-addon 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,85 @@
1
1
  # @fohte/storybook-addon
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ @fohte's personal Storybook addons.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ Runs three checks right after each story renders, and fails the story's test when one trips:
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ - **overflow-check** flags elements whose content is clipped by their container (scrollWidth > clientWidth), which usually means a silent layout bug.
8
+ - **external-resource-check** — flags stories that load a non-same-origin resource (font, image, stylesheet), which makes VRT captures non-deterministic.
9
+ - **unhandled-api-request-check** — flags stories that hit an API endpoint with no MSW handler, which would otherwise render with MSW's error response instead of failing.
8
10
 
9
- ## Purpose
11
+ It also injects CSS that hides the text-input caret and collapses all animations/transitions to their end state, keeping screenshots deterministic.
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@fohte/storybook-addon`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ## Install
15
14
 
16
- ## What is OIDC Trusted Publishing?
15
+ ```bash
16
+ pnpm add -D @fohte/storybook-addon
17
17
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
18
+ # Peer dependencies
19
+ pnpm add -D storybook vitest
20
+ ```
19
21
 
20
- ## Setup Instructions
22
+ ## Usage
21
23
 
22
- To properly configure OIDC trusted publishing for this package:
24
+ Add the addon to `.storybook/main.ts`:
23
25
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
26
+ ```ts
27
+ import type { StorybookConfig } from '@storybook/react-vite'
28
28
 
29
- ## DO NOT USE THIS PACKAGE
29
+ const config: StorybookConfig = {
30
+ addons: ['@fohte/storybook-addon'],
31
+ }
30
32
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
33
+ export default config
34
+ ```
36
35
 
37
- ## More Information
36
+ This wires up `overflow-check` and `external-resource-check` — no further setup needed.
38
37
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
38
+ ### unhandled-api-request-check
42
39
 
43
- ---
40
+ This check needs to know which paths count as "API requests" (that's app-specific), and needs to be fed unhandled requests from your MSW setup. Configure it once and call `reportUnhandledApiRequest()` from `onUnhandledRequest` in `.storybook/preview.ts`:
44
41
 
45
- **Maintained for OIDC setup purposes only**
42
+ ```ts
43
+ import {
44
+ configureUnhandledApiRequestCheck,
45
+ reportUnhandledApiRequest,
46
+ } from '@fohte/storybook-addon/preview'
47
+ import { initialize } from 'msw-storybook-addon'
48
+
49
+ configureUnhandledApiRequestCheck({ pathPrefixes: ['/api/'] })
50
+
51
+ initialize({
52
+ onUnhandledRequest: ({ url }, print) => {
53
+ if (reportUnhandledApiRequest(url)) {
54
+ print.error()
55
+ }
56
+ },
57
+ })
58
+ ```
59
+
60
+ ## Addon `afterEach` ordering
61
+
62
+ Storybook runs multiple addons' `afterEach` hooks serially, in the **reverse** of the order they're listed in `main.ts`'s `addons` array — the addon listed last runs its `afterEach` first. The consuming app's own `preview.ts`/`preview.tsx` `afterEach` runs before every addon's `afterEach`. If one hook throws, the remaining ones are skipped (fail-fast).
63
+
64
+ This matters once a screenshot-capturing addon is added: to keep "capture first, then let this package's checks fail the test" working, list that addon **after** `@fohte/storybook-addon` in the `addons` array.
65
+
66
+ ## Development
67
+
68
+ ### Setup
69
+
70
+ ```bash
71
+ pnpm install
72
+ pnpm test
73
+ ```
74
+
75
+ ### Scripts
76
+
77
+ - `pnpm test` - Run type checking and unit tests
78
+ - `pnpm test:type` - Type-check without emitting
79
+ - `pnpm test:unit` - Run unit tests (vitest)
80
+ - `pnpm build` - Compile TypeScript to `lib/`
81
+ - `pnpm lint` - Run ESLint
82
+
83
+ ### Release process
84
+
85
+ This project uses [release-please](https://github.com/googleapis/release-please). Merging a PR whose title follows [Conventional Commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, `feat!:`/`fix!:` for breaking changes) into `main` updates a Release PR; merging that Release PR tags a GitHub release and publishes to npm.
@@ -0,0 +1,6 @@
1
+ export type StorybookCheck = {
2
+ reset: () => void;
3
+ assert: (storyParameters?: unknown) => void;
4
+ };
5
+ export declare function throwIfNotEmpty(urls: string[], message: string): void;
6
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { assert } from 'vitest';
2
+ export function throwIfNotEmpty(urls, message) {
3
+ if (urls.length === 0)
4
+ return;
5
+ const list = urls.join('\n');
6
+ urls.length = 0;
7
+ assert.fail(`${message}:\n${list}`);
8
+ }
9
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1,3 @@
1
+ import type { StorybookCheck } from '#checks/check.js';
2
+ export declare const externalResourceCheck: StorybookCheck;
3
+ //# sourceMappingURL=external-resource-check.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { throwIfNotEmpty } from '#checks/check.js';
2
+ // A story that loads a non-same-origin http(s) resource (e.g. a remote
3
+ // avatar image) races the capture against that request's completion over the
4
+ // real network, so the same story can rasterize differently between runs.
5
+ // Failing the test surfaces this instead of letting it show up as unstable
6
+ // screenshot diffs; fix stories by inlining the resource as a data URI.
7
+ const externalResourceUrls = [];
8
+ // Entries older than this are from a previous story and must not be
9
+ // attributed to the one currently rendering.
10
+ let resetAt = 0;
11
+ function isExternalResourceUrl(url) {
12
+ const { protocol, origin } = new URL(url);
13
+ return ((protocol === 'http:' || protocol === 'https:') &&
14
+ origin !== window.location.origin);
15
+ }
16
+ new PerformanceObserver((list) => {
17
+ for (const entry of list.getEntries()) {
18
+ if (entry.startTime >= resetAt && isExternalResourceUrl(entry.name)) {
19
+ externalResourceUrls.push(entry.name);
20
+ }
21
+ }
22
+ }).observe({ type: 'resource', buffered: true });
23
+ export const externalResourceCheck = {
24
+ reset: () => {
25
+ resetAt = performance.now();
26
+ externalResourceUrls.length = 0;
27
+ },
28
+ assert: () => {
29
+ throwIfNotEmpty(externalResourceUrls, 'Story loaded non-same-origin resource(s), which makes VRT captures flaky');
30
+ },
31
+ };
32
+ //# sourceMappingURL=external-resource-check.js.map
@@ -0,0 +1,3 @@
1
+ import type { StorybookCheck } from '#checks/check.js';
2
+ export declare const overflowCheck: StorybookCheck;
3
+ //# sourceMappingURL=overflow-check.d.ts.map
@@ -0,0 +1,162 @@
1
+ import { throwIfNotEmpty } from '#checks/check.js';
2
+ // The consuming app's preview.ts calls reset()/assert() around each story
3
+ // render (see preview.ts's beforeEach/afterEach). The story's container is a
4
+ // bare <div> appended to document.body before anything is mounted into it —
5
+ // reset() never gets `context.canvasElement` itself, so this is the only way
6
+ // to reach the same element. Portal-based components (dialogs, popovers,
7
+ // selects, etc.) append their own <div>s directly to document.body too, but
8
+ // only *after* the story has mounted — so the container is reliably the
9
+ // *first* element appended to body once a story starts, not the last. A
10
+ // MutationObserver set up in reset() (which runs before the story mounts)
11
+ // captures it.
12
+ let storyRoot = null;
13
+ let bodyObserver = null;
14
+ function watchStoryRoot() {
15
+ storyRoot = null;
16
+ bodyObserver?.disconnect();
17
+ bodyObserver = new MutationObserver((mutations) => {
18
+ for (const mutation of mutations) {
19
+ for (const node of mutation.addedNodes) {
20
+ if (node instanceof Element) {
21
+ storyRoot = node;
22
+ bodyObserver?.disconnect();
23
+ return;
24
+ }
25
+ }
26
+ }
27
+ });
28
+ bodyObserver.observe(document.body, { childList: true });
29
+ }
30
+ function describeElement(el) {
31
+ const id = el.id ? `#${el.id}` : '';
32
+ const classAttr = el.getAttribute('class');
33
+ const classes = classAttr !== null && classAttr !== ''
34
+ ? `.${classAttr.trim().split(/\s+/).join('.')}`
35
+ : '';
36
+ return `${el.tagName.toLowerCase()}${id}${classes}`;
37
+ }
38
+ // `text-overflow: ellipsis` is excluded because it's a deliberate "this is
39
+ // cut off, here's more" affordance (e.g. Tailwind's `truncate`), not a
40
+ // silent, undiscoverable clip — exactly the class of intentional truncation
41
+ // this check isn't meant to flag.
42
+ //
43
+ // `overflow-x: visible` elements are NOT excluded, even though such an
44
+ // element doesn't clip its own content: in a real app, some clipping
45
+ // ancestor above it usually reports the same overflow independently, but a
46
+ // story has no such ancestor above its scan root — an `overflow-x: visible`
47
+ // element may be the only place a story's overflow is ever caught.
48
+ function isIntentionalTruncation(cs) {
49
+ return cs.textOverflow === 'ellipsis';
50
+ }
51
+ // The standard visually-hidden a11y technique (e.g. Tailwind's `sr-only`)
52
+ // shrinks an element to a 1x1px box and clips it on purpose, to keep it
53
+ // readable by screen readers while invisible to sighted users. A 1x1px box
54
+ // can never show meaningfully clipped content to a sighted user either way,
55
+ // so this is a safe, general skip rather than a per-story exclusion.
56
+ function isVisuallyHidden(el) {
57
+ return el.clientWidth <= 1 && el.clientHeight <= 1;
58
+ }
59
+ // A single overflowing element is picked up again by every clipping
60
+ // ancestor above it, up to the scan root (per
61
+ // https://www.w3.org/TR/cssom-view-1/#scrolling-area, unclipped overflow
62
+ // keeps bubbling upward), so one bug would otherwise read as N separate
63
+ // findings. Group ancestor-descendant reports into the topmost ancestor's
64
+ // group instead of dropping any of them — the chain itself is a hint for
65
+ // where the fix belongs, since the root cause (e.g. a parent flex layout)
66
+ // isn't always the innermost element. `root.querySelectorAll` yields
67
+ // elements in document order, so an element's ancestors are always seen
68
+ // (and can become its group) before it is.
69
+ function groupByAncestor(entries) {
70
+ const groups = [];
71
+ for (const { el, description } of entries) {
72
+ const group = groups.find((g) => g.root.contains(el));
73
+ if (group) {
74
+ group.descriptions.push(description);
75
+ }
76
+ else {
77
+ groups.push({ root: el, descriptions: [description] });
78
+ }
79
+ }
80
+ return groups.map((g) => g.descriptions.length === 1
81
+ ? g.descriptions.join('')
82
+ : `${String(g.descriptions.length)} chained overflows (same root cause, outermost first):\n ${g.descriptions.join('\n ')}`);
83
+ }
84
+ // el.scrollWidth > el.clientWidth means the element's content doesn't fit
85
+ // inside its own padding box (clientWidth) — i.e. some of it is clipped and
86
+ // invisible.
87
+ //
88
+ // This only measures overflow on the inline-end (right, in LTR) side: the
89
+ // CSSOM "scrolling area" a scrollable box exposes never extends past its
90
+ // inline-start edge, so content overflowing to the *left* is invisible to
91
+ // this check (https://www.w3.org/TR/cssom-view-1/#scrolling-area).
92
+ // `position: fixed` elements are invisible to it too — they escape the
93
+ // containing block chain, so an off-screen fixed element never shows up in
94
+ // any ancestor's scrollWidth.
95
+ function findOverflows(root, ignoreSelectors) {
96
+ const entries = [];
97
+ for (const el of root.querySelectorAll('*')) {
98
+ if (el.scrollWidth <= el.clientWidth)
99
+ continue;
100
+ if (isVisuallyHidden(el))
101
+ continue;
102
+ if (ignoreSelectors.some((selector) => el.matches(selector)))
103
+ continue;
104
+ if (isIntentionalTruncation(getComputedStyle(el)))
105
+ continue;
106
+ const overflowPx = el.scrollWidth - el.clientWidth;
107
+ entries.push({
108
+ el,
109
+ description: `${describeElement(el)}: scrollWidth=${String(el.scrollWidth)} > clientWidth=${String(el.clientWidth)} (+${String(overflowPx)}px)`,
110
+ });
111
+ }
112
+ return groupByAncestor(entries);
113
+ }
114
+ function overflowCheckParameters(storyParameters) {
115
+ if (typeof storyParameters !== 'object' || storyParameters === null) {
116
+ return undefined;
117
+ }
118
+ if (!('overflowCheck' in storyParameters))
119
+ return undefined;
120
+ const { overflowCheck } = storyParameters;
121
+ if (typeof overflowCheck !== 'object' || overflowCheck === null) {
122
+ return undefined;
123
+ }
124
+ return overflowCheck;
125
+ }
126
+ function isDisabled(overflowCheck) {
127
+ return (overflowCheck !== undefined &&
128
+ 'disable' in overflowCheck &&
129
+ overflowCheck.disable === true);
130
+ }
131
+ // Narrower than `overflowCheck.disable`: exempt one intentionally-overflowing
132
+ // element (e.g. a chip row using `overflow-x-auto`, or a fixed-scrollbar
133
+ // sizing artifact) by CSS selector via the story's own parameters, matching
134
+ // only the element itself — not its descendants — so the rest of the story
135
+ // (including anything nested inside the matched element) still gets checked.
136
+ function ignoreSelectorsOf(overflowCheck) {
137
+ if (overflowCheck === undefined)
138
+ return [];
139
+ if (!('ignoreSelectors' in overflowCheck))
140
+ return [];
141
+ const { ignoreSelectors } = overflowCheck;
142
+ if (!Array.isArray(ignoreSelectors))
143
+ return [];
144
+ return ignoreSelectors.filter((s) => typeof s === 'string');
145
+ }
146
+ export const overflowCheck = {
147
+ reset: watchStoryRoot,
148
+ assert: (storyParameters) => {
149
+ const params = overflowCheckParameters(storyParameters);
150
+ if (isDisabled(params))
151
+ return;
152
+ if (storyRoot == null) {
153
+ // The MutationObserver in watchStoryRoot() hasn't seen an element
154
+ // appended to body yet (e.g. the story renders nothing). Skip rather
155
+ // than fail, since there is no root to scan.
156
+ console.warn('[overflow-check] story root not detected; skipping overflow scan');
157
+ return;
158
+ }
159
+ throwIfNotEmpty(findOverflows(storyRoot, ignoreSelectorsOf(params)), 'Story has element(s) overflowing their container (clipped and invisible)');
160
+ },
161
+ };
162
+ //# sourceMappingURL=overflow-check.js.map
@@ -0,0 +1,7 @@
1
+ import type { StorybookCheck } from '#checks/check.js';
2
+ export declare function configureUnhandledApiRequestCheck(options: {
3
+ pathPrefixes: string[];
4
+ }): void;
5
+ export declare function reportUnhandledApiRequest(url: string): boolean;
6
+ export declare const unhandledApiRequestCheck: StorybookCheck;
7
+ //# sourceMappingURL=unhandled-api-request-check.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { throwIfNotEmpty } from '#checks/check.js';
2
+ // Populated by reportUnhandledApiRequest(), which the consuming app's MSW
3
+ // `onUnhandledRequest` callback calls — a story hitting an API endpoint with
4
+ // no MSW handler gets MSW's error response instead of real data, so the
5
+ // screenshot captures a broken UI state without failing otherwise.
6
+ const unhandledApiRequestUrls = [];
7
+ // Which paths count as "API requests" is app-specific (tq uses `/api/`, a
8
+ // future app might use a different prefix or none), so it's configured once
9
+ // by the consuming app rather than hardcoded here.
10
+ let pathPrefixes = [];
11
+ export function configureUnhandledApiRequestCheck(options) {
12
+ pathPrefixes = options.pathPrefixes;
13
+ }
14
+ // Returns whether the request was recorded, so the caller can decide whether
15
+ // to also print MSW's own error for it.
16
+ export function reportUnhandledApiRequest(url) {
17
+ const parsed = new URL(url);
18
+ if (parsed.origin !== window.location.origin)
19
+ return false;
20
+ if (!pathPrefixes.some((prefix) => parsed.pathname.startsWith(prefix))) {
21
+ return false;
22
+ }
23
+ unhandledApiRequestUrls.push(parsed.pathname);
24
+ return true;
25
+ }
26
+ export const unhandledApiRequestCheck = {
27
+ reset: () => {
28
+ unhandledApiRequestUrls.length = 0;
29
+ },
30
+ assert: () => {
31
+ throwIfNotEmpty(unhandledApiRequestUrls, 'Story made unhandled API request(s); add an MSW handler for');
32
+ },
33
+ };
34
+ //# sourceMappingURL=unhandled-api-request-check.js.map
@@ -0,0 +1,4 @@
1
+ export declare abstract class BoundaryError extends Error {
2
+ constructor(message: string, cause: unknown);
3
+ }
4
+ //# sourceMappingURL=errors.d.ts.map
package/lib/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ // Wrap a caught external error before re-throwing, so it carries a
2
+ // domain-meaningful message while preserving the original via `cause`.
3
+ // Subclass per interop boundary — `name` is derived automatically, so no
4
+ // constructor override is needed. The try and the throw this requires each
5
+ // need their own eslint-disable-next-line comment, since @fohte/eslint-config's
6
+ // errorHandling bans ThrowStatement and TryStatement as separate selectors:
7
+ //
8
+ // export class TaskStorePersistenceError extends BoundaryError {}
9
+ //
10
+ // // eslint-disable-next-line no-restricted-syntax -- interop boundary
11
+ // try {
12
+ // ...
13
+ // } catch (caughtErr) {
14
+ // const wrapped = new TaskStorePersistenceError('failed to save', caughtErr)
15
+ // // eslint-disable-next-line no-restricted-syntax -- interop boundary
16
+ // throw wrapped
17
+ // }
18
+ //
19
+ // To report a wrapped error under a stable fingerprint (for Sentry
20
+ // grouping) without changing control flow, call `captureWithFingerprint`
21
+ // (from `@fohte/service-kit/observability`, see src/bootstrap.ts — only
22
+ // generated when `error_tracking` or `is_web_app` is enabled) right before
23
+ // re-throwing.
24
+ export class BoundaryError extends Error {
25
+ constructor(message, cause) {
26
+ super(message, { cause });
27
+ this.name = new.target.name;
28
+ }
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,6 @@
1
+ import type { AfterEach, BeforeEach, Parameters, WebRenderer } from 'storybook/internal/types';
2
+ export { configureUnhandledApiRequestCheck, reportUnhandledApiRequest, } from '#checks/unhandled-api-request-check.js';
3
+ export declare const parameters: Parameters;
4
+ export declare const beforeEach: BeforeEach<WebRenderer>;
5
+ export declare const afterEach: AfterEach<WebRenderer>;
6
+ //# sourceMappingURL=preview.d.ts.map
package/lib/preview.js ADDED
@@ -0,0 +1,43 @@
1
+ import { externalResourceCheck } from '#checks/external-resource-check.js';
2
+ import { overflowCheck } from '#checks/overflow-check.js';
3
+ import { unhandledApiRequestCheck } from '#checks/unhandled-api-request-check.js';
4
+ export { configureUnhandledApiRequestCheck, reportUnhandledApiRequest, } from '#checks/unhandled-api-request-check.js';
5
+ function injectStyle(css) {
6
+ const style = document.createElement('style');
7
+ style.textContent = css;
8
+ document.head.appendChild(style);
9
+ }
10
+ // The native text-input caret blinks on an OS timer, so a captured frame of a
11
+ // focused input/contenteditable is on or off at random — same content,
12
+ // different pixels between runs. Hiding it keeps captures deterministic
13
+ // without touching application code.
14
+ injectStyle('input, textarea, [contenteditable] { caret-color: transparent !important; }');
15
+ // CSS animations/transitions (popup open/close fades, zooms, spinners, ...)
16
+ // capture at whatever frame happens to be on screen when the screenshot
17
+ // fires, so the same story rasterizes differently between runs even though
18
+ // nothing about it actually changed. Forcing zero duration collapses every
19
+ // animation/transition to its end state instantly, keeping captures
20
+ // deterministic without touching application code.
21
+ injectStyle(`
22
+ *, *::before, *::after {
23
+ animation-duration: 0s !important;
24
+ animation-delay: 0s !important;
25
+ transition-duration: 0s !important;
26
+ transition-delay: 0s !important;
27
+ }
28
+ `);
29
+ const checks = [
30
+ externalResourceCheck,
31
+ unhandledApiRequestCheck,
32
+ overflowCheck,
33
+ ];
34
+ export const parameters = {};
35
+ export const beforeEach = () => {
36
+ for (const check of checks)
37
+ check.reset();
38
+ };
39
+ export const afterEach = (context) => {
40
+ for (const check of checks)
41
+ check.assert(context.parameters);
42
+ };
43
+ //# sourceMappingURL=preview.js.map
package/package.json CHANGED
@@ -1,10 +1,67 @@
1
1
  {
2
2
  "name": "@fohte/storybook-addon",
3
- "version": "0.0.0",
4
- "description": "OIDC trusted publishing setup package for @fohte/storybook-addon",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
3
+ "description": "@fohte's personal Storybook addons",
4
+ "license": "MIT",
5
+ "author": "Hayato Kawai <fohte.hk@gmail.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/fohte/storybook-addon.git"
9
+ },
10
+ "type": "module",
11
+ "exports": {
12
+ "./preview": {
13
+ "types": "./lib/preview.d.ts",
14
+ "default": "./lib/preview.js"
15
+ }
16
+ },
17
+ "imports": {
18
+ "#*.js": "./lib/*.js"
19
+ },
20
+ "files": [
21
+ "lib",
22
+ "!lib/**/*.map",
23
+ "!lib/**/*.test.*"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "peerDependencies": {
30
+ "storybook": "^10.0.0",
31
+ "vitest": "^4.0.0"
32
+ },
33
+ "dependencies": {
34
+ "neverthrow": "8.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@commitlint/cli": "21.2.1",
38
+ "@eslint/eslintrc": "3.3.6",
39
+ "@fohte/eslint-config": "0.4.0",
40
+ "@ninoseki/eslint-plugin-neverthrow": "0.2.0",
41
+ "@tsconfig/node-lts": "24.0.0",
42
+ "@tsconfig/strictest": "2.0.8",
43
+ "@types/jsdom": "30.0.0",
44
+ "@types/node": "24.13.3",
45
+ "concurrently": "10.0.4",
46
+ "eslint": "10.6.0",
47
+ "jsdom": "30.0.1",
48
+ "prettier": "3.9.6",
49
+ "rimraf": "6.1.3",
50
+ "storybook": "10.5.7",
51
+ "typescript": "6.0.3",
52
+ "vitest": "4.1.10"
53
+ },
54
+ "version": "0.1.0",
55
+ "scripts": {
56
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
57
+ "prebuild": "pnpm run clean",
58
+ "build": "tsc --project tsconfig.build.json",
59
+ "lint": "eslint .",
60
+ "format:eslint": "eslint --fix .",
61
+ "format:prettier": "prettier --write .",
62
+ "format": "conc -m 1 pnpm:format:eslint pnpm:format:prettier",
63
+ "test": "conc pnpm:test:type pnpm:test:unit",
64
+ "test:type": "tsc --noEmit",
65
+ "test:unit": "vitest run"
66
+ }
67
+ }