@arcanejs/react-toolkit 0.1.5 → 0.3.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.
@@ -0,0 +1,96 @@
1
+ import { ReactNode, FC, Context } from 'react';
2
+ import { ZodType } from 'zod';
3
+
4
+ type WithPathChange = {
5
+ /**
6
+ * When the file path changes and the file does not yet exist,
7
+ * should the previous data be stored in the new file
8
+ * or should the new file be reset to the default value?
9
+ *
10
+ * @default 'defaultValue'
11
+ */
12
+ onPathChange?: 'transfer' | 'defaultValue';
13
+ };
14
+ type DataFileUsage = WithPathChange & {
15
+ /**
16
+ * The path to where the JSON data should be stored,
17
+ * this will be relative to the current working directory.
18
+ */
19
+ path: string;
20
+ };
21
+ type ProviderProps = DataFileUsage & {
22
+ children: ReactNode;
23
+ };
24
+ type DataFileUpdater<T> = (update: (current: T) => T) => void;
25
+ type DataFileContext<T> = {
26
+ data: T;
27
+ updateData: DataFileUpdater<T>;
28
+ /**
29
+ * Can be called to force an attempt to re-save the data to disk
30
+ */
31
+ saveData: () => void;
32
+ /**
33
+ * If an error has ocurred in the last operation (e.g. load or save,
34
+ * then this will be set to that error).
35
+ *
36
+ * Can be used for example to re-prompt users to save data.
37
+ */
38
+ error: unknown;
39
+ };
40
+ type DataFileDefinition<T> = {
41
+ Provider: FC<ProviderProps>;
42
+ context: Context<DataFileContext<T>>;
43
+ useDataFile: (props: DataFileUsage) => DataFileCore<T>;
44
+ };
45
+ declare function useDataFileData<T>(dataFile: DataFileDefinition<T>): T;
46
+ declare function useDataFileUpdater<T>(dataFile: DataFileDefinition<T>): DataFileUpdater<T>;
47
+ /**
48
+ * Convenience hook to use the internal data-file context hook
49
+ */
50
+ declare function useDataFileContext<T>(dataFile: DataFileDefinition<T>): DataFileContext<T>;
51
+ /**
52
+ * Convenience hook to use the internal data-file definition hook
53
+ * in a more react-like manner.
54
+ */
55
+ declare function useDataFile<T>(dataFile: DataFileDefinition<T>, usage: DataFileUsage): DataFileCore<T>;
56
+ type DataState<T> = {
57
+ status: 'loading';
58
+ /**
59
+ * The data is not yet loaded, so this will be undefined,
60
+ * but it's listed here as a property so that the data property can be
61
+ * directly used without a type-guard that uses `state`.
62
+ *
63
+ * This should hopefully also avoid situations where users may check for
64
+ * `state === 'ready'` instead of `state !== 'loading'`,
65
+ * and accidentally avoid displaying data that's available but unsaved.
66
+ */
67
+ data: undefined;
68
+ } | {
69
+ status: 'error';
70
+ data: T | undefined;
71
+ error: unknown;
72
+ } | {
73
+ status: 'ready';
74
+ data: T;
75
+ };
76
+ type UseDataFileCoreProps<T> = WithPathChange & {
77
+ schema: ZodType<T>;
78
+ defaultValue: T;
79
+ path: string;
80
+ };
81
+ type DataFileCore<T> = {
82
+ data: DataState<T>;
83
+ updateData: DataFileUpdater<T>;
84
+ saveData: () => void;
85
+ };
86
+ /**
87
+ * Primary hook for & logic for using data files.
88
+ */
89
+ declare function useDataFileCore<T>({ schema, defaultValue, path, onPathChange, }: UseDataFileCoreProps<T>): DataFileCore<T>;
90
+ type CreateDataFileDefinitionProps<T> = {
91
+ schema: ZodType<T>;
92
+ defaultValue: T;
93
+ };
94
+ declare function createDataFileDefinition<T>({ schema, defaultValue, }: CreateDataFileDefinitionProps<T>): DataFileDefinition<T>;
95
+
96
+ export { type CreateDataFileDefinitionProps, type DataFileContext, type DataFileCore, type DataFileDefinition, type DataFileUpdater, type DataState, type ProviderProps, type UseDataFileCoreProps, createDataFileDefinition, useDataFile, useDataFileContext, useDataFileCore, useDataFileData, useDataFileUpdater };
package/dist/data.d.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { ReactNode, FC, Context } from 'react';
2
+ import { ZodType } from 'zod';
3
+
4
+ type WithPathChange = {
5
+ /**
6
+ * When the file path changes and the file does not yet exist,
7
+ * should the previous data be stored in the new file
8
+ * or should the new file be reset to the default value?
9
+ *
10
+ * @default 'defaultValue'
11
+ */
12
+ onPathChange?: 'transfer' | 'defaultValue';
13
+ };
14
+ type DataFileUsage = WithPathChange & {
15
+ /**
16
+ * The path to where the JSON data should be stored,
17
+ * this will be relative to the current working directory.
18
+ */
19
+ path: string;
20
+ };
21
+ type ProviderProps = DataFileUsage & {
22
+ children: ReactNode;
23
+ };
24
+ type DataFileUpdater<T> = (update: (current: T) => T) => void;
25
+ type DataFileContext<T> = {
26
+ data: T;
27
+ updateData: DataFileUpdater<T>;
28
+ /**
29
+ * Can be called to force an attempt to re-save the data to disk
30
+ */
31
+ saveData: () => void;
32
+ /**
33
+ * If an error has ocurred in the last operation (e.g. load or save,
34
+ * then this will be set to that error).
35
+ *
36
+ * Can be used for example to re-prompt users to save data.
37
+ */
38
+ error: unknown;
39
+ };
40
+ type DataFileDefinition<T> = {
41
+ Provider: FC<ProviderProps>;
42
+ context: Context<DataFileContext<T>>;
43
+ useDataFile: (props: DataFileUsage) => DataFileCore<T>;
44
+ };
45
+ declare function useDataFileData<T>(dataFile: DataFileDefinition<T>): T;
46
+ declare function useDataFileUpdater<T>(dataFile: DataFileDefinition<T>): DataFileUpdater<T>;
47
+ /**
48
+ * Convenience hook to use the internal data-file context hook
49
+ */
50
+ declare function useDataFileContext<T>(dataFile: DataFileDefinition<T>): DataFileContext<T>;
51
+ /**
52
+ * Convenience hook to use the internal data-file definition hook
53
+ * in a more react-like manner.
54
+ */
55
+ declare function useDataFile<T>(dataFile: DataFileDefinition<T>, usage: DataFileUsage): DataFileCore<T>;
56
+ type DataState<T> = {
57
+ status: 'loading';
58
+ /**
59
+ * The data is not yet loaded, so this will be undefined,
60
+ * but it's listed here as a property so that the data property can be
61
+ * directly used without a type-guard that uses `state`.
62
+ *
63
+ * This should hopefully also avoid situations where users may check for
64
+ * `state === 'ready'` instead of `state !== 'loading'`,
65
+ * and accidentally avoid displaying data that's available but unsaved.
66
+ */
67
+ data: undefined;
68
+ } | {
69
+ status: 'error';
70
+ data: T | undefined;
71
+ error: unknown;
72
+ } | {
73
+ status: 'ready';
74
+ data: T;
75
+ };
76
+ type UseDataFileCoreProps<T> = WithPathChange & {
77
+ schema: ZodType<T>;
78
+ defaultValue: T;
79
+ path: string;
80
+ };
81
+ type DataFileCore<T> = {
82
+ data: DataState<T>;
83
+ updateData: DataFileUpdater<T>;
84
+ saveData: () => void;
85
+ };
86
+ /**
87
+ * Primary hook for & logic for using data files.
88
+ */
89
+ declare function useDataFileCore<T>({ schema, defaultValue, path, onPathChange, }: UseDataFileCoreProps<T>): DataFileCore<T>;
90
+ type CreateDataFileDefinitionProps<T> = {
91
+ schema: ZodType<T>;
92
+ defaultValue: T;
93
+ };
94
+ declare function createDataFileDefinition<T>({ schema, defaultValue, }: CreateDataFileDefinitionProps<T>): DataFileDefinition<T>;
95
+
96
+ export { type CreateDataFileDefinitionProps, type DataFileContext, type DataFileCore, type DataFileDefinition, type DataFileUpdater, type DataState, type ProviderProps, type UseDataFileCoreProps, createDataFileDefinition, useDataFile, useDataFileContext, useDataFileCore, useDataFileData, useDataFileUpdater };
package/dist/data.js ADDED
@@ -0,0 +1,248 @@
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/data.tsx
21
+ var data_exports = {};
22
+ __export(data_exports, {
23
+ createDataFileDefinition: () => createDataFileDefinition,
24
+ useDataFile: () => useDataFile,
25
+ useDataFileContext: () => useDataFileContext,
26
+ useDataFileCore: () => useDataFileCore,
27
+ useDataFileData: () => useDataFileData,
28
+ useDataFileUpdater: () => useDataFileUpdater
29
+ });
30
+ module.exports = __toCommonJS(data_exports);
31
+ var import_fs = require("fs");
32
+ var import_react = require("react");
33
+ var import_lodash = require("lodash");
34
+ var import_path = require("path");
35
+ var import_jsx_runtime = require("react/jsx-runtime");
36
+ function useDataFileData(dataFile) {
37
+ return (0, import_react.useContext)(dataFile.context).data;
38
+ }
39
+ function useDataFileUpdater(dataFile) {
40
+ return (0, import_react.useContext)(dataFile.context).updateData;
41
+ }
42
+ function useDataFileContext(dataFile) {
43
+ return (0, import_react.useContext)(dataFile.context);
44
+ }
45
+ function useDataFile(dataFile, usage) {
46
+ return dataFile.useDataFile(usage);
47
+ }
48
+ function useDataFileCore({
49
+ schema,
50
+ defaultValue,
51
+ path,
52
+ onPathChange = "defaultValue"
53
+ }) {
54
+ const state = (0, import_react.useRef)({
55
+ initialized: false,
56
+ path: null,
57
+ data: void 0,
58
+ previousData: void 0,
59
+ state: {
60
+ state: "saved"
61
+ }
62
+ });
63
+ (0, import_react.useEffect)(() => {
64
+ if (!state.current.initialized) {
65
+ state.current.initialized = true;
66
+ } else {
67
+ throw new Error(
68
+ "Cannot change schema or defaultValue after initialization"
69
+ );
70
+ }
71
+ }, [schema, defaultValue]);
72
+ const [data, setData] = (0, import_react.useState)({
73
+ status: "loading",
74
+ data: void 0
75
+ });
76
+ const updateDataFromState = (0, import_react.useMemo)(
77
+ () => () => {
78
+ const data2 = state.current.data;
79
+ if (state.current.state.state === "error") {
80
+ setData({
81
+ status: "error",
82
+ error: state.current.state.error,
83
+ data: data2
84
+ });
85
+ return;
86
+ }
87
+ if (data2 === void 0) {
88
+ setData({
89
+ status: "loading",
90
+ data: void 0
91
+ });
92
+ return;
93
+ }
94
+ setData({
95
+ status: "ready",
96
+ data: data2
97
+ });
98
+ },
99
+ []
100
+ );
101
+ const saveData = (0, import_react.useMemo)(
102
+ () => (0, import_lodash.throttle)(
103
+ async () => {
104
+ if (state.current.state.state === "saved") {
105
+ return;
106
+ }
107
+ const currentPath = state.current.path;
108
+ const currentData = state.current.data;
109
+ if (!currentPath || currentData === void 0) {
110
+ return;
111
+ }
112
+ try {
113
+ const json = JSON.stringify(currentData, null, 2);
114
+ await import_fs.promises.mkdir((0, import_path.dirname)(currentPath), { recursive: true });
115
+ await import_fs.promises.writeFile(currentPath, json, "utf8");
116
+ if (state.current.path === currentPath && state.current.data === currentData) {
117
+ state.current.state = { state: "saved" };
118
+ }
119
+ } catch (error) {
120
+ if (state.current.path === currentPath && state.current.data === currentData) {
121
+ state.current.state = { state: "error", error };
122
+ updateDataFromState();
123
+ }
124
+ }
125
+ },
126
+ 500,
127
+ {
128
+ // Write leading so that we always write to disk quickly when
129
+ // only single things have changed
130
+ leading: true,
131
+ // Trailing is important otherwise we may lose data
132
+ trailing: true
133
+ }
134
+ ),
135
+ []
136
+ );
137
+ (0, import_react.useEffect)(() => {
138
+ state.current = {
139
+ ...state.current,
140
+ path,
141
+ data: void 0,
142
+ previousData: state.current.data ?? state.current.previousData,
143
+ state: {
144
+ state: "saved"
145
+ }
146
+ };
147
+ import_fs.promises.readFile(path, "utf8").then((data2) => {
148
+ const parsedData = schema.parse(JSON.parse(data2));
149
+ if (state.current.path === path) {
150
+ state.current.data = parsedData;
151
+ state.current.state = { state: "saved" };
152
+ updateDataFromState();
153
+ }
154
+ }).catch((error) => {
155
+ if (state.current.path !== path) {
156
+ return;
157
+ }
158
+ if (error.code === "ENOENT") {
159
+ console.log("Creating new file");
160
+ const initialData = onPathChange === "transfer" && state.current.previousData !== void 0 ? state.current.previousData : defaultValue;
161
+ state.current.data = initialData;
162
+ state.current.state = { state: "dirty" };
163
+ saveData();
164
+ updateDataFromState();
165
+ return;
166
+ }
167
+ state.current.state = { state: "error", error };
168
+ updateDataFromState();
169
+ });
170
+ }, [path, onPathChange]);
171
+ const updateData = (0, import_react.useMemo)(
172
+ () => (update) => {
173
+ if (state.current.path !== path) {
174
+ return;
175
+ }
176
+ if (state.current.data === void 0) {
177
+ throw new Error("Attempt to update data before it has been loaded");
178
+ }
179
+ state.current.data = update(state.current.data);
180
+ state.current.state = { state: "dirty" };
181
+ saveData();
182
+ updateDataFromState();
183
+ },
184
+ [path]
185
+ );
186
+ return {
187
+ data,
188
+ updateData,
189
+ saveData
190
+ };
191
+ }
192
+ function createDataFileDefinition({
193
+ schema,
194
+ defaultValue
195
+ }) {
196
+ const context = (0, import_react.createContext)({
197
+ data: defaultValue,
198
+ updateData: () => {
199
+ throw new Error("Data file provider not used");
200
+ },
201
+ saveData: () => {
202
+ throw new Error("Data file provider not used");
203
+ },
204
+ error: void 0
205
+ });
206
+ const useDataFile2 = ({
207
+ path,
208
+ onPathChange
209
+ }) => useDataFileCore({
210
+ schema,
211
+ defaultValue,
212
+ path,
213
+ onPathChange
214
+ });
215
+ const Provider = ({ path, onPathChange, children }) => {
216
+ const { data, updateData, saveData } = useDataFile2({
217
+ path,
218
+ onPathChange
219
+ });
220
+ const providedContext = (0, import_react.useMemo)(
221
+ () => ({
222
+ data: data.status !== "loading" && data.data !== void 0 ? data.data : defaultValue,
223
+ updateData,
224
+ saveData,
225
+ error: data.status === "error" ? data.error : void 0
226
+ }),
227
+ [data, updateData]
228
+ );
229
+ if (data.status === "loading") {
230
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "Loading..." });
231
+ }
232
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(context.Provider, { value: providedContext, children });
233
+ };
234
+ return {
235
+ Provider,
236
+ context,
237
+ useDataFile: useDataFile2
238
+ };
239
+ }
240
+ // Annotate the CommonJS export names for ESM import in node:
241
+ 0 && (module.exports = {
242
+ createDataFileDefinition,
243
+ useDataFile,
244
+ useDataFileContext,
245
+ useDataFileCore,
246
+ useDataFileData,
247
+ useDataFileUpdater
248
+ });
package/dist/data.mjs ADDED
@@ -0,0 +1,225 @@
1
+ // src/data.tsx
2
+ import { promises as fs } from "fs";
3
+ import {
4
+ useMemo,
5
+ useContext,
6
+ createContext,
7
+ useState,
8
+ useEffect,
9
+ useRef
10
+ } from "react";
11
+ import { throttle } from "lodash";
12
+ import { dirname } from "path";
13
+ import { Fragment, jsx } from "react/jsx-runtime";
14
+ function useDataFileData(dataFile) {
15
+ return useContext(dataFile.context).data;
16
+ }
17
+ function useDataFileUpdater(dataFile) {
18
+ return useContext(dataFile.context).updateData;
19
+ }
20
+ function useDataFileContext(dataFile) {
21
+ return useContext(dataFile.context);
22
+ }
23
+ function useDataFile(dataFile, usage) {
24
+ return dataFile.useDataFile(usage);
25
+ }
26
+ function useDataFileCore({
27
+ schema,
28
+ defaultValue,
29
+ path,
30
+ onPathChange = "defaultValue"
31
+ }) {
32
+ const state = useRef({
33
+ initialized: false,
34
+ path: null,
35
+ data: void 0,
36
+ previousData: void 0,
37
+ state: {
38
+ state: "saved"
39
+ }
40
+ });
41
+ useEffect(() => {
42
+ if (!state.current.initialized) {
43
+ state.current.initialized = true;
44
+ } else {
45
+ throw new Error(
46
+ "Cannot change schema or defaultValue after initialization"
47
+ );
48
+ }
49
+ }, [schema, defaultValue]);
50
+ const [data, setData] = useState({
51
+ status: "loading",
52
+ data: void 0
53
+ });
54
+ const updateDataFromState = useMemo(
55
+ () => () => {
56
+ const data2 = state.current.data;
57
+ if (state.current.state.state === "error") {
58
+ setData({
59
+ status: "error",
60
+ error: state.current.state.error,
61
+ data: data2
62
+ });
63
+ return;
64
+ }
65
+ if (data2 === void 0) {
66
+ setData({
67
+ status: "loading",
68
+ data: void 0
69
+ });
70
+ return;
71
+ }
72
+ setData({
73
+ status: "ready",
74
+ data: data2
75
+ });
76
+ },
77
+ []
78
+ );
79
+ const saveData = useMemo(
80
+ () => throttle(
81
+ async () => {
82
+ if (state.current.state.state === "saved") {
83
+ return;
84
+ }
85
+ const currentPath = state.current.path;
86
+ const currentData = state.current.data;
87
+ if (!currentPath || currentData === void 0) {
88
+ return;
89
+ }
90
+ try {
91
+ const json = JSON.stringify(currentData, null, 2);
92
+ await fs.mkdir(dirname(currentPath), { recursive: true });
93
+ await fs.writeFile(currentPath, json, "utf8");
94
+ if (state.current.path === currentPath && state.current.data === currentData) {
95
+ state.current.state = { state: "saved" };
96
+ }
97
+ } catch (error) {
98
+ if (state.current.path === currentPath && state.current.data === currentData) {
99
+ state.current.state = { state: "error", error };
100
+ updateDataFromState();
101
+ }
102
+ }
103
+ },
104
+ 500,
105
+ {
106
+ // Write leading so that we always write to disk quickly when
107
+ // only single things have changed
108
+ leading: true,
109
+ // Trailing is important otherwise we may lose data
110
+ trailing: true
111
+ }
112
+ ),
113
+ []
114
+ );
115
+ useEffect(() => {
116
+ state.current = {
117
+ ...state.current,
118
+ path,
119
+ data: void 0,
120
+ previousData: state.current.data ?? state.current.previousData,
121
+ state: {
122
+ state: "saved"
123
+ }
124
+ };
125
+ fs.readFile(path, "utf8").then((data2) => {
126
+ const parsedData = schema.parse(JSON.parse(data2));
127
+ if (state.current.path === path) {
128
+ state.current.data = parsedData;
129
+ state.current.state = { state: "saved" };
130
+ updateDataFromState();
131
+ }
132
+ }).catch((error) => {
133
+ if (state.current.path !== path) {
134
+ return;
135
+ }
136
+ if (error.code === "ENOENT") {
137
+ console.log("Creating new file");
138
+ const initialData = onPathChange === "transfer" && state.current.previousData !== void 0 ? state.current.previousData : defaultValue;
139
+ state.current.data = initialData;
140
+ state.current.state = { state: "dirty" };
141
+ saveData();
142
+ updateDataFromState();
143
+ return;
144
+ }
145
+ state.current.state = { state: "error", error };
146
+ updateDataFromState();
147
+ });
148
+ }, [path, onPathChange]);
149
+ const updateData = useMemo(
150
+ () => (update) => {
151
+ if (state.current.path !== path) {
152
+ return;
153
+ }
154
+ if (state.current.data === void 0) {
155
+ throw new Error("Attempt to update data before it has been loaded");
156
+ }
157
+ state.current.data = update(state.current.data);
158
+ state.current.state = { state: "dirty" };
159
+ saveData();
160
+ updateDataFromState();
161
+ },
162
+ [path]
163
+ );
164
+ return {
165
+ data,
166
+ updateData,
167
+ saveData
168
+ };
169
+ }
170
+ function createDataFileDefinition({
171
+ schema,
172
+ defaultValue
173
+ }) {
174
+ const context = createContext({
175
+ data: defaultValue,
176
+ updateData: () => {
177
+ throw new Error("Data file provider not used");
178
+ },
179
+ saveData: () => {
180
+ throw new Error("Data file provider not used");
181
+ },
182
+ error: void 0
183
+ });
184
+ const useDataFile2 = ({
185
+ path,
186
+ onPathChange
187
+ }) => useDataFileCore({
188
+ schema,
189
+ defaultValue,
190
+ path,
191
+ onPathChange
192
+ });
193
+ const Provider = ({ path, onPathChange, children }) => {
194
+ const { data, updateData, saveData } = useDataFile2({
195
+ path,
196
+ onPathChange
197
+ });
198
+ const providedContext = useMemo(
199
+ () => ({
200
+ data: data.status !== "loading" && data.data !== void 0 ? data.data : defaultValue,
201
+ updateData,
202
+ saveData,
203
+ error: data.status === "error" ? data.error : void 0
204
+ }),
205
+ [data, updateData]
206
+ );
207
+ if (data.status === "loading") {
208
+ return /* @__PURE__ */ jsx(Fragment, { children: "Loading..." });
209
+ }
210
+ return /* @__PURE__ */ jsx(context.Provider, { value: providedContext, children });
211
+ };
212
+ return {
213
+ Provider,
214
+ context,
215
+ useDataFile: useDataFile2
216
+ };
217
+ }
218
+ export {
219
+ createDataFileDefinition,
220
+ useDataFile,
221
+ useDataFileContext,
222
+ useDataFileCore,
223
+ useDataFileData,
224
+ useDataFileUpdater
225
+ };