@firtoz/router-toolkit 7.0.2 → 8.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 +5 -1
- package/dist/ConcurrentSubmitterProvider.d.ts +41 -0
- package/dist/ConcurrentSubmitterProvider.js +3 -0
- package/dist/ConcurrentSubmitterProvider.js.map +1 -0
- package/dist/chunk-2RLEUOSR.js +3 -0
- package/dist/chunk-2RLEUOSR.js.map +1 -0
- package/dist/chunk-2W5QFQTE.js +3 -0
- package/dist/chunk-2W5QFQTE.js.map +1 -0
- package/dist/chunk-5MOCOBGV.js +45 -0
- package/dist/chunk-5MOCOBGV.js.map +1 -0
- package/dist/chunk-HX57TC2S.js +33 -0
- package/dist/chunk-HX57TC2S.js.map +1 -0
- package/dist/chunk-JJN6GBJL.js +55 -0
- package/dist/chunk-JJN6GBJL.js.map +1 -0
- package/dist/chunk-MKH4ZP5U.js +3 -0
- package/dist/chunk-MKH4ZP5U.js.map +1 -0
- package/dist/chunk-OQGTU34H.js +44 -0
- package/dist/chunk-OQGTU34H.js.map +1 -0
- package/dist/chunk-QZDEBX3D.js +71 -0
- package/dist/chunk-QZDEBX3D.js.map +1 -0
- package/dist/chunk-R6HEJ5E5.js +3 -0
- package/dist/chunk-R6HEJ5E5.js.map +1 -0
- package/dist/chunk-S4QCJFVR.js +3 -0
- package/dist/chunk-S4QCJFVR.js.map +1 -0
- package/dist/chunk-W5R7YYHR.js +16 -0
- package/dist/chunk-W5R7YYHR.js.map +1 -0
- package/dist/chunk-YYJDSKJG.js +3 -0
- package/dist/chunk-YYJDSKJG.js.map +1 -0
- package/dist/chunk-ZC6U3MIB.js +189 -0
- package/dist/chunk-ZC6U3MIB.js.map +1 -0
- package/dist/chunk-ZMRGBYFH.js +3 -0
- package/dist/chunk-ZMRGBYFH.js.map +1 -0
- package/dist/formAction.d.ts +290 -0
- package/dist/formAction.js +3 -0
- package/dist/formAction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/types/Func.d.ts +3 -0
- package/dist/types/Func.js +3 -0
- package/dist/types/Func.js.map +1 -0
- package/dist/types/HrefArgs.d.ts +8 -0
- package/dist/types/HrefArgs.js +3 -0
- package/dist/types/HrefArgs.js.map +1 -0
- package/dist/types/RegisterPages.d.ts +11 -0
- package/dist/types/RegisterPages.js +3 -0
- package/dist/types/RegisterPages.js.map +1 -0
- package/dist/types/RoutePath.d.ts +6 -0
- package/dist/types/RoutePath.js +3 -0
- package/dist/types/RoutePath.js.map +1 -0
- package/dist/types/RouteWithActionModule.d.ts +18 -0
- package/dist/types/RouteWithActionModule.js +3 -0
- package/dist/types/RouteWithActionModule.js.map +1 -0
- package/dist/types/RouteWithLoaderModule.d.ts +10 -0
- package/dist/types/RouteWithLoaderModule.js +3 -0
- package/dist/types/RouteWithLoaderModule.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/useCachedFetch.d.ts +13 -0
- package/dist/useCachedFetch.js +3 -0
- package/dist/useCachedFetch.js.map +1 -0
- package/dist/useConcurrentSubmitter.d.ts +34 -0
- package/dist/useConcurrentSubmitter.js +4 -0
- package/dist/useConcurrentSubmitter.js.map +1 -0
- package/dist/useDynamicFetcher.d.ts +219 -0
- package/dist/useDynamicFetcher.js +3 -0
- package/dist/useDynamicFetcher.js.map +1 -0
- package/dist/useDynamicSubmitter.d.ts +256 -0
- package/dist/useDynamicSubmitter.js +3 -0
- package/dist/useDynamicSubmitter.js.map +1 -0
- package/dist/useFetcherStateChanged.d.ts +10 -0
- package/dist/useFetcherStateChanged.js +3 -0
- package/dist/useFetcherStateChanged.js.map +1 -0
- package/package.json +25 -16
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { createContext, useRef, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { href, useFetcher } from 'react-router';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/ConcurrentSubmitterProvider.tsx
|
|
6
|
+
function buildActionUrl(path, args) {
|
|
7
|
+
return args ? href(path, args) : href(path);
|
|
8
|
+
}
|
|
9
|
+
var ConcurrentSubmitterContext = createContext(
|
|
10
|
+
null
|
|
11
|
+
);
|
|
12
|
+
function FetcherRunner({
|
|
13
|
+
id,
|
|
14
|
+
pendingSubmit,
|
|
15
|
+
onSettle
|
|
16
|
+
}) {
|
|
17
|
+
const fetcher = useFetcher({ key: id });
|
|
18
|
+
const submittedRef = useRef(false);
|
|
19
|
+
const settledRef = useRef(false);
|
|
20
|
+
const prevStateRef = useRef(fetcher.state);
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
if (!pendingSubmit || submittedRef.current) return;
|
|
23
|
+
submittedRef.current = true;
|
|
24
|
+
if (pendingSubmit.kind === "json") {
|
|
25
|
+
fetcher.submit(pendingSubmit.data, {
|
|
26
|
+
action: pendingSubmit.actionUrl,
|
|
27
|
+
method: pendingSubmit.method,
|
|
28
|
+
encType: "application/json"
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
fetcher.submit(pendingSubmit.formData, {
|
|
32
|
+
action: pendingSubmit.actionUrl,
|
|
33
|
+
method: pendingSubmit.method,
|
|
34
|
+
encType: "multipart/form-data"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}, [pendingSubmit, fetcher.submit]);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
const wasWorking = prevStateRef.current === "submitting" || prevStateRef.current === "loading";
|
|
40
|
+
prevStateRef.current = fetcher.state;
|
|
41
|
+
if (wasWorking && fetcher.state === "idle" && !settledRef.current) {
|
|
42
|
+
settledRef.current = true;
|
|
43
|
+
if (fetcher.data !== void 0) {
|
|
44
|
+
onSettle(id, fetcher.data, void 0);
|
|
45
|
+
} else {
|
|
46
|
+
const err = fetcher.error ?? new Error("Submission failed");
|
|
47
|
+
onSettle(id, void 0, err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [id, fetcher.state, fetcher.data, onSettle]);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function ConcurrentSubmitterProvider({
|
|
54
|
+
children
|
|
55
|
+
}) {
|
|
56
|
+
const nextIdRef = useRef(0);
|
|
57
|
+
const [operationsState, setOperationsState] = useState({});
|
|
58
|
+
const onSettle = useCallback(
|
|
59
|
+
(id, data, error) => {
|
|
60
|
+
setOperationsState((prev) => {
|
|
61
|
+
const op = prev[id];
|
|
62
|
+
if (!op) return prev;
|
|
63
|
+
const { resolve, reject, pendingSubmit: _p, ...rest } = op;
|
|
64
|
+
if (error !== void 0) {
|
|
65
|
+
reject(error);
|
|
66
|
+
return {
|
|
67
|
+
...prev,
|
|
68
|
+
[id]: {
|
|
69
|
+
...rest,
|
|
70
|
+
status: "error",
|
|
71
|
+
error,
|
|
72
|
+
resolve,
|
|
73
|
+
reject
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
resolve(data);
|
|
78
|
+
return {
|
|
79
|
+
...prev,
|
|
80
|
+
[id]: {
|
|
81
|
+
...rest,
|
|
82
|
+
status: "done",
|
|
83
|
+
data,
|
|
84
|
+
resolve,
|
|
85
|
+
reject
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
[]
|
|
91
|
+
);
|
|
92
|
+
const addJsonSubmission = useCallback(
|
|
93
|
+
(path, args, data, options = {}) => {
|
|
94
|
+
const id = `op-${++nextIdRef.current}`;
|
|
95
|
+
const method = options.method ?? "POST";
|
|
96
|
+
const actionUrl = buildActionUrl(path, args);
|
|
97
|
+
let resolve;
|
|
98
|
+
let reject;
|
|
99
|
+
const promise = new Promise((res, rej) => {
|
|
100
|
+
resolve = res;
|
|
101
|
+
reject = rej;
|
|
102
|
+
});
|
|
103
|
+
const pendingSubmit = {
|
|
104
|
+
kind: "json",
|
|
105
|
+
actionUrl,
|
|
106
|
+
method,
|
|
107
|
+
encType: "application/json",
|
|
108
|
+
data
|
|
109
|
+
};
|
|
110
|
+
const op = {
|
|
111
|
+
id,
|
|
112
|
+
status: "pending",
|
|
113
|
+
submittedData: data,
|
|
114
|
+
resolve,
|
|
115
|
+
reject,
|
|
116
|
+
pendingSubmit
|
|
117
|
+
};
|
|
118
|
+
setOperationsState((prev) => ({ ...prev, [id]: op }));
|
|
119
|
+
return { id, promise };
|
|
120
|
+
},
|
|
121
|
+
[]
|
|
122
|
+
);
|
|
123
|
+
const addFormSubmission = useCallback(
|
|
124
|
+
(path, args, formData, submittedData, options = {}) => {
|
|
125
|
+
const id = `op-${++nextIdRef.current}`;
|
|
126
|
+
const method = options.method ?? "POST";
|
|
127
|
+
const actionUrl = buildActionUrl(path, args);
|
|
128
|
+
let resolve;
|
|
129
|
+
let reject;
|
|
130
|
+
const promise = new Promise((res, rej) => {
|
|
131
|
+
resolve = res;
|
|
132
|
+
reject = rej;
|
|
133
|
+
});
|
|
134
|
+
const pendingSubmit = {
|
|
135
|
+
kind: "form",
|
|
136
|
+
actionUrl,
|
|
137
|
+
method,
|
|
138
|
+
encType: "multipart/form-data",
|
|
139
|
+
formData
|
|
140
|
+
};
|
|
141
|
+
const op = {
|
|
142
|
+
id,
|
|
143
|
+
status: "pending",
|
|
144
|
+
submittedData,
|
|
145
|
+
resolve,
|
|
146
|
+
reject,
|
|
147
|
+
pendingSubmit
|
|
148
|
+
};
|
|
149
|
+
setOperationsState((prev) => ({ ...prev, [id]: op }));
|
|
150
|
+
return { id, promise };
|
|
151
|
+
},
|
|
152
|
+
[]
|
|
153
|
+
);
|
|
154
|
+
const operations = useMemo(() => {
|
|
155
|
+
const result = {};
|
|
156
|
+
for (const [k, v] of Object.entries(operationsState)) {
|
|
157
|
+
const { resolve: _r, reject: _j, pendingSubmit: _p, ...rest } = v;
|
|
158
|
+
result[k] = rest;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}, [operationsState]);
|
|
162
|
+
const value = useMemo(
|
|
163
|
+
() => ({
|
|
164
|
+
operations,
|
|
165
|
+
addJsonSubmission,
|
|
166
|
+
addFormSubmission,
|
|
167
|
+
onSettle
|
|
168
|
+
}),
|
|
169
|
+
[operations, addJsonSubmission, addFormSubmission, onSettle]
|
|
170
|
+
);
|
|
171
|
+
return /* @__PURE__ */ jsxs(ConcurrentSubmitterContext.Provider, { value, children: [
|
|
172
|
+
children,
|
|
173
|
+
Object.entries(operationsState).map(
|
|
174
|
+
([id, op]) => op.pendingSubmit && /* @__PURE__ */ jsx(
|
|
175
|
+
FetcherRunner,
|
|
176
|
+
{
|
|
177
|
+
id,
|
|
178
|
+
pendingSubmit: op.pendingSubmit,
|
|
179
|
+
onSettle
|
|
180
|
+
},
|
|
181
|
+
id
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider };
|
|
188
|
+
//# sourceMappingURL=chunk-ZC6U3MIB.js.map
|
|
189
|
+
//# sourceMappingURL=chunk-ZC6U3MIB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ConcurrentSubmitterProvider.tsx"],"names":[],"mappings":"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","file":"chunk-ZC6U3MIB.js","sourcesContent":["/**\n * @fileoverview Global provider for concurrent form submissions via React Router fetchers.\n *\n * Mounts components that use useFetcher with unique keys so each submission goes through\n * the framework (correct .data URL and turbo-stream decoding). Path/args are per submission.\n *\n * @example\n * ```tsx\n * // Root:\n * <ConcurrentSubmitterProvider>\n * <Outlet />\n * </ConcurrentSubmitterProvider>\n *\n * // Anywhere: use useConcurrentSubmitter() from \"./useConcurrentSubmitter\"\n * ```\n */\n\nimport React, {\n\tuseCallback,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n\tcreateContext,\n} from \"react\";\nimport { href, useFetcher } from \"react-router\";\nimport type { SubmitTarget } from \"react-router\";\n\n/** Status of a single concurrent submission */\nexport type OperationStatus = \"pending\" | \"done\" | \"error\";\n\n/** One tracked submission: pending → done (or error). Includes submitted payload for optimistic UI. */\nexport type Operation<TResponse = unknown, TFormData = unknown> = {\n\tid: string;\n\tstatus: OperationStatus;\n\t/** Data that was sent (for optimistic display while pending, or to show what was submitted) */\n\tsubmittedData: TFormData;\n\t/** Response from the action when status is \"done\" */\n\tdata?: TResponse;\n\terror?: unknown;\n};\n\n/** Result of submitJson / submitFormData: id to look up in operations, promise to await */\nexport type SubmitJsonResult<T> = {\n\tid: string;\n\tpromise: Promise<T>;\n};\n\n/** Optional serializable payload for FormData submissions (for display in operations list). */\nexport type FormDataSubmittedData = Record<string, unknown>;\n\nexport type SubmitJsonOptions = {\n\tmethod?: \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n};\n\nexport type SubmitFormDataOptions = {\n\theaders?: HeadersInit;\n\tmethod?: \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n};\n\ntype PendingJson = {\n\tkind: \"json\";\n\tactionUrl: string;\n\tmethod: string;\n\tencType: \"application/json\";\n\tdata: unknown;\n};\n\ntype PendingForm = {\n\tkind: \"form\";\n\tactionUrl: string;\n\tmethod: string;\n\tencType: \"multipart/form-data\";\n\tformData: FormData;\n};\n\ntype PendingSubmit = PendingJson | PendingForm;\n\ntype OperationState = Operation<unknown, unknown> & {\n\tresolve: (value: unknown) => void;\n\treject: (error: unknown) => void;\n\tpendingSubmit?: PendingSubmit;\n};\n\nfunction buildActionUrl(path: string, args?: Record<string, string>): string {\n\t// biome-ignore lint/suspicious/noExplicitAny: path is dynamic from caller\n\treturn args ? href(path as any, args as any) : (href(path as any) as string);\n}\n\ntype ContextValue = {\n\toperations: Record<string, Operation<unknown, unknown>>;\n\taddJsonSubmission: (\n\t\tpath: string,\n\t\targs: Record<string, string> | undefined,\n\t\tdata: unknown,\n\t\toptions?: SubmitJsonOptions,\n\t) => SubmitJsonResult<unknown>;\n\taddFormSubmission: (\n\t\tpath: string,\n\t\targs: Record<string, string> | undefined,\n\t\tformData: FormData,\n\t\tsubmittedData: FormDataSubmittedData,\n\t\toptions?: SubmitFormDataOptions,\n\t) => SubmitJsonResult<unknown>;\n\tonSettle: (id: string, data?: unknown, error?: unknown) => void;\n};\n\nexport const ConcurrentSubmitterContext = createContext<ContextValue | null>(\n\tnull,\n);\n\nfunction FetcherRunner({\n\tid,\n\tpendingSubmit,\n\tonSettle,\n}: {\n\tid: string;\n\tpendingSubmit: PendingSubmit;\n\tonSettle: (id: string, data?: unknown, error?: unknown) => void;\n}) {\n\tconst fetcher = useFetcher({ key: id });\n\tconst submittedRef = useRef(false);\n\tconst settledRef = useRef(false);\n\tconst prevStateRef = useRef(fetcher.state);\n\n\tReact.useEffect(() => {\n\t\tif (!pendingSubmit || submittedRef.current) return;\n\t\tsubmittedRef.current = true;\n\n\t\tif (pendingSubmit.kind === \"json\") {\n\t\t\tfetcher.submit(pendingSubmit.data as SubmitTarget, {\n\t\t\t\taction: pendingSubmit.actionUrl,\n\t\t\t\tmethod: pendingSubmit.method as \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\",\n\t\t\t\tencType: \"application/json\",\n\t\t\t});\n\t\t} else {\n\t\t\tfetcher.submit(pendingSubmit.formData, {\n\t\t\t\taction: pendingSubmit.actionUrl,\n\t\t\t\tmethod: pendingSubmit.method as \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\",\n\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t});\n\t\t}\n\t}, [pendingSubmit, fetcher.submit]);\n\n\tReact.useEffect(() => {\n\t\tconst wasWorking =\n\t\t\tprevStateRef.current === \"submitting\" ||\n\t\t\tprevStateRef.current === \"loading\";\n\t\tprevStateRef.current = fetcher.state;\n\n\t\tif (wasWorking && fetcher.state === \"idle\" && !settledRef.current) {\n\t\t\tsettledRef.current = true;\n\t\t\tif (fetcher.data !== undefined) {\n\t\t\t\tonSettle(id, fetcher.data, undefined);\n\t\t\t} else {\n\t\t\t\tconst err =\n\t\t\t\t\t(fetcher as { error?: unknown }).error ??\n\t\t\t\t\tnew Error(\"Submission failed\");\n\t\t\t\tonSettle(id, undefined, err);\n\t\t\t}\n\t\t}\n\t}, [id, fetcher.state, fetcher.data, onSettle]);\n\n\treturn null;\n}\n\nexport function ConcurrentSubmitterProvider({\n\tchildren,\n}: {\n\tchildren: React.ReactNode;\n}) {\n\tconst nextIdRef = useRef(0);\n\tconst [operationsState, setOperationsState] = useState<\n\t\tRecord<string, OperationState>\n\t>({});\n\n\tconst onSettle = useCallback(\n\t\t(id: string, data?: unknown, error?: unknown) => {\n\t\t\tsetOperationsState((prev) => {\n\t\t\t\tconst op = prev[id];\n\t\t\t\tif (!op) return prev;\n\t\t\t\tconst { resolve, reject, pendingSubmit: _p, ...rest } = op;\n\t\t\t\tif (error !== undefined) {\n\t\t\t\t\treject(error);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...prev,\n\t\t\t\t\t\t[id]: {\n\t\t\t\t\t\t\t...rest,\n\t\t\t\t\t\t\tstatus: \"error\" as OperationStatus,\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\tresolve,\n\t\t\t\t\t\t\treject,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tresolve(data);\n\t\t\t\treturn {\n\t\t\t\t\t...prev,\n\t\t\t\t\t[id]: {\n\t\t\t\t\t\t...rest,\n\t\t\t\t\t\tstatus: \"done\" as OperationStatus,\n\t\t\t\t\t\tdata,\n\t\t\t\t\t\tresolve,\n\t\t\t\t\t\treject,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\t[],\n\t);\n\n\tconst addJsonSubmission = useCallback(\n\t\t(\n\t\t\tpath: string,\n\t\t\targs: Record<string, string> | undefined,\n\t\t\tdata: unknown,\n\t\t\toptions: SubmitJsonOptions = {},\n\t\t): SubmitJsonResult<unknown> => {\n\t\t\tconst id = `op-${++nextIdRef.current}`;\n\t\t\tconst method = options.method ?? \"POST\";\n\t\t\tconst actionUrl = buildActionUrl(path, args);\n\n\t\t\tlet resolve!: (value: unknown) => void;\n\t\t\tlet reject!: (error: unknown) => void;\n\t\t\tconst promise = new Promise<unknown>((res, rej) => {\n\t\t\t\tresolve = res;\n\t\t\t\treject = rej;\n\t\t\t});\n\n\t\t\tconst pendingSubmit: PendingJson = {\n\t\t\t\tkind: \"json\",\n\t\t\t\tactionUrl,\n\t\t\t\tmethod,\n\t\t\t\tencType: \"application/json\",\n\t\t\t\tdata,\n\t\t\t};\n\n\t\t\tconst op: OperationState = {\n\t\t\t\tid,\n\t\t\t\tstatus: \"pending\",\n\t\t\t\tsubmittedData: data as Record<string, unknown>,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t\tpendingSubmit,\n\t\t\t};\n\n\t\t\tsetOperationsState((prev) => ({ ...prev, [id]: op }));\n\t\t\treturn { id, promise };\n\t\t},\n\t\t[],\n\t);\n\n\tconst addFormSubmission = useCallback(\n\t\t(\n\t\t\tpath: string,\n\t\t\targs: Record<string, string> | undefined,\n\t\t\tformData: FormData,\n\t\t\tsubmittedData: FormDataSubmittedData,\n\t\t\toptions: SubmitFormDataOptions = {},\n\t\t): SubmitJsonResult<unknown> => {\n\t\t\tconst id = `op-${++nextIdRef.current}`;\n\t\t\tconst method = options.method ?? \"POST\";\n\t\t\tconst actionUrl = buildActionUrl(path, args);\n\n\t\t\tlet resolve!: (value: unknown) => void;\n\t\t\tlet reject!: (error: unknown) => void;\n\t\t\tconst promise = new Promise<unknown>((res, rej) => {\n\t\t\t\tresolve = res;\n\t\t\t\treject = rej;\n\t\t\t});\n\n\t\t\tconst pendingSubmit: PendingForm = {\n\t\t\t\tkind: \"form\",\n\t\t\t\tactionUrl,\n\t\t\t\tmethod,\n\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t\tformData,\n\t\t\t};\n\n\t\t\tconst op: OperationState = {\n\t\t\t\tid,\n\t\t\t\tstatus: \"pending\",\n\t\t\t\tsubmittedData,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t\tpendingSubmit,\n\t\t\t};\n\n\t\t\tsetOperationsState((prev) => ({ ...prev, [id]: op }));\n\t\t\treturn { id, promise };\n\t\t},\n\t\t[],\n\t);\n\n\tconst operations = useMemo(() => {\n\t\tconst result: Record<string, Operation<unknown, unknown>> = {};\n\t\tfor (const [k, v] of Object.entries(operationsState)) {\n\t\t\tconst { resolve: _r, reject: _j, pendingSubmit: _p, ...rest } = v;\n\t\t\tresult[k] = rest;\n\t\t}\n\t\treturn result;\n\t}, [operationsState]);\n\n\tconst value = useMemo<ContextValue>(\n\t\t() => ({\n\t\t\toperations,\n\t\t\taddJsonSubmission,\n\t\t\taddFormSubmission,\n\t\t\tonSettle,\n\t\t}),\n\t\t[operations, addJsonSubmission, addFormSubmission, onSettle],\n\t);\n\n\treturn (\n\t\t<ConcurrentSubmitterContext.Provider value={value}>\n\t\t\t{children}\n\t\t\t{Object.entries(operationsState).map(\n\t\t\t\t([id, op]) =>\n\t\t\t\t\top.pendingSubmit && (\n\t\t\t\t\t\t<FetcherRunner\n\t\t\t\t\t\t\tkey={id}\n\t\t\t\t\t\t\tid={id}\n\t\t\t\t\t\t\tpendingSubmit={op.pendingSubmit}\n\t\t\t\t\t\t\tonSettle={onSettle}\n\t\t\t\t\t\t/>\n\t\t\t\t\t),\n\t\t\t)}\n\t\t</ConcurrentSubmitterContext.Provider>\n\t);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-ZMRGBYFH.js"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { MaybeError } from '@firtoz/maybe-error';
|
|
2
|
+
import { ActionFunctionArgs } from 'react-router';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Type-safe form action utility for React Router 7
|
|
7
|
+
*
|
|
8
|
+
* This module provides a wrapper for React Router actions that handles form data and JSON
|
|
9
|
+
* validation using Zod schemas and provides structured error handling with MaybeError.
|
|
10
|
+
*
|
|
11
|
+
* Supports both:
|
|
12
|
+
* - **JSON requests** (`Content-Type: application/json`) - parsed with `request.json()` and validated directly
|
|
13
|
+
* - **FormData requests** (`multipart/form-data` or `application/x-www-form-urlencoded`) - parsed with `request.formData()` and validated with zod-form-data
|
|
14
|
+
*
|
|
15
|
+
* ## Overview
|
|
16
|
+
*
|
|
17
|
+
* `formAction` is designed to work seamlessly with `useDynamicSubmitter` and `useDynamicFetcher`
|
|
18
|
+
* to provide end-to-end type safety for your React Router forms.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ### Basic Route Setup (`app/routes/auth.login.tsx`)
|
|
22
|
+
*
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { z } from "zod";
|
|
25
|
+
* import { formAction, type RoutePath } from "@firtoz/router-toolkit";
|
|
26
|
+
* import { success, fail } from "@firtoz/maybe-error";
|
|
27
|
+
*
|
|
28
|
+
* // 1. Export the route path for type inference
|
|
29
|
+
* export const route: RoutePath<"/auth/login"> = "/auth/login";
|
|
30
|
+
*
|
|
31
|
+
* // 2. Define your form schema with Zod
|
|
32
|
+
* export const formSchema = z.object({
|
|
33
|
+
* email: z.string().email("Please enter a valid email"),
|
|
34
|
+
* password: z.string().min(8, "Password must be at least 8 characters"),
|
|
35
|
+
* rememberMe: z.boolean().optional().default(false),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // 3. Create the action with formAction
|
|
39
|
+
* export const action = formAction({
|
|
40
|
+
* schema: formSchema,
|
|
41
|
+
* handler: async ({ request }, data) => {
|
|
42
|
+
* // data is fully typed: { email: string, password: string, rememberMe: boolean }
|
|
43
|
+
* try {
|
|
44
|
+
* const user = await authenticateUser(data.email, data.password);
|
|
45
|
+
* if (data.rememberMe) {
|
|
46
|
+
* await createPersistentSession(user.id);
|
|
47
|
+
* }
|
|
48
|
+
* return success({ user });
|
|
49
|
+
* } catch (error) {
|
|
50
|
+
* return fail("Invalid email or password");
|
|
51
|
+
* }
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ### Using with useDynamicSubmitter
|
|
58
|
+
*
|
|
59
|
+
* The route above can be used with `useDynamicSubmitter` for type-safe form submissions:
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { useDynamicSubmitter } from "@firtoz/router-toolkit";
|
|
63
|
+
*
|
|
64
|
+
* function LoginForm() {
|
|
65
|
+
* const submitter = useDynamicSubmitter<typeof import("./auth.login")>("/auth/login");
|
|
66
|
+
*
|
|
67
|
+
* // Option 1: Submit as JSON (defaults to POST)
|
|
68
|
+
* const handleLoginJson = async () => {
|
|
69
|
+
* await submitter.submitJson({
|
|
70
|
+
* email: "user@example.com",
|
|
71
|
+
* password: "secret123",
|
|
72
|
+
* rememberMe: true,
|
|
73
|
+
* });
|
|
74
|
+
* };
|
|
75
|
+
*
|
|
76
|
+
* // Option 2: Use the Form component (defaults to POST)
|
|
77
|
+
* return (
|
|
78
|
+
* <submitter.Form>
|
|
79
|
+
* <input name="email" type="email" placeholder="Email" />
|
|
80
|
+
* <input name="password" type="password" placeholder="Password" />
|
|
81
|
+
* <label>
|
|
82
|
+
* <input name="rememberMe" type="checkbox" /> Remember me
|
|
83
|
+
* </label>
|
|
84
|
+
* <button disabled={submitter.state !== "idle"}>
|
|
85
|
+
* {submitter.state === "submitting" ? "Logging in..." : "Login"}
|
|
86
|
+
* </button>
|
|
87
|
+
*
|
|
88
|
+
* {submitter.data && !submitter.data.success && (
|
|
89
|
+
* <div className="error">
|
|
90
|
+
* {submitter.data.error.type === "validation"
|
|
91
|
+
* ? "Please check your inputs"
|
|
92
|
+
* : submitter.data.error.type === "handler"
|
|
93
|
+
* ? submitter.data.error.error // "Invalid email or password"
|
|
94
|
+
* : "An unexpected error occurred"}
|
|
95
|
+
* </div>
|
|
96
|
+
* )}
|
|
97
|
+
* </submitter.Form>
|
|
98
|
+
* );
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ### Combined loader + action route (`app/routes/admin.posts.$id.tsx`)
|
|
104
|
+
*
|
|
105
|
+
* You can combine `formAction` with a loader for full CRUD operations:
|
|
106
|
+
*
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { z } from "zod";
|
|
109
|
+
* import { formAction, type RoutePath } from "@firtoz/router-toolkit";
|
|
110
|
+
* import { success, fail } from "@firtoz/maybe-error";
|
|
111
|
+
* import type { LoaderFunctionArgs } from "react-router";
|
|
112
|
+
*
|
|
113
|
+
* export const route: RoutePath<"/admin/posts/:id"> = "/admin/posts/:id";
|
|
114
|
+
*
|
|
115
|
+
* // Loader for fetching data (used with useDynamicFetcher)
|
|
116
|
+
* export const loader = async ({ params }: LoaderFunctionArgs) => {
|
|
117
|
+
* const post = await db.posts.findUnique({ where: { id: params.id } });
|
|
118
|
+
* return { post };
|
|
119
|
+
* };
|
|
120
|
+
*
|
|
121
|
+
* // Form schema for updates
|
|
122
|
+
* export const formSchema = z.object({
|
|
123
|
+
* title: z.string().min(1, "Title is required"),
|
|
124
|
+
* content: z.string().min(10, "Content must be at least 10 characters"),
|
|
125
|
+
* published: z.boolean().optional().default(false),
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* // Action for handling form submissions (used with useDynamicSubmitter)
|
|
129
|
+
* export const action = formAction({
|
|
130
|
+
* schema: formSchema,
|
|
131
|
+
* handler: async ({ params }, data) => {
|
|
132
|
+
* const updated = await db.posts.update({
|
|
133
|
+
* where: { id: params.id },
|
|
134
|
+
* data,
|
|
135
|
+
* });
|
|
136
|
+
* return success({ post: updated });
|
|
137
|
+
* },
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ### Full CRUD component using both hooks
|
|
143
|
+
*
|
|
144
|
+
* ```tsx
|
|
145
|
+
* import { useDynamicFetcher, useDynamicSubmitter } from "@firtoz/router-toolkit";
|
|
146
|
+
* import { useEffect } from "react";
|
|
147
|
+
*
|
|
148
|
+
* function PostEditor({ postId }: { postId: string }) {
|
|
149
|
+
* // Fetch post data
|
|
150
|
+
* const fetcher = useDynamicFetcher<typeof import("./admin.posts.$id")>(
|
|
151
|
+
* "/admin/posts/:id",
|
|
152
|
+
* { id: postId }
|
|
153
|
+
* );
|
|
154
|
+
*
|
|
155
|
+
* // Submit updates
|
|
156
|
+
* const submitter = useDynamicSubmitter<typeof import("./admin.posts.$id")>(
|
|
157
|
+
* "/admin/posts/:id",
|
|
158
|
+
* { id: postId }
|
|
159
|
+
* );
|
|
160
|
+
*
|
|
161
|
+
* useEffect(() => {
|
|
162
|
+
* fetcher.load();
|
|
163
|
+
* }, [fetcher.load]);
|
|
164
|
+
*
|
|
165
|
+
* if (fetcher.state === "loading" && !fetcher.data) {
|
|
166
|
+
* return <div>Loading...</div>;
|
|
167
|
+
* }
|
|
168
|
+
*
|
|
169
|
+
* const post = fetcher.data?.post;
|
|
170
|
+
*
|
|
171
|
+
* return (
|
|
172
|
+
* <submitter.Form method="PUT">
|
|
173
|
+
* <input name="title" defaultValue={post?.title} />
|
|
174
|
+
* <textarea name="content" defaultValue={post?.content} />
|
|
175
|
+
* <label>
|
|
176
|
+
* <input name="published" type="checkbox" defaultChecked={post?.published} />
|
|
177
|
+
* Published
|
|
178
|
+
* </label>
|
|
179
|
+
* <button disabled={submitter.state !== "idle"}>
|
|
180
|
+
* {submitter.state === "submitting" ? "Saving..." : "Save"}
|
|
181
|
+
* </button>
|
|
182
|
+
* </submitter.Form>
|
|
183
|
+
* );
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Error types that can be returned by formAction
|
|
190
|
+
*/
|
|
191
|
+
type FormActionError<TError, TSchema extends z.ZodTypeAny> = {
|
|
192
|
+
type: "validation";
|
|
193
|
+
error: ReturnType<typeof z.treeifyError<z.infer<TSchema>>>;
|
|
194
|
+
} | {
|
|
195
|
+
type: "handler";
|
|
196
|
+
error: TError;
|
|
197
|
+
} | {
|
|
198
|
+
type: "unknown";
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Configuration object for formAction
|
|
202
|
+
*
|
|
203
|
+
* @template TSchema - The Zod schema type for form validation
|
|
204
|
+
* @template TResult - The success result type from the handler
|
|
205
|
+
* @template TError - The error type that the handler can return
|
|
206
|
+
* @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)
|
|
207
|
+
*/
|
|
208
|
+
interface FormActionConfig<TSchema extends z.ZodTypeAny, TResult = undefined, TError = string, ActionArgs extends ActionFunctionArgs = ActionFunctionArgs> {
|
|
209
|
+
/**
|
|
210
|
+
* Zod schema to validate the form data against
|
|
211
|
+
*/
|
|
212
|
+
schema: TSchema;
|
|
213
|
+
/**
|
|
214
|
+
* Handler function that processes the validated form data
|
|
215
|
+
*
|
|
216
|
+
* @param args - The original action function arguments
|
|
217
|
+
* @param data - The validated form data (typed according to the schema)
|
|
218
|
+
* @returns A promise that resolves to a MaybeError with the result or error
|
|
219
|
+
*/
|
|
220
|
+
handler: (args: ActionArgs, data: z.infer<TSchema>) => Promise<MaybeError<TResult, TError>>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Creates a type-safe form action handler that validates form data or JSON and provides structured error handling.
|
|
224
|
+
*
|
|
225
|
+
* This function wraps a React Router action to:
|
|
226
|
+
* 1. Detect content type (JSON vs FormData) from the request headers
|
|
227
|
+
* 2. Parse and validate the request body using a Zod schema
|
|
228
|
+
* 3. Call the provided handler with validated data
|
|
229
|
+
* 4. Return structured errors for validation failures, handler errors, or unknown errors
|
|
230
|
+
* 5. Preserve React Router Response objects (redirects, etc.) by re-throwing them
|
|
231
|
+
*
|
|
232
|
+
* **Content-Type handling:**
|
|
233
|
+
* - `application/json`: Uses `request.json()` and validates directly with the schema
|
|
234
|
+
* - `multipart/form-data` or `application/x-www-form-urlencoded`: Uses `request.formData()` and validates with zod-form-data
|
|
235
|
+
*
|
|
236
|
+
* @template TSchema - The Zod schema type for form validation
|
|
237
|
+
* @template TResult - The success result type from the handler (defaults to undefined)
|
|
238
|
+
* @template TError - The error type that the handler can return (defaults to string)
|
|
239
|
+
* @template ActionArgs - The action function arguments type (defaults to ActionFunctionArgs)
|
|
240
|
+
*
|
|
241
|
+
* @param config - Configuration object containing schema and handler
|
|
242
|
+
* @returns An action function that can be used with React Router
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* import { z } from "zod";
|
|
247
|
+
* import { formAction } from "@firtoz/router-toolkit";
|
|
248
|
+
* import { success, fail } from "@firtoz/maybe-error";
|
|
249
|
+
*
|
|
250
|
+
* const loginSchema = z.object({
|
|
251
|
+
* email: z.string().email("Invalid email format"),
|
|
252
|
+
* password: z.string().min(8, "Password must be at least 8 characters"),
|
|
253
|
+
* });
|
|
254
|
+
*
|
|
255
|
+
* export const action = formAction({
|
|
256
|
+
* schema: loginSchema,
|
|
257
|
+
* handler: async (args, data) => {
|
|
258
|
+
* try {
|
|
259
|
+
* const user = await authenticateUser(data.email, data.password);
|
|
260
|
+
* return success(user);
|
|
261
|
+
* } catch (error) {
|
|
262
|
+
* return fail("Invalid credentials");
|
|
263
|
+
* }
|
|
264
|
+
* },
|
|
265
|
+
* });
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* // In your component, handle the different error types:
|
|
271
|
+
* const actionData = useActionData<typeof action>();
|
|
272
|
+
*
|
|
273
|
+
* if (actionData && !actionData.success) {
|
|
274
|
+
* switch (actionData.error.type) {
|
|
275
|
+
* case "validation":
|
|
276
|
+
* // Handle validation errors - actionData.error.error contains field-specific errors
|
|
277
|
+
* break;
|
|
278
|
+
* case "handler":
|
|
279
|
+
* // Handle business logic errors - actionData.error.error contains your custom error
|
|
280
|
+
* break;
|
|
281
|
+
* case "unknown":
|
|
282
|
+
* // Handle unexpected errors
|
|
283
|
+
* break;
|
|
284
|
+
* }
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
declare const formAction: <TSchema extends z.ZodTypeAny, TResult = undefined, TError = string, ActionArgs extends ActionFunctionArgs = ActionFunctionArgs>({ schema, handler, }: FormActionConfig<TSchema, TResult, TError, ActionArgs>) => (args: ActionArgs) => Promise<MaybeError<TResult, FormActionError<TError, TSchema>>>;
|
|
289
|
+
|
|
290
|
+
export { type FormActionConfig, type FormActionError, formAction };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"formAction.js"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider, FormDataSubmittedData, Operation, OperationStatus, SubmitFormDataOptions, SubmitJsonOptions, SubmitJsonResult } from './ConcurrentSubmitterProvider.js';
|
|
2
|
+
export { FormActionConfig, FormActionError, formAction } from './formAction.js';
|
|
3
|
+
export * from '@firtoz/maybe-error';
|
|
4
|
+
export { Func } from './types/Func.js';
|
|
5
|
+
export { HrefArgs } from './types/HrefArgs.js';
|
|
6
|
+
export { RegisterPages } from './types/RegisterPages.js';
|
|
7
|
+
export { RoutePath } from './types/RoutePath.js';
|
|
8
|
+
export { ActionResult, RouteWithActionModule } from './types/RouteWithActionModule.js';
|
|
9
|
+
export { RouteWithLoaderModule } from './types/RouteWithLoaderModule.js';
|
|
10
|
+
export { useCachedFetch } from './useCachedFetch.js';
|
|
11
|
+
export { UseConcurrentSubmitterReturn, useConcurrentSubmitter } from './useConcurrentSubmitter.js';
|
|
12
|
+
export { useDynamicFetcher } from './useDynamicFetcher.js';
|
|
13
|
+
export { useDynamicSubmitter } from './useDynamicSubmitter.js';
|
|
14
|
+
export { useFetcherStateChanged } from './useFetcherStateChanged.js';
|
|
15
|
+
import 'react/jsx-runtime';
|
|
16
|
+
import 'react';
|
|
17
|
+
import 'react-router';
|
|
18
|
+
import 'zod';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import './chunk-2RLEUOSR.js';
|
|
2
|
+
import './chunk-2W5QFQTE.js';
|
|
3
|
+
export { useCachedFetch } from './chunk-OQGTU34H.js';
|
|
4
|
+
export { useConcurrentSubmitter } from './chunk-QZDEBX3D.js';
|
|
5
|
+
export { useDynamicFetcher } from './chunk-HX57TC2S.js';
|
|
6
|
+
export { useDynamicSubmitter } from './chunk-JJN6GBJL.js';
|
|
7
|
+
export { useFetcherStateChanged } from './chunk-W5R7YYHR.js';
|
|
8
|
+
export { ConcurrentSubmitterContext, ConcurrentSubmitterProvider } from './chunk-ZC6U3MIB.js';
|
|
9
|
+
export { formAction } from './chunk-5MOCOBGV.js';
|
|
10
|
+
import './chunk-YYJDSKJG.js';
|
|
11
|
+
import './chunk-ZMRGBYFH.js';
|
|
12
|
+
import './chunk-MKH4ZP5U.js';
|
|
13
|
+
import './chunk-S4QCJFVR.js';
|
|
14
|
+
import './chunk-R6HEJ5E5.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Func.js"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RegisterPages } from './RegisterPages.js';
|
|
2
|
+
import 'react-router';
|
|
3
|
+
|
|
4
|
+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
|
|
5
|
+
type ToArgs<Params extends Record<string, string | undefined>> = Equal<Params, {}> extends true ? [] : Partial<Params> extends Params ? [Params] | [] : [Params];
|
|
6
|
+
type HrefArgs<T extends keyof RegisterPages> = ToArgs<RegisterPages[T]["params"]>;
|
|
7
|
+
|
|
8
|
+
export type { HrefArgs };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"HrefArgs.js"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Register } from 'react-router';
|
|
2
|
+
|
|
3
|
+
type AnyParams = Record<string, string | undefined>;
|
|
4
|
+
type AnyPages = Record<string, {
|
|
5
|
+
params: AnyParams;
|
|
6
|
+
}>;
|
|
7
|
+
type RegisterPages = Register extends {
|
|
8
|
+
pages: infer Registered extends AnyPages;
|
|
9
|
+
} ? Registered : AnyPages;
|
|
10
|
+
|
|
11
|
+
export type { RegisterPages };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"RegisterPages.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"RoutePath.js"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Func } from './Func.js';
|
|
2
|
+
import { RegisterPages } from './RegisterPages.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import 'react-router';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Route module shape for type-safe form actions: route path, action handler, and form schema.
|
|
8
|
+
* Use with useDynamicSubmitter and useConcurrentSubmitter.
|
|
9
|
+
*/
|
|
10
|
+
type RouteWithActionModule = {
|
|
11
|
+
route: keyof RegisterPages;
|
|
12
|
+
action: Func;
|
|
13
|
+
formSchema: z.ZodType;
|
|
14
|
+
};
|
|
15
|
+
/** Action result type inferred from the route module's action */
|
|
16
|
+
type ActionResult<TModule extends RouteWithActionModule> = Awaited<ReturnType<TModule["action"]>>;
|
|
17
|
+
|
|
18
|
+
export type { ActionResult, RouteWithActionModule };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"RouteWithActionModule.js"}
|