Skip to content

Commit 0d30ee4

Browse files
authored
feat: add function for creating server component (#1)
* feat: add server component types * feat: add function to create server component * feat: add axios as dependency * feat: module exports
1 parent 350b89c commit 0d30ee4

File tree

7 files changed

+274
-20
lines changed

7 files changed

+274
-20
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
"react": "*",
8181
"react-native": "*"
8282
},
83+
"dependencies": {
84+
"axios": "^1.6.7"
85+
},
8386
"workspaces": [
8487
"example"
8588
],

src/@types/index.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export type RSCConfig = {
2+
readonly global?: any;
3+
};
4+
5+
export type RSCSource =
6+
| {
7+
readonly uri: string;
8+
}
9+
| string;
10+
11+
export type RSCActions = 'NAVIGATE' | 'IO' | 'STATE_CHANGE';
12+
13+
export type RSCProps = {
14+
readonly source: RSCSource;
15+
readonly fallbackComponent?: () => JSX.Element;
16+
readonly loadingComponent?: () => JSX.Element;
17+
readonly errorComponent?: () => JSX.Element;
18+
readonly onError?: (error: Error) => void;
19+
readonly navigationRef?: React.Ref<any>;
20+
readonly onAction?: (
21+
action: RSCActions,
22+
payload: Record<string, any>
23+
) => void;
24+
readonly openRSC?: (source: RSCSource) => Promise<React.Component>;
25+
};
26+
27+
export type RSCPromise<T> = {
28+
readonly resolve: (result: T) => void;
29+
readonly reject: (error: Error) => void;
30+
};

src/component/ServerComponent.tsx

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as React from 'react';
2+
import { RSCProps } from '../@types';
3+
4+
export default function RSC({
5+
source,
6+
openRSC,
7+
fallbackComponent,
8+
loadingComponent,
9+
errorComponent,
10+
...extras
11+
}: RSCProps): JSX.Element {
12+
const [ServerComponent, setServerComponent] =
13+
React.useState<React.Component | null>(null);
14+
15+
const [error, setError] = React.useState<Error | null>(null);
16+
17+
React.useEffect(() => {
18+
(async () => {
19+
try {
20+
if (typeof openRSC === 'function') {
21+
const rsc = await openRSC(source);
22+
return setServerComponent(() => rsc);
23+
}
24+
throw new Error(`[ServerComponent]: typeof openRSC should be function`);
25+
} catch (e) {
26+
setServerComponent(() => null);
27+
setError(e);
28+
}
29+
})();
30+
}, [source, openRSC]);
31+
32+
const FallbackComponent = React.useCallback((): JSX.Element => {
33+
if (fallbackComponent) {
34+
return fallbackComponent();
35+
}
36+
return <></>;
37+
}, [fallbackComponent]);
38+
39+
if (typeof ServerComponent === 'function') {
40+
return (
41+
<React.Fragment>
42+
<React.Suspense fallback={<FallbackComponent />} />
43+
{/* @ts-ignore */}
44+
<ServerComponent {...extras} />
45+
</React.Fragment>
46+
);
47+
} else if (error) {
48+
return errorComponent();
49+
}
50+
return loadingComponent();
51+
}
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/* eslint-disable no-new-func */
2+
import * as React from 'react';
3+
import type { RSCPromise, RSCConfig, RSCProps, RSCSource } from '../@types';
4+
import RSC from './ServerComponent';
5+
import axios, { type AxiosRequestConfig } from 'axios';
6+
7+
const defaultGlobal = Object.freeze({
8+
require: (moduleId: string) => {
9+
if (moduleId === 'react') {
10+
// @ts-ignore
11+
return require('react');
12+
} else if (moduleId === 'react-native') {
13+
// @ts-ignore
14+
return require('react-native');
15+
}
16+
return null;
17+
},
18+
});
19+
20+
const createComponent =
21+
(global: any) =>
22+
async (src: string): Promise<React.Component> => {
23+
const globalName = '__SERVER_COMPONENT__';
24+
const Component = await new Function(
25+
globalName,
26+
`${Object.keys(global)
27+
.map((key) => `var ${key} = ${globalName}.${key};`)
28+
.join('\n')}; const exports = {}; ${src}; return exports.default`
29+
)(global);
30+
31+
return Component;
32+
};
33+
34+
const axiosRequest = (config: AxiosRequestConfig) => axios(config);
35+
36+
const buildRSC =
37+
({
38+
openURI,
39+
}: {
40+
readonly openURI: (
41+
uri: string,
42+
callback: RSCPromise<React.Component>
43+
) => void;
44+
}) =>
45+
async (source: RSCSource): Promise<React.Component> => {
46+
// TODO handle string source
47+
if (source && typeof source === 'object') {
48+
const { uri } = source;
49+
// TODO handle uri validation
50+
if (typeof uri === 'string') {
51+
return new Promise<React.Component>((resolve, reject) =>
52+
openURI(uri, { resolve, reject })
53+
);
54+
}
55+
}
56+
};
57+
58+
const buildRequest =
59+
({
60+
component,
61+
}: {
62+
readonly component: (src: string) => Promise<React.Component>;
63+
}) =>
64+
async (uri: string) => {
65+
//const handler = completionHandler();
66+
67+
try {
68+
const result = await axiosRequest({ url: uri, method: 'get' });
69+
const { data } = result;
70+
if (typeof data !== 'string') {
71+
throw new Error(
72+
`[ServerComponent]: Expected string data, encountered ${typeof data}`
73+
);
74+
}
75+
76+
component(data);
77+
} catch (e) {
78+
console.log(`[ServerComponent]: Build Request caught error ${e}`);
79+
}
80+
};
81+
82+
const buildURIForRSC =
83+
({ uriRequest }: { readonly uriRequest: (uri: string) => void }) =>
84+
(uri: string, callback: RSCPromise<React.Component>): void => {
85+
const { resolve, reject } = callback;
86+
// TODO: handle caching and queueing here
87+
return uriRequest(uri);
88+
};
89+
90+
export default function createServerComponent({
91+
global = defaultGlobal,
92+
}: RSCConfig) {
93+
//const handler = completionHandler();
94+
95+
const component = createComponent(global);
96+
97+
const uriRequest = buildRequest({ component });
98+
99+
const openURI = buildURIForRSC({ uriRequest });
100+
101+
const openRSC = buildRSC({ openURI });
102+
103+
const ServerComponent = (props: RSCProps) => (
104+
<RSC {...props} openRSC={openRSC} />
105+
);
106+
107+
return Object.freeze({
108+
ServerComponent,
109+
});
110+
}

