@arun-dev/headless 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arun Tripathi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ booleanAttribute: () => booleanAttribute,
24
+ getStateAttributes: () => getStateAttributes,
25
+ mergeProps: () => mergeProps,
26
+ useControlled: () => useControlled,
27
+ useRender: () => useRender
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/core/mergeProps.ts
32
+ function isEventHandler(key) {
33
+ return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
34
+ }
35
+ function mergeRefs(...refs) {
36
+ return (node) => {
37
+ const cleanups = refs.map((ref) => {
38
+ if (typeof ref === "function") {
39
+ const result = ref(node);
40
+ return typeof result === "function" ? result : () => ref(null);
41
+ }
42
+ if (ref && typeof ref === "object") {
43
+ ref.current = node;
44
+ return () => {
45
+ ref.current = null;
46
+ };
47
+ }
48
+ return void 0;
49
+ });
50
+ return () => {
51
+ cleanups.forEach((cleanup) => {
52
+ if (typeof cleanup === "function") cleanup();
53
+ });
54
+ };
55
+ };
56
+ }
57
+ function mergeProps(...objects) {
58
+ const merged = {};
59
+ for (const props of objects) {
60
+ if (!props) continue;
61
+ for (const key of Object.keys(props)) {
62
+ const value = props[key];
63
+ if (value === void 0) continue;
64
+ if (isEventHandler(key) && typeof value === "function") {
65
+ const existing = merged[key];
66
+ merged[key] = typeof existing === "function" ? (...args) => {
67
+ existing(...args);
68
+ value(...args);
69
+ } : value;
70
+ } else if (key === "className") {
71
+ merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
72
+ } else if (key === "style") {
73
+ merged.style = { ...merged.style, ...value };
74
+ } else if (key === "ref") {
75
+ merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
76
+ } else {
77
+ merged[key] = value;
78
+ }
79
+ }
80
+ }
81
+ return merged;
82
+ }
83
+
84
+ // src/core/useRender.ts
85
+ var import_react = require("react");
86
+
87
+ // src/core/stateAttributes.ts
88
+ function getStateAttributes(state, mapping) {
89
+ if (!mapping) return {};
90
+ const attributes = {};
91
+ for (const key of Object.keys(state)) {
92
+ const toAttributes = mapping[key];
93
+ if (!toAttributes) continue;
94
+ const result = toAttributes(state[key]);
95
+ if (result) Object.assign(attributes, result);
96
+ }
97
+ return attributes;
98
+ }
99
+ function booleanAttribute(whenTrue, whenFalse) {
100
+ return (value) => {
101
+ if (value) return { [whenTrue]: "" };
102
+ return whenFalse ? { [whenFalse]: "" } : null;
103
+ };
104
+ }
105
+
106
+ // src/core/useRender.ts
107
+ function useRender({
108
+ render,
109
+ defaultTagName,
110
+ props,
111
+ state,
112
+ stateAttributes
113
+ }) {
114
+ const attributes = state ? getStateAttributes(state, stateAttributes) : {};
115
+ const merged = mergeProps(attributes, ...props);
116
+ if ((0, import_react.isValidElement)(render)) {
117
+ return (0, import_react.cloneElement)(render, mergeProps(merged, render.props));
118
+ }
119
+ return (0, import_react.createElement)(defaultTagName, merged);
120
+ }
121
+
122
+ // src/core/useControlled.ts
123
+ var import_react2 = require("react");
124
+ function useControlled({
125
+ controlled,
126
+ default: defaultValue,
127
+ name = "Component",
128
+ state = "value"
129
+ }) {
130
+ const { current: isControlled } = (0, import_react2.useRef)(controlled !== void 0);
131
+ const [uncontrolled, setUncontrolled] = (0, import_react2.useState)(defaultValue);
132
+ const defaultRef = (0, import_react2.useRef)(defaultValue);
133
+ const value = isControlled ? controlled : uncontrolled;
134
+ if (process.env.NODE_ENV !== "production") {
135
+ if (isControlled !== (controlled !== void 0)) {
136
+ console.error(
137
+ `${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
138
+ );
139
+ }
140
+ if (!isControlled && defaultRef.current !== defaultValue) {
141
+ console.error(
142
+ `${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
143
+ );
144
+ }
145
+ }
146
+ const setValue = (0, import_react2.useCallback)(
147
+ (next) => {
148
+ if (!isControlled) setUncontrolled(next);
149
+ },
150
+ [isControlled]
151
+ );
152
+ return [value, setValue];
153
+ }
154
+ // Annotate the CommonJS export names for ESM import in node:
155
+ 0 && (module.exports = {
156
+ booleanAttribute,
157
+ getStateAttributes,
158
+ mergeProps,
159
+ useControlled,
160
+ useRender
161
+ });
@@ -0,0 +1,99 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ /**
4
+ * Prop merging for headless components.
5
+ *
6
+ * A component and its consumer both want to put things on the same element. Naive
7
+ * spreading lets whichever runs last silently destroy the other's behaviour, so each
8
+ * kind of prop is combined rather than replaced.
9
+ */
10
+ type UnknownProps = Record<string, unknown>;
11
+ /**
12
+ * Merge prop objects left to right. Later objects win for plain values, but:
13
+ * - event handlers are chained rather than replaced
14
+ * - className is concatenated
15
+ * - style is shallow-merged
16
+ * - refs are merged
17
+ *
18
+ * `undefined` values are skipped, so an absent key on a later object never
19
+ * clobbers a value set by an earlier one.
20
+ */
21
+ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
22
+
23
+ /**
24
+ * Projects a component's state onto the DOM as `data-*` attributes.
25
+ *
26
+ * This is the contract between behaviour and styling. A headless component owns no
27
+ * class names, so the only way CSS can react to `checked` or `disabled` is for the
28
+ * state to be visible in the DOM:
29
+ *
30
+ * .switch[data-checked] { background: var(--switch-track-bg-checked); }
31
+ *
32
+ * Doing it through a declared mapping rather than by hand keeps the attributes
33
+ * consistent across components — Base UI's Checkbox emits eleven of them, which is
34
+ * not something anyone maintains by hand — and makes the state a component exposes
35
+ * a single readable object rather than scattered JSX.
36
+ */
37
+ /**
38
+ * Maps one state value to the attributes it should produce. Return `null` for
39
+ * "no attribute". A single value may produce several, and mutually exclusive
40
+ * attributes are how a third state stays addressable: with only `data-checked`,
41
+ * `:not([data-checked])` would match both unchecked *and* indeterminate.
42
+ */
43
+ type StateAttributeMapping<State> = {
44
+ [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
45
+ };
46
+ declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
47
+ /**
48
+ * The common case: a boolean that emits one attribute when true and another when
49
+ * false, so both sides are addressable at equal specificity.
50
+ *
51
+ * checked: booleanAttribute('data-checked', 'data-unchecked')
52
+ */
53
+ declare function booleanAttribute(whenTrue: string, whenFalse?: string): (value: unknown) => Record<string, string> | null;
54
+
55
+ /**
56
+ * Resolves what a component part actually renders.
57
+ *
58
+ * Every part goes through here, which is what makes the whole library consistent:
59
+ * one place decides how props merge, how state reaches the DOM, and what element
60
+ * comes out. A part supplies its default element, its state, and the props it wants;
61
+ * the consumer can replace the element entirely via `render` without losing any of it.
62
+ *
63
+ * Currently uses no hooks. The `use` prefix matches the API it will grow into
64
+ * (memoised merging, so parts stop re-rendering their children needlessly) and keeps
65
+ * call sites stable when that lands.
66
+ */
67
+ interface UseRenderParams<State extends Record<string, unknown>> {
68
+ /** Element to render instead of the default. Props, className and ref are merged onto it. */
69
+ render?: ReactElement | undefined;
70
+ /** Tag rendered when `render` is not supplied. */
71
+ defaultTagName: string;
72
+ /** Prop objects to merge, in precedence order (later wins). */
73
+ props: (UnknownProps | undefined)[];
74
+ /** The part's state, projected onto the DOM through `stateAttributes`. */
75
+ state?: State;
76
+ /** How each state field becomes `data-*` attributes. */
77
+ stateAttributes?: StateAttributeMapping<State>;
78
+ }
79
+ declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
80
+
81
+ /**
82
+ * Supports both controlled and uncontrolled use of a single value.
83
+ *
84
+ * The mode is decided once, at mount, and never re-evaluated. Recomputing it per
85
+ * render would let a parent that briefly passes `undefined` flip the component to
86
+ * uncontrolled and hand it stale internal state — a bug that is very hard to see.
87
+ *
88
+ * In controlled mode the setter deliberately does not write state: the parent owns
89
+ * the value. Callers still fire their `onChange` in both modes, so the component
90
+ * notifies either way.
91
+ */
92
+ declare function useControlled<T>({ controlled, default: defaultValue, name, state, }: {
93
+ controlled: T | undefined;
94
+ default: T;
95
+ name?: string;
96
+ state?: string;
97
+ }): [T, (next: T) => void];
98
+
99
+ export { type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, getStateAttributes, mergeProps, useControlled, useRender };
@@ -0,0 +1,99 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ /**
4
+ * Prop merging for headless components.
5
+ *
6
+ * A component and its consumer both want to put things on the same element. Naive
7
+ * spreading lets whichever runs last silently destroy the other's behaviour, so each
8
+ * kind of prop is combined rather than replaced.
9
+ */
10
+ type UnknownProps = Record<string, unknown>;
11
+ /**
12
+ * Merge prop objects left to right. Later objects win for plain values, but:
13
+ * - event handlers are chained rather than replaced
14
+ * - className is concatenated
15
+ * - style is shallow-merged
16
+ * - refs are merged
17
+ *
18
+ * `undefined` values are skipped, so an absent key on a later object never
19
+ * clobbers a value set by an earlier one.
20
+ */
21
+ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
22
+
23
+ /**
24
+ * Projects a component's state onto the DOM as `data-*` attributes.
25
+ *
26
+ * This is the contract between behaviour and styling. A headless component owns no
27
+ * class names, so the only way CSS can react to `checked` or `disabled` is for the
28
+ * state to be visible in the DOM:
29
+ *
30
+ * .switch[data-checked] { background: var(--switch-track-bg-checked); }
31
+ *
32
+ * Doing it through a declared mapping rather than by hand keeps the attributes
33
+ * consistent across components — Base UI's Checkbox emits eleven of them, which is
34
+ * not something anyone maintains by hand — and makes the state a component exposes
35
+ * a single readable object rather than scattered JSX.
36
+ */
37
+ /**
38
+ * Maps one state value to the attributes it should produce. Return `null` for
39
+ * "no attribute". A single value may produce several, and mutually exclusive
40
+ * attributes are how a third state stays addressable: with only `data-checked`,
41
+ * `:not([data-checked])` would match both unchecked *and* indeterminate.
42
+ */
43
+ type StateAttributeMapping<State> = {
44
+ [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
45
+ };
46
+ declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
47
+ /**
48
+ * The common case: a boolean that emits one attribute when true and another when
49
+ * false, so both sides are addressable at equal specificity.
50
+ *
51
+ * checked: booleanAttribute('data-checked', 'data-unchecked')
52
+ */
53
+ declare function booleanAttribute(whenTrue: string, whenFalse?: string): (value: unknown) => Record<string, string> | null;
54
+
55
+ /**
56
+ * Resolves what a component part actually renders.
57
+ *
58
+ * Every part goes through here, which is what makes the whole library consistent:
59
+ * one place decides how props merge, how state reaches the DOM, and what element
60
+ * comes out. A part supplies its default element, its state, and the props it wants;
61
+ * the consumer can replace the element entirely via `render` without losing any of it.
62
+ *
63
+ * Currently uses no hooks. The `use` prefix matches the API it will grow into
64
+ * (memoised merging, so parts stop re-rendering their children needlessly) and keeps
65
+ * call sites stable when that lands.
66
+ */
67
+ interface UseRenderParams<State extends Record<string, unknown>> {
68
+ /** Element to render instead of the default. Props, className and ref are merged onto it. */
69
+ render?: ReactElement | undefined;
70
+ /** Tag rendered when `render` is not supplied. */
71
+ defaultTagName: string;
72
+ /** Prop objects to merge, in precedence order (later wins). */
73
+ props: (UnknownProps | undefined)[];
74
+ /** The part's state, projected onto the DOM through `stateAttributes`. */
75
+ state?: State;
76
+ /** How each state field becomes `data-*` attributes. */
77
+ stateAttributes?: StateAttributeMapping<State>;
78
+ }
79
+ declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
80
+
81
+ /**
82
+ * Supports both controlled and uncontrolled use of a single value.
83
+ *
84
+ * The mode is decided once, at mount, and never re-evaluated. Recomputing it per
85
+ * render would let a parent that briefly passes `undefined` flip the component to
86
+ * uncontrolled and hand it stale internal state — a bug that is very hard to see.
87
+ *
88
+ * In controlled mode the setter deliberately does not write state: the parent owns
89
+ * the value. Callers still fire their `onChange` in both modes, so the component
90
+ * notifies either way.
91
+ */
92
+ declare function useControlled<T>({ controlled, default: defaultValue, name, state, }: {
93
+ controlled: T | undefined;
94
+ default: T;
95
+ name?: string;
96
+ state?: string;
97
+ }): [T, (next: T) => void];
98
+
99
+ export { type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, getStateAttributes, mergeProps, useControlled, useRender };
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ // src/core/mergeProps.ts
2
+ function isEventHandler(key) {
3
+ return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
4
+ }
5
+ function mergeRefs(...refs) {
6
+ return (node) => {
7
+ const cleanups = refs.map((ref) => {
8
+ if (typeof ref === "function") {
9
+ const result = ref(node);
10
+ return typeof result === "function" ? result : () => ref(null);
11
+ }
12
+ if (ref && typeof ref === "object") {
13
+ ref.current = node;
14
+ return () => {
15
+ ref.current = null;
16
+ };
17
+ }
18
+ return void 0;
19
+ });
20
+ return () => {
21
+ cleanups.forEach((cleanup) => {
22
+ if (typeof cleanup === "function") cleanup();
23
+ });
24
+ };
25
+ };
26
+ }
27
+ function mergeProps(...objects) {
28
+ const merged = {};
29
+ for (const props of objects) {
30
+ if (!props) continue;
31
+ for (const key of Object.keys(props)) {
32
+ const value = props[key];
33
+ if (value === void 0) continue;
34
+ if (isEventHandler(key) && typeof value === "function") {
35
+ const existing = merged[key];
36
+ merged[key] = typeof existing === "function" ? (...args) => {
37
+ existing(...args);
38
+ value(...args);
39
+ } : value;
40
+ } else if (key === "className") {
41
+ merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
42
+ } else if (key === "style") {
43
+ merged.style = { ...merged.style, ...value };
44
+ } else if (key === "ref") {
45
+ merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
46
+ } else {
47
+ merged[key] = value;
48
+ }
49
+ }
50
+ }
51
+ return merged;
52
+ }
53
+
54
+ // src/core/useRender.ts
55
+ import { cloneElement, createElement, isValidElement } from "react";
56
+
57
+ // src/core/stateAttributes.ts
58
+ function getStateAttributes(state, mapping) {
59
+ if (!mapping) return {};
60
+ const attributes = {};
61
+ for (const key of Object.keys(state)) {
62
+ const toAttributes = mapping[key];
63
+ if (!toAttributes) continue;
64
+ const result = toAttributes(state[key]);
65
+ if (result) Object.assign(attributes, result);
66
+ }
67
+ return attributes;
68
+ }
69
+ function booleanAttribute(whenTrue, whenFalse) {
70
+ return (value) => {
71
+ if (value) return { [whenTrue]: "" };
72
+ return whenFalse ? { [whenFalse]: "" } : null;
73
+ };
74
+ }
75
+
76
+ // src/core/useRender.ts
77
+ function useRender({
78
+ render,
79
+ defaultTagName,
80
+ props,
81
+ state,
82
+ stateAttributes
83
+ }) {
84
+ const attributes = state ? getStateAttributes(state, stateAttributes) : {};
85
+ const merged = mergeProps(attributes, ...props);
86
+ if (isValidElement(render)) {
87
+ return cloneElement(render, mergeProps(merged, render.props));
88
+ }
89
+ return createElement(defaultTagName, merged);
90
+ }
91
+
92
+ // src/core/useControlled.ts
93
+ import { useCallback, useRef, useState } from "react";
94
+ function useControlled({
95
+ controlled,
96
+ default: defaultValue,
97
+ name = "Component",
98
+ state = "value"
99
+ }) {
100
+ const { current: isControlled } = useRef(controlled !== void 0);
101
+ const [uncontrolled, setUncontrolled] = useState(defaultValue);
102
+ const defaultRef = useRef(defaultValue);
103
+ const value = isControlled ? controlled : uncontrolled;
104
+ if (process.env.NODE_ENV !== "production") {
105
+ if (isControlled !== (controlled !== void 0)) {
106
+ console.error(
107
+ `${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
108
+ );
109
+ }
110
+ if (!isControlled && defaultRef.current !== defaultValue) {
111
+ console.error(
112
+ `${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
113
+ );
114
+ }
115
+ }
116
+ const setValue = useCallback(
117
+ (next) => {
118
+ if (!isControlled) setUncontrolled(next);
119
+ },
120
+ [isControlled]
121
+ );
122
+ return [value, setValue];
123
+ }
124
+ export {
125
+ booleanAttribute,
126
+ getStateAttributes,
127
+ mergeProps,
128
+ useControlled,
129
+ useRender
130
+ };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@arun-dev/headless",
3
+ "version": "0.1.0",
4
+ "description": "Unstyled React behaviour primitives — the render engine and state plumbing @arun-dev/ui is built on. Ships no CSS and no class names.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/arun9483/arun-design-system.git",
10
+ "directory": "packages/headless"
11
+ },
12
+ "keywords": [
13
+ "react",
14
+ "headless",
15
+ "unstyled",
16
+ "design-system",
17
+ "primitives"
18
+ ],
19
+ "sideEffects": false,
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "@types/react": ">=19.0.0",
43
+ "react": ">=19.0.0",
44
+ "react-dom": ">=19.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@types/react": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@testing-library/jest-dom": "6.9.1",
53
+ "@testing-library/react": "16.3.0",
54
+ "@types/react": "19.2.18",
55
+ "@types/react-dom": "19.2.5",
56
+ "eslint": "9.39.4",
57
+ "jsdom": "27.0.0",
58
+ "react": "19.2.8",
59
+ "react-dom": "19.2.8",
60
+ "tsup": "8.5.1",
61
+ "typescript": "5.9.3",
62
+ "vitest": "3.2.6",
63
+ "@arun-dev/config": "0.0.0"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "clean": "rm -rf dist .turbo",
68
+ "typecheck": "tsc --noEmit",
69
+ "lint": "eslint .",
70
+ "lint:fix": "eslint . --fix",
71
+ "test": "pnpm run test:unit",
72
+ "test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts"
73
+ }
74
+ }