@edgestore/react 0.0.0-alpha.12

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 ADDED
@@ -0,0 +1,86 @@
1
+ # Getting Started
2
+
3
+ ### Next.js Setup
4
+
5
+ #### Install
6
+
7
+ ```bash
8
+ npm install @edgestore/react
9
+ ```
10
+
11
+ #### Environment Variables
12
+
13
+ ```bash
14
+ # .env
15
+ EDGE_STORE_ACCESS_KEY=your-access-key
16
+ EDGE_STORE_SECRET_KEY=your-secret-key
17
+ ```
18
+
19
+ #### API Route
20
+
21
+ ```jsx
22
+ // pages/api/edgestore/[...edgestore].js
23
+ import EdgeStore from '@edgestore/react/next';
24
+
25
+ export default EdgeStore();
26
+ ```
27
+
28
+ #### Provider
29
+
30
+ ```jsx
31
+ // pages/_app.jsx
32
+ import { EdgeStoreProvider } from '@edgestore/react';
33
+
34
+ export default function App({ Component, pageProps }) {
35
+ return (
36
+ <EdgeStoreProvider>
37
+ <Component {...pageProps} />
38
+ </EdgeStoreProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Upload image
44
+
45
+ ```jsx
46
+ import { useEdgeStore } from '@edgestore/react';
47
+
48
+ const Page = () => {
49
+ const [file, setFile] = useState(null);
50
+ const { upload } = useEdgeStore();
51
+
52
+ return (
53
+ <div>
54
+ <input type="file" onChange={(e) => setFile(e.target.files[0])} />
55
+ <button
56
+ onClick={async () => {
57
+ await upload({
58
+ file,
59
+ key: 'path/to/image.jpg',
60
+ });
61
+ }}
62
+ >
63
+ Upload
64
+ </button>
65
+ </div>
66
+ );
67
+ };
68
+
69
+ export default Page;
70
+ ```
71
+
72
+ ### Show image
73
+
74
+ ```jsx
75
+ import { useEdgeStore } from '@edgestore/react';
76
+
77
+ const Page = () => {
78
+ const { getImgSrc } = useEdgeStore();
79
+
80
+ return (
81
+ <div>
82
+ <img src={getImgSrc('path/to/image.jpg')} />
83
+ </div>
84
+ );
85
+ };
86
+ ```
@@ -0,0 +1,36 @@
1
+ import { AnyRouter } from '@edgestore/server/core';
2
+ import * as React from 'react';
3
+ import { BucketFunctions } from './createNextProxy';
4
+ type EdgeStoreContextValue<TRouter extends AnyRouter> = {
5
+ edgestore: BucketFunctions<TRouter>;
6
+ /**
7
+ * In development, if this is a protected file, this function will add the token as a query param to the url.
8
+ * This is needed because third party cookies don't work with http urls.
9
+ */
10
+ getSrc: (url: string) => string;
11
+ };
12
+ export declare function createEdgeStoreProvider<TRouter extends AnyRouter>(opts?: {
13
+ /**
14
+ * The maximum number of concurrent uploads.
15
+ *
16
+ * Uploads will automatically be queued if this limit is reached.
17
+ *
18
+ * @default 5
19
+ */
20
+ maxConcurrentUploads?: number;
21
+ }): {
22
+ EdgeStoreProvider: ({ children, basePath, }: {
23
+ children: React.ReactNode;
24
+ /**
25
+ * In case your app is not hosted at the root of your domain, you can specify the base path here.
26
+ * If you set this, make sure to set the full path to the EdgeStore API.
27
+ * e.g. `/my-app/api/edgestore` or `https://example.com/my-app/api/edgestore`
28
+ *
29
+ * @example - If your app is hosted at `https://example.com/my-app`, you can set the `basePath` to `/my-app/api/edgestore`.
30
+ */
31
+ basePath?: string | undefined;
32
+ }) => JSX.Element;
33
+ useEdgeStore: () => EdgeStoreContextValue<TRouter>;
34
+ };
35
+ export {};
36
+ //# sourceMappingURL=contextProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextProvider.d.ts","sourceRoot":"","sources":["../src/contextProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAmB,MAAM,mBAAmB,CAAC;AAKrE,KAAK,qBAAqB,CAAC,OAAO,SAAS,SAAS,IAAI;IACtD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACpC;;;OAGG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,SAAS,EAAE,IAAI,CAAC,EAAE;IACxE;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;;kBAUa,MAAM,SAAS;QACzB;;;;;;WAMG;;;;EAgCN"}
@@ -0,0 +1,42 @@
1
+ /// <reference types="react" />
2
+ import { AnyRouter, InferBucketPathKeys, InferMetadataObject } from '@edgestore/server/core';
3
+ import { z } from 'zod';
4
+ export type BucketFunctions<TRouter extends AnyRouter> = {
5
+ [K in keyof TRouter['buckets']]: {
6
+ upload: (params: z.infer<TRouter['buckets'][K]['_def']['input']> extends object ? {
7
+ file: File;
8
+ input: z.infer<TRouter['buckets'][K]['_def']['input']>;
9
+ onProgressChange?: OnProgressChangeHandler;
10
+ options?: UploadOptions;
11
+ } : {
12
+ file: File;
13
+ onProgressChange?: OnProgressChangeHandler;
14
+ options?: UploadOptions;
15
+ }) => Promise<{
16
+ url: string;
17
+ thumbnailUrl: TRouter['buckets'][K]['_def']['type'] extends 'IMAGE' ? string | null : never;
18
+ size: number;
19
+ uploadedAt: Date;
20
+ metadata: InferMetadataObject<TRouter['buckets'][K]>;
21
+ path: {
22
+ [TKey in InferBucketPathKeys<TRouter['buckets'][K]>]: string;
23
+ };
24
+ }>;
25
+ delete: (params: {
26
+ url: string;
27
+ }) => Promise<{
28
+ success: boolean;
29
+ }>;
30
+ };
31
+ };
32
+ type OnProgressChangeHandler = (progress: number) => void;
33
+ type UploadOptions = {
34
+ replaceTargetUrl?: string;
35
+ };
36
+ export declare function createNextProxy<TRouter extends AnyRouter>({ apiPath, uploadingCountRef, maxConcurrentUploads, }: {
37
+ apiPath: string;
38
+ uploadingCountRef: React.MutableRefObject<number>;
39
+ maxConcurrentUploads?: number;
40
+ }): BucketFunctions<TRouter>;
41
+ export {};
42
+ //# sourceMappingURL=createNextProxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNextProxy.d.ts","sourceRoot":"","sources":["../src/createNextProxy.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,SAAS,IAAI;KACtD,CAAC,IAAI,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG;QAC/B,MAAM,EAAE,CACN,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,MAAM,GAClE;YACE,IAAI,EAAE,IAAI,CAAC;YACX,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACvD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,GACD;YACE,IAAI,EAAE,IAAI,CAAC;YACX,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,KACF,OAAO,CAAC;YACX,GAAG,EAAE,MAAM,CAAC;YACZ,YAAY,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,OAAO,GAC/D,MAAM,GAAG,IAAI,GACb,KAAK,CAAC;YACV,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,IAAI,CAAC;YACjB,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,EAAE;iBACH,IAAI,IAAI,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM;aAC7D,CAAC;SACH,CAAC,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC;YAC3C,OAAO,EAAE,OAAO,CAAC;SAClB,CAAC,CAAC;KACJ;CACF,CAAC;AAEF,KAAK,uBAAuB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAE1D,KAAK,aAAa,GAAG;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EAAE,EACzD,OAAO,EACP,iBAAiB,EACjB,oBAAwB,GACzB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,4BAiCA"}
@@ -0,0 +1,2 @@
1
+ export * from './contextProvider';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var React = require('react');
6
+
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n["default"] = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
+
27
+ class EdgeStoreError extends Error {
28
+ constructor(message){
29
+ super(message);
30
+ this.name = 'EdgeStoreError';
31
+ }
32
+ }
33
+
34
+ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5 }) {
35
+ return new Proxy({}, {
36
+ get (_, prop) {
37
+ const bucketName = prop;
38
+ const bucketFunctions = {
39
+ upload: async (params)=>{
40
+ try {
41
+ params.onProgressChange?.(0);
42
+ while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
43
+ await new Promise((resolve)=>setTimeout(resolve, 300));
44
+ }
45
+ uploadingCountRef.current++;
46
+ return await uploadFile(params, {
47
+ bucketName: bucketName,
48
+ apiPath
49
+ });
50
+ } finally{
51
+ uploadingCountRef.current--;
52
+ }
53
+ },
54
+ delete: async (params)=>{
55
+ return await deleteFile(params, {
56
+ bucketName: bucketName,
57
+ apiPath
58
+ });
59
+ }
60
+ };
61
+ return bucketFunctions;
62
+ }
63
+ });
64
+ }
65
+ async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
66
+ try {
67
+ onProgressChange?.(0);
68
+ const res = await fetch(`${apiPath}/request-upload`, {
69
+ method: 'POST',
70
+ body: JSON.stringify({
71
+ bucketName,
72
+ input,
73
+ fileInfo: {
74
+ extension: file.name.split('.').pop(),
75
+ type: file.type,
76
+ size: file.size,
77
+ replaceTargetUrl: options?.replaceTargetUrl
78
+ }
79
+ }),
80
+ headers: {
81
+ 'Content-Type': 'application/json'
82
+ }
83
+ });
84
+ const json = await res.json();
85
+ if (!json.uploadUrl) {
86
+ throw new EdgeStoreError('An error occurred');
87
+ }
88
+ // Upload the file to the signed URL and get the progress
89
+ await uploadFileInner(file, json.uploadUrl, onProgressChange);
90
+ return {
91
+ url: json.accessUrl,
92
+ thumbnailUrl: json.thumbnailUrl,
93
+ size: json.size,
94
+ uploadedAt: new Date(json.uploadedAt),
95
+ path: json.path,
96
+ metadata: json.metadata
97
+ };
98
+ } catch (e) {
99
+ onProgressChange?.(0);
100
+ throw e;
101
+ }
102
+ }
103
+ const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
104
+ const promise = new Promise((resolve, reject)=>{
105
+ const request = new XMLHttpRequest();
106
+ request.open('PUT', uploadUrl);
107
+ request.addEventListener('loadstart', ()=>{
108
+ onProgressChange?.(0);
109
+ });
110
+ request.upload.addEventListener('progress', (e)=>{
111
+ if (e.lengthComputable) {
112
+ // 2 decimal progress
113
+ const progress = Math.round(e.loaded / e.total * 10000) / 100;
114
+ onProgressChange?.(progress);
115
+ }
116
+ });
117
+ request.addEventListener('error', ()=>{
118
+ reject(new Error('Error uploading file'));
119
+ });
120
+ request.addEventListener('abort', ()=>{
121
+ reject(new Error('File upload aborted'));
122
+ });
123
+ request.addEventListener('loadend', ()=>{
124
+ resolve();
125
+ });
126
+ request.send(file);
127
+ });
128
+ return promise;
129
+ };
130
+ async function deleteFile({ url }, { apiPath, bucketName }) {
131
+ const res = await fetch(`${apiPath}/delete-file`, {
132
+ method: 'POST',
133
+ body: JSON.stringify({
134
+ url,
135
+ bucketName
136
+ }),
137
+ headers: {
138
+ 'Content-Type': 'application/json'
139
+ }
140
+ });
141
+ if (!res.ok) {
142
+ throw new EdgeStoreError('An error occurred');
143
+ }
144
+ return {
145
+ success: true
146
+ };
147
+ }
148
+
149
+ const DEFAULT_BASE_URL = process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
150
+ function createEdgeStoreProvider(opts) {
151
+ const EdgeStoreContext = /*#__PURE__*/ React__namespace.createContext(undefined);
152
+ const EdgeStoreProvider = ({ // TODO: Add basePath when custom domain is supported
153
+ children, basePath })=>{
154
+ return EdgeStoreProviderInner({
155
+ children,
156
+ context: EdgeStoreContext,
157
+ basePath,
158
+ maxConcurrentUploads: opts?.maxConcurrentUploads
159
+ });
160
+ };
161
+ function useEdgeStore() {
162
+ if (!EdgeStoreContext) {
163
+ throw new Error('React Context is unavailable in Server Components');
164
+ }
165
+ // @ts-expect-error - We know that the context value should not be undefined
166
+ const value = React__namespace.useContext(EdgeStoreContext);
167
+ if (!value && process.env.NODE_ENV !== 'production') {
168
+ throw new Error('[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />');
169
+ }
170
+ return value;
171
+ }
172
+ return {
173
+ EdgeStoreProvider,
174
+ useEdgeStore
175
+ };
176
+ }
177
+ function EdgeStoreProviderInner({ children, context, basePath, maxConcurrentUploads }) {
178
+ const apiPath = basePath ? `${basePath}` : '/api/edgestore';
179
+ const [token, setToken] = React__namespace.useState(null);
180
+ const uploadingCountRef = React__namespace.useRef(0);
181
+ React__namespace.useEffect(()=>{
182
+ void fetch(`${apiPath}/init`, {
183
+ method: 'POST'
184
+ }).then(async (res)=>{
185
+ if (res.ok) {
186
+ const json = await res.json();
187
+ setToken(json.token);
188
+ await fetch(`${DEFAULT_BASE_URL}/_init`, {
189
+ method: 'GET',
190
+ headers: {
191
+ 'x-edgestore-token': json.token
192
+ }
193
+ });
194
+ }
195
+ });
196
+ }, []);
197
+ function getSrc(url) {
198
+ if (// in production we use cookies, so we don't need a token
199
+ process.env.NODE_ENV === 'production' || // public urls don't need a token
200
+ // e.g. https://files.edge-store.com/project/bucket/_public/...
201
+ url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)) {
202
+ return `${url}`;
203
+ } else {
204
+ // in development, third party cookies don't work, so we need to pass the token as a query param
205
+ const uri = new URL(url);
206
+ uri.searchParams.set('token', token ?? '');
207
+ return `${uri}`;
208
+ }
209
+ }
210
+ return /*#__PURE__*/ React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/ React__namespace.createElement(context.Provider, {
211
+ value: {
212
+ edgestore: createNextProxy({
213
+ apiPath,
214
+ uploadingCountRef,
215
+ maxConcurrentUploads
216
+ }),
217
+ getSrc
218
+ }
219
+ }, children));
220
+ }
221
+
222
+ exports.createEdgeStoreProvider = createEdgeStoreProvider;
package/dist/index.mjs ADDED
@@ -0,0 +1,198 @@
1
+ import * as React from 'react';
2
+
3
+ class EdgeStoreError extends Error {
4
+ constructor(message){
5
+ super(message);
6
+ this.name = 'EdgeStoreError';
7
+ }
8
+ }
9
+
10
+ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5 }) {
11
+ return new Proxy({}, {
12
+ get (_, prop) {
13
+ const bucketName = prop;
14
+ const bucketFunctions = {
15
+ upload: async (params)=>{
16
+ try {
17
+ params.onProgressChange?.(0);
18
+ while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
19
+ await new Promise((resolve)=>setTimeout(resolve, 300));
20
+ }
21
+ uploadingCountRef.current++;
22
+ return await uploadFile(params, {
23
+ bucketName: bucketName,
24
+ apiPath
25
+ });
26
+ } finally{
27
+ uploadingCountRef.current--;
28
+ }
29
+ },
30
+ delete: async (params)=>{
31
+ return await deleteFile(params, {
32
+ bucketName: bucketName,
33
+ apiPath
34
+ });
35
+ }
36
+ };
37
+ return bucketFunctions;
38
+ }
39
+ });
40
+ }
41
+ async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
42
+ try {
43
+ onProgressChange?.(0);
44
+ const res = await fetch(`${apiPath}/request-upload`, {
45
+ method: 'POST',
46
+ body: JSON.stringify({
47
+ bucketName,
48
+ input,
49
+ fileInfo: {
50
+ extension: file.name.split('.').pop(),
51
+ type: file.type,
52
+ size: file.size,
53
+ replaceTargetUrl: options?.replaceTargetUrl
54
+ }
55
+ }),
56
+ headers: {
57
+ 'Content-Type': 'application/json'
58
+ }
59
+ });
60
+ const json = await res.json();
61
+ if (!json.uploadUrl) {
62
+ throw new EdgeStoreError('An error occurred');
63
+ }
64
+ // Upload the file to the signed URL and get the progress
65
+ await uploadFileInner(file, json.uploadUrl, onProgressChange);
66
+ return {
67
+ url: json.accessUrl,
68
+ thumbnailUrl: json.thumbnailUrl,
69
+ size: json.size,
70
+ uploadedAt: new Date(json.uploadedAt),
71
+ path: json.path,
72
+ metadata: json.metadata
73
+ };
74
+ } catch (e) {
75
+ onProgressChange?.(0);
76
+ throw e;
77
+ }
78
+ }
79
+ const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
80
+ const promise = new Promise((resolve, reject)=>{
81
+ const request = new XMLHttpRequest();
82
+ request.open('PUT', uploadUrl);
83
+ request.addEventListener('loadstart', ()=>{
84
+ onProgressChange?.(0);
85
+ });
86
+ request.upload.addEventListener('progress', (e)=>{
87
+ if (e.lengthComputable) {
88
+ // 2 decimal progress
89
+ const progress = Math.round(e.loaded / e.total * 10000) / 100;
90
+ onProgressChange?.(progress);
91
+ }
92
+ });
93
+ request.addEventListener('error', ()=>{
94
+ reject(new Error('Error uploading file'));
95
+ });
96
+ request.addEventListener('abort', ()=>{
97
+ reject(new Error('File upload aborted'));
98
+ });
99
+ request.addEventListener('loadend', ()=>{
100
+ resolve();
101
+ });
102
+ request.send(file);
103
+ });
104
+ return promise;
105
+ };
106
+ async function deleteFile({ url }, { apiPath, bucketName }) {
107
+ const res = await fetch(`${apiPath}/delete-file`, {
108
+ method: 'POST',
109
+ body: JSON.stringify({
110
+ url,
111
+ bucketName
112
+ }),
113
+ headers: {
114
+ 'Content-Type': 'application/json'
115
+ }
116
+ });
117
+ if (!res.ok) {
118
+ throw new EdgeStoreError('An error occurred');
119
+ }
120
+ return {
121
+ success: true
122
+ };
123
+ }
124
+
125
+ const DEFAULT_BASE_URL = process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
126
+ function createEdgeStoreProvider(opts) {
127
+ const EdgeStoreContext = /*#__PURE__*/ React.createContext(undefined);
128
+ const EdgeStoreProvider = ({ // TODO: Add basePath when custom domain is supported
129
+ children, basePath })=>{
130
+ return EdgeStoreProviderInner({
131
+ children,
132
+ context: EdgeStoreContext,
133
+ basePath,
134
+ maxConcurrentUploads: opts?.maxConcurrentUploads
135
+ });
136
+ };
137
+ function useEdgeStore() {
138
+ if (!EdgeStoreContext) {
139
+ throw new Error('React Context is unavailable in Server Components');
140
+ }
141
+ // @ts-expect-error - We know that the context value should not be undefined
142
+ const value = React.useContext(EdgeStoreContext);
143
+ if (!value && process.env.NODE_ENV !== 'production') {
144
+ throw new Error('[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />');
145
+ }
146
+ return value;
147
+ }
148
+ return {
149
+ EdgeStoreProvider,
150
+ useEdgeStore
151
+ };
152
+ }
153
+ function EdgeStoreProviderInner({ children, context, basePath, maxConcurrentUploads }) {
154
+ const apiPath = basePath ? `${basePath}` : '/api/edgestore';
155
+ const [token, setToken] = React.useState(null);
156
+ const uploadingCountRef = React.useRef(0);
157
+ React.useEffect(()=>{
158
+ void fetch(`${apiPath}/init`, {
159
+ method: 'POST'
160
+ }).then(async (res)=>{
161
+ if (res.ok) {
162
+ const json = await res.json();
163
+ setToken(json.token);
164
+ await fetch(`${DEFAULT_BASE_URL}/_init`, {
165
+ method: 'GET',
166
+ headers: {
167
+ 'x-edgestore-token': json.token
168
+ }
169
+ });
170
+ }
171
+ });
172
+ }, []);
173
+ function getSrc(url) {
174
+ if (// in production we use cookies, so we don't need a token
175
+ process.env.NODE_ENV === 'production' || // public urls don't need a token
176
+ // e.g. https://files.edge-store.com/project/bucket/_public/...
177
+ url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)) {
178
+ return `${url}`;
179
+ } else {
180
+ // in development, third party cookies don't work, so we need to pass the token as a query param
181
+ const uri = new URL(url);
182
+ uri.searchParams.set('token', token ?? '');
183
+ return `${uri}`;
184
+ }
185
+ }
186
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(context.Provider, {
187
+ value: {
188
+ edgestore: createNextProxy({
189
+ apiPath,
190
+ uploadingCountRef,
191
+ maxConcurrentUploads
192
+ }),
193
+ getSrc
194
+ }
195
+ }, children));
196
+ }
197
+
198
+ export { createEdgeStoreProvider };
@@ -0,0 +1,5 @@
1
+ declare class EdgeStoreError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export default EdgeStoreError;
5
+ //# sourceMappingURL=EdgeStoreError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EdgeStoreError.d.ts","sourceRoot":"","sources":["../../../src/libs/errors/EdgeStoreError.ts"],"names":[],"mappings":"AAAA,cAAM,cAAe,SAAQ,KAAK;gBACpB,OAAO,EAAE,MAAM;CAI5B;AAED,eAAe,cAAc,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@edgestore/react",
3
+ "version": "0.0.0-alpha.12",
4
+ "description": "Image Handling for React/Next.js",
5
+ "homepage": "https://edge-store.com",
6
+ "repository": "https://github.com/edgestorejs/edge-store.git",
7
+ "author": "Ravi <me@ravi.com>",
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.mjs",
10
+ "typings": "dist/index.d.ts",
11
+ "keywords": [
12
+ "react",
13
+ "nodejs",
14
+ "nextjs",
15
+ "image",
16
+ "cdn",
17
+ "edgestore",
18
+ "edge-store"
19
+ ],
20
+ "scripts": {
21
+ "build": "rollup --config rollup.config.ts --configPlugin rollup-plugin-swc3",
22
+ "dev": "pnpm build --watch",
23
+ "codegen:entrypoints": "tsx entrypoints.script.ts",
24
+ "lint": "eslint --cache --ext \".js,.ts,.tsx\" --ignore-path ../../.gitignore --report-unused-disable-directives src"
25
+ },
26
+ "exports": {
27
+ "./package.json": "./package.json",
28
+ ".": {
29
+ "import": "./dist/index.mjs",
30
+ "require": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "README.md",
38
+ "package.json",
39
+ "!**/*.test.*"
40
+ ],
41
+ "private": false,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@aws-sdk/client-s3": "^3.294.0",
48
+ "@aws-sdk/s3-request-presigner": "^3.294.0",
49
+ "@panva/hkdf": "^1.0.4",
50
+ "cookie": "^0.5.0",
51
+ "jose": "^4.13.1",
52
+ "uuid": "^9.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@edgestore/server": "0.0.0-alpha.12",
56
+ "next": "*",
57
+ "react": ">=16.8.0",
58
+ "react-dom": ">=16.8.0",
59
+ "zod": ">=3.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@edgestore/server": "0.0.0-alpha.12",
63
+ "@types/cookie": "^0.5.1",
64
+ "@types/node": "^18.11.18",
65
+ "@types/uuid": "^9.0.1",
66
+ "next": "^13.4.9",
67
+ "react": "^18.2.0",
68
+ "react-dom": "^18.2.0",
69
+ "typescript": "^5.1.6",
70
+ "zod": "^3.21.4"
71
+ },
72
+ "gitHead": "bec47f77e223a231f5e04070aa8da6a609838e6b"
73
+ }
@@ -0,0 +1,141 @@
1
+ import { AnyRouter } from '@edgestore/server/core';
2
+ import * as React from 'react';
3
+ import { BucketFunctions, createNextProxy } from './createNextProxy';
4
+
5
+ const DEFAULT_BASE_URL =
6
+ process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
7
+
8
+ type EdgeStoreContextValue<TRouter extends AnyRouter> = {
9
+ edgestore: BucketFunctions<TRouter>;
10
+ /**
11
+ * In development, if this is a protected file, this function will add the token as a query param to the url.
12
+ * This is needed because third party cookies don't work with http urls.
13
+ */
14
+ getSrc: (url: string) => string;
15
+ };
16
+
17
+ export function createEdgeStoreProvider<TRouter extends AnyRouter>(opts?: {
18
+ /**
19
+ * The maximum number of concurrent uploads.
20
+ *
21
+ * Uploads will automatically be queued if this limit is reached.
22
+ *
23
+ * @default 5
24
+ */
25
+ maxConcurrentUploads?: number;
26
+ }) {
27
+ const EdgeStoreContext = React.createContext<
28
+ EdgeStoreContextValue<TRouter> | undefined
29
+ >(undefined);
30
+
31
+ const EdgeStoreProvider = ({
32
+ // TODO: Add basePath when custom domain is supported
33
+ children,
34
+ basePath,
35
+ }: {
36
+ children: React.ReactNode;
37
+ /**
38
+ * In case your app is not hosted at the root of your domain, you can specify the base path here.
39
+ * If you set this, make sure to set the full path to the EdgeStore API.
40
+ * e.g. `/my-app/api/edgestore` or `https://example.com/my-app/api/edgestore`
41
+ *
42
+ * @example - If your app is hosted at `https://example.com/my-app`, you can set the `basePath` to `/my-app/api/edgestore`.
43
+ */
44
+ basePath?: string;
45
+ }) => {
46
+ return EdgeStoreProviderInner<TRouter>({
47
+ children,
48
+ context: EdgeStoreContext,
49
+ basePath,
50
+ maxConcurrentUploads: opts?.maxConcurrentUploads,
51
+ });
52
+ };
53
+
54
+ function useEdgeStore() {
55
+ if (!EdgeStoreContext) {
56
+ throw new Error('React Context is unavailable in Server Components');
57
+ }
58
+
59
+ // @ts-expect-error - We know that the context value should not be undefined
60
+ const value: EdgeStoreContextValue<TRouter> =
61
+ React.useContext(EdgeStoreContext);
62
+ if (!value && process.env.NODE_ENV !== 'production') {
63
+ throw new Error(
64
+ '[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />',
65
+ );
66
+ }
67
+
68
+ return value;
69
+ }
70
+
71
+ return {
72
+ EdgeStoreProvider,
73
+ useEdgeStore,
74
+ };
75
+ }
76
+
77
+ function EdgeStoreProviderInner<TRouter extends AnyRouter>({
78
+ children,
79
+ context,
80
+ basePath,
81
+ maxConcurrentUploads,
82
+ }: {
83
+ children: React.ReactNode;
84
+ context: React.Context<EdgeStoreContextValue<TRouter> | undefined>;
85
+ basePath?: string;
86
+ maxConcurrentUploads?: number;
87
+ }) {
88
+ const apiPath = basePath ? `${basePath}` : '/api/edgestore';
89
+ const [token, setToken] = React.useState<string | null>(null);
90
+ const uploadingCountRef = React.useRef(0);
91
+ React.useEffect(() => {
92
+ void fetch(`${apiPath}/init`, {
93
+ method: 'POST',
94
+ }).then(async (res) => {
95
+ if (res.ok) {
96
+ const json = await res.json();
97
+ setToken(json.token);
98
+ await fetch(`${DEFAULT_BASE_URL}/_init`, {
99
+ method: 'GET',
100
+ headers: {
101
+ 'x-edgestore-token': json.token,
102
+ },
103
+ });
104
+ }
105
+ });
106
+ }, []);
107
+
108
+ function getSrc(url: string) {
109
+ if (
110
+ // in production we use cookies, so we don't need a token
111
+ process.env.NODE_ENV === 'production' ||
112
+ // public urls don't need a token
113
+ // e.g. https://files.edge-store.com/project/bucket/_public/...
114
+ url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)
115
+ ) {
116
+ return `${url}`;
117
+ } else {
118
+ // in development, third party cookies don't work, so we need to pass the token as a query param
119
+ const uri = new URL(url);
120
+ uri.searchParams.set('token', token ?? '');
121
+ return `${uri}`;
122
+ }
123
+ }
124
+
125
+ return (
126
+ <>
127
+ <context.Provider
128
+ value={{
129
+ edgestore: createNextProxy<TRouter>({
130
+ apiPath,
131
+ uploadingCountRef,
132
+ maxConcurrentUploads,
133
+ }),
134
+ getSrc,
135
+ }}
136
+ >
137
+ {children}
138
+ </context.Provider>
139
+ </>
140
+ );
141
+ }
@@ -0,0 +1,210 @@
1
+ import {
2
+ AnyRouter,
3
+ InferBucketPathKeys,
4
+ InferMetadataObject,
5
+ } from '@edgestore/server/core';
6
+ import { z } from 'zod';
7
+ import EdgeStoreError from './libs/errors/EdgeStoreError';
8
+
9
+ export type BucketFunctions<TRouter extends AnyRouter> = {
10
+ [K in keyof TRouter['buckets']]: {
11
+ upload: (
12
+ params: z.infer<TRouter['buckets'][K]['_def']['input']> extends object
13
+ ? {
14
+ file: File;
15
+ input: z.infer<TRouter['buckets'][K]['_def']['input']>;
16
+ onProgressChange?: OnProgressChangeHandler;
17
+ options?: UploadOptions;
18
+ }
19
+ : {
20
+ file: File;
21
+ onProgressChange?: OnProgressChangeHandler;
22
+ options?: UploadOptions;
23
+ },
24
+ ) => Promise<{
25
+ url: string;
26
+ thumbnailUrl: TRouter['buckets'][K]['_def']['type'] extends 'IMAGE'
27
+ ? string | null
28
+ : never;
29
+ size: number;
30
+ uploadedAt: Date;
31
+ metadata: InferMetadataObject<TRouter['buckets'][K]>;
32
+ path: {
33
+ [TKey in InferBucketPathKeys<TRouter['buckets'][K]>]: string;
34
+ };
35
+ }>;
36
+ delete: (params: { url: string }) => Promise<{
37
+ success: boolean;
38
+ }>;
39
+ };
40
+ };
41
+
42
+ type OnProgressChangeHandler = (progress: number) => void;
43
+
44
+ type UploadOptions = {
45
+ replaceTargetUrl?: string;
46
+ };
47
+
48
+ export function createNextProxy<TRouter extends AnyRouter>({
49
+ apiPath,
50
+ uploadingCountRef,
51
+ maxConcurrentUploads = 5,
52
+ }: {
53
+ apiPath: string;
54
+ uploadingCountRef: React.MutableRefObject<number>;
55
+ maxConcurrentUploads?: number;
56
+ }) {
57
+ return new Proxy<BucketFunctions<TRouter>>({} as BucketFunctions<TRouter>, {
58
+ get(_, prop) {
59
+ const bucketName = prop as keyof TRouter['buckets'];
60
+ const bucketFunctions: BucketFunctions<TRouter>[string] = {
61
+ upload: async (params) => {
62
+ try {
63
+ params.onProgressChange?.(0);
64
+ while (
65
+ uploadingCountRef.current >= maxConcurrentUploads &&
66
+ uploadingCountRef.current > 0
67
+ ) {
68
+ await new Promise((resolve) => setTimeout(resolve, 300));
69
+ }
70
+ uploadingCountRef.current++;
71
+ return await uploadFile(params, {
72
+ bucketName: bucketName as string,
73
+ apiPath,
74
+ });
75
+ } finally {
76
+ uploadingCountRef.current--;
77
+ }
78
+ },
79
+ delete: async (params: { url: string }) => {
80
+ return await deleteFile(params, {
81
+ bucketName: bucketName as string,
82
+ apiPath,
83
+ });
84
+ },
85
+ };
86
+ return bucketFunctions;
87
+ },
88
+ });
89
+ }
90
+
91
+ async function uploadFile(
92
+ {
93
+ file,
94
+ input,
95
+ onProgressChange,
96
+ options,
97
+ }: {
98
+ file: File;
99
+ input?: object;
100
+ onProgressChange?: OnProgressChangeHandler;
101
+ options?: UploadOptions;
102
+ },
103
+ {
104
+ apiPath,
105
+ bucketName,
106
+ }: {
107
+ apiPath: string;
108
+ bucketName: string;
109
+ },
110
+ ) {
111
+ try {
112
+ onProgressChange?.(0);
113
+ const res = await fetch(`${apiPath}/request-upload`, {
114
+ method: 'POST',
115
+ body: JSON.stringify({
116
+ bucketName,
117
+ input,
118
+ fileInfo: {
119
+ extension: file.name.split('.').pop(),
120
+ type: file.type,
121
+ size: file.size,
122
+ replaceTargetUrl: options?.replaceTargetUrl,
123
+ },
124
+ }),
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ });
129
+ const json = await res.json();
130
+ if (!json.uploadUrl) {
131
+ throw new EdgeStoreError('An error occurred');
132
+ }
133
+ // Upload the file to the signed URL and get the progress
134
+ await uploadFileInner(file, json.uploadUrl, onProgressChange);
135
+ return {
136
+ url: json.accessUrl,
137
+ thumbnailUrl: json.thumbnailUrl,
138
+ size: json.size,
139
+ uploadedAt: new Date(json.uploadedAt),
140
+ path: json.path,
141
+ metadata: json.metadata,
142
+ };
143
+ } catch (e) {
144
+ onProgressChange?.(0);
145
+ throw e;
146
+ }
147
+ }
148
+
149
+ const uploadFileInner = async (
150
+ file: File,
151
+ uploadUrl: string,
152
+ onProgressChange?: OnProgressChangeHandler,
153
+ ) => {
154
+ const promise = new Promise<void>((resolve, reject) => {
155
+ const request = new XMLHttpRequest();
156
+ request.open('PUT', uploadUrl);
157
+ request.addEventListener('loadstart', () => {
158
+ onProgressChange?.(0);
159
+ });
160
+ request.upload.addEventListener('progress', (e) => {
161
+ if (e.lengthComputable) {
162
+ // 2 decimal progress
163
+ const progress = Math.round((e.loaded / e.total) * 10000) / 100;
164
+ onProgressChange?.(progress);
165
+ }
166
+ });
167
+ request.addEventListener('error', () => {
168
+ reject(new Error('Error uploading file'));
169
+ });
170
+ request.addEventListener('abort', () => {
171
+ reject(new Error('File upload aborted'));
172
+ });
173
+ request.addEventListener('loadend', () => {
174
+ resolve();
175
+ });
176
+
177
+ request.send(file);
178
+ });
179
+ return promise;
180
+ };
181
+
182
+ async function deleteFile(
183
+ {
184
+ url,
185
+ }: {
186
+ url: string;
187
+ },
188
+ {
189
+ apiPath,
190
+ bucketName,
191
+ }: {
192
+ apiPath: string;
193
+ bucketName: string;
194
+ },
195
+ ) {
196
+ const res = await fetch(`${apiPath}/delete-file`, {
197
+ method: 'POST',
198
+ body: JSON.stringify({
199
+ url,
200
+ bucketName,
201
+ }),
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ },
205
+ });
206
+ if (!res.ok) {
207
+ throw new EdgeStoreError('An error occurred');
208
+ }
209
+ return { success: true };
210
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './contextProvider';
@@ -0,0 +1,8 @@
1
+ class EdgeStoreError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'EdgeStoreError';
5
+ }
6
+ }
7
+
8
+ export default EdgeStoreError;