src/component/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as createServerComponent } from './createServerComponent';

src/index.tsx

+22-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
import { NativeModules, Platform } from 'react-native';
1+
// import { NativeModules, Platform } from 'react-native';
22

3-
const LINKING_ERROR =
4-
`The package 'react-native-server-component' doesn't seem to be linked. Make sure: \n\n` +
5-
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
6-
'- You rebuilt the app after installing the package\n' +
7-
'- You are not using Expo Go\n';
3+
// const LINKING_ERROR =
4+
// `The package 'react-native-server-component' doesn't seem to be linked. Make sure: \n\n` +
5+
// Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
6+
// '- You rebuilt the app after installing the package\n' +
7+
// '- You are not using Expo Go\n';
88

9-
const ServerComponent = NativeModules.ServerComponent
10-
? NativeModules.ServerComponent
11-
: new Proxy(
12-
{},
13-
{
14-
get() {
15-
throw new Error(LINKING_ERROR);
16-
},
17-
}
18-
);
9+
// const ServerComponent = NativeModules.ServerComponent
10+
// ? NativeModules.ServerComponent
11+
// : new Proxy(
12+
// {},
13+
// {
14+
// get() {
15+
// throw new Error(LINKING_ERROR);
16+
// },
17+
// }
18+
// );
1919

20-
export function multiply(a: number, b: number): Promise<number> {
21-
return ServerComponent.multiply(a, b);
22-
}
20+
// export function multiply(a: number, b: number): Promise<number> {
21+
// return ServerComponent.multiply(a, b);
22+
// }
23+
24+
export * from './@types';
25+
export * from './component';

yarn.lock

+57-1
Original file line numberDiff line numberDiff line change
@@ -3862,6 +3862,13 @@ __metadata:
38623862
languageName: node
38633863
linkType: hard
38643864

