atomirx 0.0.1
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 +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { StableFn } from '../core/equality';
|
|
2
|
+
import { AnyFunc, Equality } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* Extracts non-function keys from an object type.
|
|
5
|
+
*/
|
|
6
|
+
type NonFunctionKeys<T> = {
|
|
7
|
+
[K in keyof T]: T[K] extends AnyFunc ? never : K;
|
|
8
|
+
}[keyof T];
|
|
9
|
+
/**
|
|
10
|
+
* Equals options for useStable - only non-function properties can have custom equality.
|
|
11
|
+
*/
|
|
12
|
+
export type UseStableEquals<T> = {
|
|
13
|
+
[K in NonFunctionKeys<T>]?: Equality<T[K]>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Result type for useStable - functions are wrapped in StableFn.
|
|
17
|
+
*/
|
|
18
|
+
export type UseStableResult<T> = {
|
|
19
|
+
[K in keyof T]: T[K] extends (...args: infer A) => infer R ? StableFn<A, R> : T[K];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* React hook that provides stable references for objects, arrays, and callbacks.
|
|
23
|
+
*
|
|
24
|
+
* `useStable` solves the common React problem of unstable references causing
|
|
25
|
+
* unnecessary re-renders, useEffect re-runs, and useCallback/useMemo invalidations.
|
|
26
|
+
*
|
|
27
|
+
* ## Why Use `useStable`?
|
|
28
|
+
*
|
|
29
|
+
* In React, inline objects, arrays, and callbacks create new references on every render:
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* // ❌ Problem: new reference every render
|
|
33
|
+
* function Parent() {
|
|
34
|
+
* const config = { theme: 'dark' }; // New object every render!
|
|
35
|
+
* const onClick = () => doSomething(); // New function every render!
|
|
36
|
+
* return <Child config={config} onClick={onClick} />;
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // ✅ Solution: stable references
|
|
40
|
+
* function Parent() {
|
|
41
|
+
* const stable = useStable({
|
|
42
|
+
* config: { theme: 'dark' },
|
|
43
|
+
* onClick: () => doSomething(),
|
|
44
|
+
* });
|
|
45
|
+
* return <Child config={stable.config} onClick={stable.onClick} />;
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ## How It Works
|
|
50
|
+
*
|
|
51
|
+
* Each property is independently stabilized based on its type:
|
|
52
|
+
*
|
|
53
|
+
* | Type | Default Equality | Behavior |
|
|
54
|
+
* |------|------------------|----------|
|
|
55
|
+
* | **Functions** | N/A (always wrapped) | Reference never changes, calls latest implementation |
|
|
56
|
+
* | **Arrays** | shallow | Stable if items are reference-equal |
|
|
57
|
+
* | **Dates** | timestamp | Stable if same time value |
|
|
58
|
+
* | **Objects** | shallow | Stable if keys have reference-equal values |
|
|
59
|
+
* | **Primitives** | strict | Stable if same value |
|
|
60
|
+
*
|
|
61
|
+
* ## Key Benefits
|
|
62
|
+
*
|
|
63
|
+
* 1. **Stable callbacks**: Functions maintain reference identity while always calling latest implementation
|
|
64
|
+
* 2. **Stable objects/arrays**: Prevent unnecessary child re-renders
|
|
65
|
+
* 3. **Safe for deps arrays**: Use in useEffect, useMemo, useCallback deps
|
|
66
|
+
* 4. **Per-property equality**: Customize comparison strategy for each property
|
|
67
|
+
* 5. **No wrapper overhead**: Returns the same result object reference
|
|
68
|
+
*
|
|
69
|
+
* @template T - The type of the input object
|
|
70
|
+
* @param input - Object with properties to stabilize
|
|
71
|
+
* @param equals - Optional custom equality strategies per property (except functions)
|
|
72
|
+
* @returns Stable object with same properties (functions wrapped in StableFn)
|
|
73
|
+
*
|
|
74
|
+
* @example Basic usage - stable callbacks and objects
|
|
75
|
+
* ```tsx
|
|
76
|
+
* function MyComponent({ userId }) {
|
|
77
|
+
* const stable = useStable({
|
|
78
|
+
* // Object - stable if shallow equal
|
|
79
|
+
* config: { theme: 'dark', userId },
|
|
80
|
+
* // Array - stable if items are reference-equal
|
|
81
|
+
* items: [1, 2, 3],
|
|
82
|
+
* // Function - reference never changes
|
|
83
|
+
* onClick: () => console.log('clicked', userId),
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* // Safe to use in deps - won't cause infinite loops
|
|
87
|
+
* useEffect(() => {
|
|
88
|
+
* console.log(stable.config);
|
|
89
|
+
* }, [stable.config]);
|
|
90
|
+
*
|
|
91
|
+
* // stable.onClick is always the same reference
|
|
92
|
+
* return <button onClick={stable.onClick}>Click</button>;
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @example Preventing child re-renders
|
|
97
|
+
* ```tsx
|
|
98
|
+
* function Parent() {
|
|
99
|
+
* const [count, setCount] = useState(0);
|
|
100
|
+
*
|
|
101
|
+
* const stable = useStable({
|
|
102
|
+
* // These won't cause Child to re-render when count changes
|
|
103
|
+
* user: { id: 1, name: 'John' },
|
|
104
|
+
* onSave: () => saveUser(),
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* return (
|
|
108
|
+
* <div>
|
|
109
|
+
* <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
|
|
110
|
+
* <MemoizedChild user={stable.user} onSave={stable.onSave} />
|
|
111
|
+
* </div>
|
|
112
|
+
* );
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example Custom equality per property
|
|
117
|
+
* ```tsx
|
|
118
|
+
* const stable = useStable(
|
|
119
|
+
* {
|
|
120
|
+
* user: { id: 1, profile: { name: "John", avatar: "..." } },
|
|
121
|
+
* tags: ["react", "typescript"],
|
|
122
|
+
* settings: { theme: "dark" },
|
|
123
|
+
* },
|
|
124
|
+
* {
|
|
125
|
+
* user: "deep", // Deep compare nested objects
|
|
126
|
+
* tags: "strict", // Override default shallow for arrays
|
|
127
|
+
* settings: "shallow", // Explicit shallow (same as default)
|
|
128
|
+
* }
|
|
129
|
+
* );
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Custom equality function
|
|
133
|
+
* ```tsx
|
|
134
|
+
* const stable = useStable(
|
|
135
|
+
* { user: { id: 1, name: "John", updatedAt: new Date() } },
|
|
136
|
+
* {
|
|
137
|
+
* // Only compare by id - ignore name and updatedAt changes
|
|
138
|
+
* user: (a, b) => a?.id === b?.id
|
|
139
|
+
* }
|
|
140
|
+
* );
|
|
141
|
+
* // stable.user reference only changes when id changes
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @example With useEffect deps
|
|
145
|
+
* ```tsx
|
|
146
|
+
* function DataFetcher({ filters }) {
|
|
147
|
+
* const stable = useStable({
|
|
148
|
+
* filters: { ...filters, timestamp: Date.now() },
|
|
149
|
+
* onSuccess: (data) => processData(data),
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* useEffect(() => {
|
|
153
|
+
* // Only re-runs when filters actually change (shallow comparison)
|
|
154
|
+
* fetchData(stable.filters).then(stable.onSuccess);
|
|
155
|
+
* }, [stable.filters, stable.onSuccess]);
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @example Stable event handlers for lists
|
|
160
|
+
* ```tsx
|
|
161
|
+
* function TodoList({ todos }) {
|
|
162
|
+
* const stable = useStable({
|
|
163
|
+
* onDelete: (id) => deleteTodo(id),
|
|
164
|
+
* onToggle: (id) => toggleTodo(id),
|
|
165
|
+
* });
|
|
166
|
+
*
|
|
167
|
+
* return (
|
|
168
|
+
* <ul>
|
|
169
|
+
* {todos.map(todo => (
|
|
170
|
+
* <TodoItem
|
|
171
|
+
* key={todo.id}
|
|
172
|
+
* todo={todo}
|
|
173
|
+
* onDelete={stable.onDelete} // Same reference for all items
|
|
174
|
+
* onToggle={stable.onToggle} // Prevents unnecessary re-renders
|
|
175
|
+
* />
|
|
176
|
+
* ))}
|
|
177
|
+
* </ul>
|
|
178
|
+
* );
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export declare function useStable<T extends Record<string, unknown>>(input: T, equals?: UseStableEquals<T>): UseStableResult<T>;
|
|
183
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ContextSelectorFn } from '../core/select';
|
|
2
|
+
import { Atom, Equality } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* React hook that selects/derives a value from atom(s) with automatic subscriptions.
|
|
5
|
+
*
|
|
6
|
+
* Uses `useSyncExternalStore` for proper React 18+ concurrent mode support.
|
|
7
|
+
* Only subscribes to atoms that are actually accessed during selection.
|
|
8
|
+
*
|
|
9
|
+
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
10
|
+
*
|
|
11
|
+
* **The selector function MUST NOT be async or return a Promise.**
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // ❌ WRONG - Don't use async function
|
|
15
|
+
* useValue(async ({ get }) => {
|
|
16
|
+
* const data = await fetch('/api');
|
|
17
|
+
* return data;
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // ❌ WRONG - Don't return a Promise
|
|
21
|
+
* useValue(({ get }) => fetch('/api').then(r => r.json()));
|
|
22
|
+
*
|
|
23
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
24
|
+
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
25
|
+
* useValue(({ get }) => get(data$)); // Suspends until resolved
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ## IMPORTANT: Suspense-Style API
|
|
29
|
+
*
|
|
30
|
+
* This hook uses a **Suspense-style API** for async atoms:
|
|
31
|
+
* - When an atom is **loading**, the getter throws a Promise (suspends)
|
|
32
|
+
* - When an atom has an **error**, the getter throws the error
|
|
33
|
+
* - When an atom is **resolved**, the getter returns the value
|
|
34
|
+
*
|
|
35
|
+
* This means:
|
|
36
|
+
* - **You MUST wrap components with `<Suspense>`** to handle loading states
|
|
37
|
+
* - **You MUST wrap components with `<ErrorBoundary>`** to handle errors
|
|
38
|
+
*
|
|
39
|
+
* ## Alternative: Using staleValue for Non-Suspense
|
|
40
|
+
*
|
|
41
|
+
* If you want to show loading states without Suspense:
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function MyComponent() {
|
|
45
|
+
* // Access staleValue directly - always has a value (with fallback)
|
|
46
|
+
* const count = myDerivedAtom$.staleValue;
|
|
47
|
+
* const isLoading = isPending(myDerivedAtom$.value);
|
|
48
|
+
*
|
|
49
|
+
* return (
|
|
50
|
+
* <div>
|
|
51
|
+
* {isLoading && <Spinner />}
|
|
52
|
+
* Count: {count}
|
|
53
|
+
* </div>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @template T - The type of the selected value
|
|
59
|
+
* @param selectorOrAtom - Atom or context-based selector function (must return sync value)
|
|
60
|
+
* @param equals - Equality function or shorthand. Defaults to "shallow"
|
|
61
|
+
* @returns The selected value (Awaited<T>)
|
|
62
|
+
* @throws Error if selector returns a Promise or PromiseLike
|
|
63
|
+
*
|
|
64
|
+
* @example Single atom (shorthand)
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const count = atom(5);
|
|
67
|
+
*
|
|
68
|
+
* function Counter() {
|
|
69
|
+
* const value = useValue(count);
|
|
70
|
+
* return <div>{value}</div>;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example With selector
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const count = atom(5);
|
|
77
|
+
*
|
|
78
|
+
* function Counter() {
|
|
79
|
+
* const doubled = useValue(({ get }) => get(count) * 2);
|
|
80
|
+
* return <div>{doubled}</div>;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example Multiple atoms
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const firstName = atom("John");
|
|
87
|
+
* const lastName = atom("Doe");
|
|
88
|
+
*
|
|
89
|
+
* function FullName() {
|
|
90
|
+
* const fullName = useValue(({ get }) =>
|
|
91
|
+
* `${get(firstName)} ${get(lastName)}`
|
|
92
|
+
* );
|
|
93
|
+
* return <div>{fullName}</div>;
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example Async atom with Suspense
|
|
98
|
+
* ```tsx
|
|
99
|
+
* const userAtom = atom(fetchUser());
|
|
100
|
+
*
|
|
101
|
+
* function UserProfile() {
|
|
102
|
+
* const user = useValue(({ get }) => get(userAtom));
|
|
103
|
+
* return <div>{user.name}</div>;
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* // MUST wrap with Suspense and ErrorBoundary
|
|
107
|
+
* function App() {
|
|
108
|
+
* return (
|
|
109
|
+
* <ErrorBoundary fallback={<div>Error!</div>}>
|
|
110
|
+
* <Suspense fallback={<div>Loading...</div>}>
|
|
111
|
+
* <UserProfile />
|
|
112
|
+
* </Suspense>
|
|
113
|
+
* </ErrorBoundary>
|
|
114
|
+
* );
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @example Using all() for multiple async atoms
|
|
119
|
+
* ```tsx
|
|
120
|
+
* const userAtom = atom(fetchUser());
|
|
121
|
+
* const postsAtom = atom(fetchPosts());
|
|
122
|
+
*
|
|
123
|
+
* function Dashboard() {
|
|
124
|
+
* const data = useValue(({ all }) => {
|
|
125
|
+
* const [user, posts] = all(userAtom, postsAtom);
|
|
126
|
+
* return { user, posts };
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* return <DashboardContent user={data.user} posts={data.posts} />;
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function useValue<T>(atom: Atom<T>, equals?: Equality<Awaited<T>>): Awaited<T>;
|
|
134
|
+
export declare function useValue<T>(selector: ContextSelectorFn<T>, equals?: Equality<T>): T;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atomirx",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/atomirx.umd.cjs",
|
|
6
|
+
"module": "./dist/atomirx.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/atomirx.js",
|
|
11
|
+
"require": "./dist/atomirx.umd.cjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./react": {
|
|
15
|
+
"import": "./dist/react/index.js",
|
|
16
|
+
"require": "./dist/react/index.cjs",
|
|
17
|
+
"types": "./dist/react/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc && vite build",
|
|
22
|
+
"test": "vitest --no-watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepublish:patch": "node scripts/publish.js pre patch",
|
|
25
|
+
"prepublish:minor": "node scripts/publish.js pre minor",
|
|
26
|
+
"prepublish:major": "node scripts/publish.js pre major",
|
|
27
|
+
"postpublish": "node scripts/publish.js post",
|
|
28
|
+
"release:patch": "node scripts/publish.js full patch",
|
|
29
|
+
"release:minor": "node scripts/publish.js full minor",
|
|
30
|
+
"release:major": "node scripts/publish.js full major"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"lodash": "^4.17.21"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^18.0.0",
|
|
37
|
+
"react-dom": "^18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react-dom": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/react": "^14.0.0",
|
|
49
|
+
"@types/lodash": "^4.17.14",
|
|
50
|
+
"@types/react": "^18.2.0",
|
|
51
|
+
"@types/react-dom": "^18.2.0",
|
|
52
|
+
"@vitest/coverage-v8": "^1.6.1",
|
|
53
|
+
"jsdom": "^24.0.0",
|
|
54
|
+
"react": "^18.2.0",
|
|
55
|
+
"react-dom": "^18.2.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publish helper scripts for manual npm publish with OTP.
|
|
5
|
+
*
|
|
6
|
+
* Workflow:
|
|
7
|
+
* 1. pnpm prepublish:patch (test -> build -> bump)
|
|
8
|
+
* 2. npm publish (manual, enter OTP via browser)
|
|
9
|
+
* 3. pnpm postpublish (git commit -> tag -> push)
|
|
10
|
+
*
|
|
11
|
+
* Or all at once (if you have automation token):
|
|
12
|
+
* pnpm publish:patch
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { dirname, join } from "path";
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
const packageJsonPath = join(__dirname, "..", "package.json");
|
|
23
|
+
|
|
24
|
+
function exec(command, options = {}) {
|
|
25
|
+
console.log(`\n> ${command}`);
|
|
26
|
+
try {
|
|
27
|
+
execSync(command, { stdio: "inherit", ...options });
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (options.allowFail) {
|
|
31
|
+
console.warn(` (skipped - already done or nothing to do)`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
console.error(`\n❌ Failed: ${command}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasGitRemote() {
|
|
40
|
+
try {
|
|
41
|
+
const result = execSync("git remote", { encoding: "utf-8" });
|
|
42
|
+
return result.trim().length > 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getVersion() {
|
|
49
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
50
|
+
return pkg.version;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bumpVersion(type) {
|
|
54
|
+
const validTypes = ["patch", "minor", "major"];
|
|
55
|
+
if (!validTypes.includes(type)) {
|
|
56
|
+
console.error(
|
|
57
|
+
`❌ Invalid version type: ${type}. Must be one of: ${validTypes.join(
|
|
58
|
+
", "
|
|
59
|
+
)}`
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`\n📦 Bumping ${type} version...`);
|
|
65
|
+
exec(`npm version ${type} --no-git-tag-version`);
|
|
66
|
+
return getVersion();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pre-publish: test -> build -> bump version
|
|
71
|
+
*/
|
|
72
|
+
function prepublish(versionType) {
|
|
73
|
+
console.log("🚀 Pre-publish: preparing for release...\n");
|
|
74
|
+
|
|
75
|
+
// Step 1: Test
|
|
76
|
+
console.log("1️⃣ Running tests...");
|
|
77
|
+
exec("npm test");
|
|
78
|
+
|
|
79
|
+
// Step 2: Build
|
|
80
|
+
console.log("\n2️⃣ Building...");
|
|
81
|
+
exec("npm run build");
|
|
82
|
+
|
|
83
|
+
// Step 3: Bump version
|
|
84
|
+
const newVersion = bumpVersion(versionType);
|
|
85
|
+
console.log(`\n✅ Version bumped to ${newVersion}`);
|
|
86
|
+
|
|
87
|
+
console.log("\n" + "=".repeat(50));
|
|
88
|
+
console.log(`📦 Ready to publish v${newVersion}`);
|
|
89
|
+
console.log("=".repeat(50));
|
|
90
|
+
console.log("\nNow run:");
|
|
91
|
+
console.log(" npm publish");
|
|
92
|
+
console.log("\nThen after successful publish, run:");
|
|
93
|
+
console.log(" pnpm postpublish\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Post-publish: git commit -> tag -> push
|
|
98
|
+
*/
|
|
99
|
+
function postpublish() {
|
|
100
|
+
const version = getVersion();
|
|
101
|
+
|
|
102
|
+
console.log("🚀 Post-publish: committing and pushing...\n");
|
|
103
|
+
|
|
104
|
+
// Git commit and tag
|
|
105
|
+
console.log("1️⃣ Creating git commit and tag...");
|
|
106
|
+
exec("git add package.json");
|
|
107
|
+
exec(`git commit -m "chore: release v${version}"`, { allowFail: true });
|
|
108
|
+
exec(`git tag v${version}`, { allowFail: true });
|
|
109
|
+
|
|
110
|
+
// Push to git (skip if no remote)
|
|
111
|
+
if (hasGitRemote()) {
|
|
112
|
+
console.log("\n2️⃣ Pushing to git...");
|
|
113
|
+
exec("git push");
|
|
114
|
+
exec("git push --tags");
|
|
115
|
+
} else {
|
|
116
|
+
console.log("\n⚠️ No git remote configured. Skipping push.");
|
|
117
|
+
console.log(" To push later, run:");
|
|
118
|
+
console.log(" git remote add origin <url>");
|
|
119
|
+
console.log(" git push -u origin main --tags");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`\n✅ Successfully released fluxdom@${version}!\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Full publish (for automation tokens)
|
|
127
|
+
*/
|
|
128
|
+
function fullPublish(versionType) {
|
|
129
|
+
console.log("🚀 Full publish workflow...\n");
|
|
130
|
+
|
|
131
|
+
// Pre-publish steps
|
|
132
|
+
console.log("1️⃣ Running tests...");
|
|
133
|
+
exec("npm test");
|
|
134
|
+
|
|
135
|
+
console.log("\n2️⃣ Building...");
|
|
136
|
+
exec("npm run build");
|
|
137
|
+
|
|
138
|
+
const newVersion = bumpVersion(versionType);
|
|
139
|
+
console.log(`✅ Version bumped to ${newVersion}`);
|
|
140
|
+
|
|
141
|
+
// Publish
|
|
142
|
+
console.log("\n3️⃣ Publishing to npm...");
|
|
143
|
+
exec("npm publish");
|
|
144
|
+
|
|
145
|
+
// Post-publish steps
|
|
146
|
+
console.log("\n4️⃣ Creating git commit and tag...");
|
|
147
|
+
exec("git add package.json");
|
|
148
|
+
exec(`git commit -m "chore: release v${newVersion}"`);
|
|
149
|
+
exec(`git tag v${newVersion}`);
|
|
150
|
+
|
|
151
|
+
// Push to git (skip if no remote)
|
|
152
|
+
if (hasGitRemote()) {
|
|
153
|
+
console.log("\n5️⃣ Pushing to git...");
|
|
154
|
+
exec("git push");
|
|
155
|
+
exec("git push --tags");
|
|
156
|
+
} else {
|
|
157
|
+
console.log("\n⚠️ No git remote configured. Skipping push.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`\n✅ Successfully published fluxdom@${newVersion}!\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Main
|
|
164
|
+
const command = process.argv[2];
|
|
165
|
+
const versionType = process.argv[3];
|
|
166
|
+
|
|
167
|
+
switch (command) {
|
|
168
|
+
case "pre":
|
|
169
|
+
if (!versionType) {
|
|
170
|
+
console.error(
|
|
171
|
+
"❌ Usage: node scripts/publish.js pre <patch|minor|major>"
|
|
172
|
+
);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
prepublish(versionType);
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case "post":
|
|
179
|
+
postpublish();
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case "full":
|
|
183
|
+
if (!versionType) {
|
|
184
|
+
console.error(
|
|
185
|
+
"❌ Usage: node scripts/publish.js full <patch|minor|major>"
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
fullPublish(versionType);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
console.error("❌ Usage:");
|
|
194
|
+
console.error(" node scripts/publish.js pre <patch|minor|major>");
|
|
195
|
+
console.error(" node scripts/publish.js post");
|
|
196
|
+
console.error(" node scripts/publish.js full <patch|minor|major>");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|