3865+
"asynckit@npm:^0.4.0":
3866+
version: 0.4.0
3867+
resolution: "asynckit@npm:0.4.0"
3868+
checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be
3869+
languageName: node
3870+
linkType: hard
3871+
38653872
"available-typed-arrays@npm:^1.0.7":
38663873
version: 1.0.7
38673874
resolution: "available-typed-arrays@npm:1.0.7"
@@ -3871,6 +3878,17 @@ __metadata:
38713878
languageName: node
38723879
linkType: hard
38733880

3881+
"axios@npm:^1.6.7":
3882+
version: 1.6.8
3883+
resolution: "axios@npm:1.6.8"
3884+
dependencies:
3885+
follow-redirects: ^1.15.6
3886+
form-data: ^4.0.0
3887+
proxy-from-env: ^1.1.0
3888+
checksum: bf007fa4b207d102459300698620b3b0873503c6d47bf5a8f6e43c0c64c90035a4f698b55027ca1958f61ab43723df2781c38a99711848d232cad7accbcdfcdd
3889+
languageName: node
3890+
linkType: hard
3891+
38743892
"babel-core@npm:^7.0.0-bridge.0":
38753893
version: 7.0.0-bridge.0
38763894
resolution: "babel-core@npm:7.0.0-bridge.0"
@@ -4592,6 +4610,15 @@ __metadata:
45924610
languageName: node
45934611
linkType: hard
45944612

4613+
"combined-stream@npm:^1.0.8":
4614+
version: 1.0.8
4615+
resolution: "combined-stream@npm:1.0.8"
4616+
dependencies:
4617+
delayed-stream: ~1.0.0
4618+
checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c
4619+
languageName: node
4620+
linkType: hard
4621+
45954622
"command-exists@npm:^1.2.8":
45964623
version: 1.2.9
45974624
resolution: "command-exists@npm:1.2.9"
@@ -5374,6 +5401,13 @@ __metadata:
53745401
languageName: node
53755402
linkType: hard
53765403

5404+
"delayed-stream@npm:~1.0.0":
5405+
version: 1.0.0
5406+
resolution: "delayed-stream@npm:1.0.0"
5407+
checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020
5408+
languageName: node
5409+
linkType: hard
5410+
53775411
"denodeify@npm:^1.2.1":
53785412
version: 1.2.1
53795413
resolution: "denodeify@npm:1.2.1"
@@ -6458,6 +6492,16 @@ __metadata:
64586492
languageName: node
64596493
linkType: hard
64606494

6495+
"follow-redirects@npm:^1.15.6":
6496+
version: 1.15.6
6497+
resolution: "follow-redirects@npm:1.15.6"
6498+
peerDependenciesMeta:
6499+
debug:
6500+
optional: true
6501+
checksum: a62c378dfc8c00f60b9c80cab158ba54e99ba0239a5dd7c81245e5a5b39d10f0c35e249c3379eae719ff0285fff88c365dd446fab19dee771f1d76252df1bbf5
6502+
languageName: node
6503+
linkType: hard
6504+
64616505
"for-each@npm:^0.3.3":
64626506
version: 0.3.3
64636507
resolution: "for-each@npm:0.3.3"
@@ -6484,6 +6528,17 @@ __metadata:
64846528
languageName: node
64856529
linkType: hard
64866530

6531+
"form-data@npm:^4.0.0":
6532+
version: 4.0.0
6533+
resolution: "form-data@npm:4.0.0"
6534+
dependencies:
6535+
asynckit: ^0.4.0
6536+
combined-stream: ^1.0.8
6537+
mime-types: ^2.1.12
6538+
checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c
6539+
languageName: node
6540+
linkType: hard
6541+
64876542
"formdata-polyfill@npm:^4.0.10":
64886543
version: 4.0.10
64896544
resolution: "formdata-polyfill@npm:4.0.10"
@@ -9433,7 +9488,7 @@ __metadata:
94339488
languageName: node
94349489
linkType: hard
94359490

9436-
"mime-types@npm:2.1.35, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34":
9491+
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34":
94379492
version: 2.1.35
94389493
resolution: "mime-types@npm:2.1.35"
94399494
dependencies:
@@ -10816,6 +10871,7 @@ __metadata:
1081610871
"@release-it/conventional-changelog": ^5.0.0
1081710872
"@types/jest": ^29.5.5
1081810873
"@types/react": ^18.2.44
10874+
axios: ^1.6.7
1081910875
commitlint: ^17.0.2
1082010876
del-cli: ^5.1.0
1082110877
eslint: ^8.51.0

0 commit comments

Comments
 (0)