Skip to content

Commit

Permalink
feat: add function for creating server component (#1)
Browse files Browse the repository at this point in the history
* feat: add server component types

* feat: add function to create server component

* feat: add axios as dependency

* feat: module exports
  • Loading branch information
kunalchavhan authored Apr 1, 2024
1 parent 350b89c commit 0d30ee4
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 20 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
"react": "*",
"react-native": "*"
},
"dependencies": {
"axios": "^1.6.7"
},
"workspaces": [
"example"
],
Expand Down
30 changes: 30 additions & 0 deletions src/@types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export type RSCConfig = {
readonly global?: any;
};

export type RSCSource =
| {
readonly uri: string;
}
| string;

export type RSCActions = 'NAVIGATE' | 'IO' | 'STATE_CHANGE';

export type RSCProps = {
readonly source: RSCSource;
readonly fallbackComponent?: () => JSX.Element;
readonly loadingComponent?: () => JSX.Element;
readonly errorComponent?: () => JSX.Element;
readonly onError?: (error: Error) => void;
readonly navigationRef?: React.Ref<any>;
readonly onAction?: (
action: RSCActions,
payload: Record<string, any>
) => void;
readonly openRSC?: (source: RSCSource) => Promise<React.Component>;
};

export type RSCPromise<T> = {
readonly resolve: (result: T) => void;
readonly reject: (error: Error) => void;
};
51 changes: 51 additions & 0 deletions src/component/ServerComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import { RSCProps } from '../@types';

export default function RSC({
source,
openRSC,
fallbackComponent,
loadingComponent,
errorComponent,
...extras
}: RSCProps): JSX.Element {
const [ServerComponent, setServerComponent] =
React.useState<React.Component | null>(null);

const [error, setError] = React.useState<Error | null>(null);

React.useEffect(() => {
(async () => {
try {
if (typeof openRSC === 'function') {
const rsc = await openRSC(source);
return setServerComponent(() => rsc);
}
throw new Error(`[ServerComponent]: typeof openRSC should be function`);
} catch (e) {
setServerComponent(() => null);
setError(e);
}
})();
}, [source, openRSC]);

const FallbackComponent = React.useCallback((): JSX.Element => {
if (fallbackComponent) {
return fallbackComponent();
}
return <></>;
}, [fallbackComponent]);

if (typeof ServerComponent === 'function') {
return (
<React.Fragment>
<React.Suspense fallback={<FallbackComponent />} />
{/* @ts-ignore */}
<ServerComponent {...extras} />
</React.Fragment>
);
} else if (error) {
return errorComponent();
}
return loadingComponent();
}
110 changes: 110 additions & 0 deletions src/component/createServerComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* eslint-disable no-new-func */
import * as React from 'react';
import type { RSCPromise, RSCConfig, RSCProps, RSCSource } from '../@types';
import RSC from './ServerComponent';
import axios, { type AxiosRequestConfig } from 'axios';

const defaultGlobal = Object.freeze({
require: (moduleId: string) => {
if (moduleId === 'react') {
// @ts-ignore
return require('react');
} else if (moduleId === 'react-native') {
// @ts-ignore
return require('react-native');
}
return null;
},
});

const createComponent =
(global: any) =>
async (src: string): Promise<React.Component> => {
const globalName = '__SERVER_COMPONENT__';
const Component = await new Function(
globalName,
`${Object.keys(global)
.map((key) => `var ${key} = ${globalName}.${key};`)
.join('\n')}; const exports = {}; ${src}; return exports.default`
)(global);

return Component;
};

const axiosRequest = (config: AxiosRequestConfig) => axios(config);

const buildRSC =
({
openURI,
}: {
readonly openURI: (
uri: string,
callback: RSCPromise<React.Component>
) => void;
}) =>
async (source: RSCSource): Promise<React.Component> => {
// TODO handle string source
if (source && typeof source === 'object') {
const { uri } = source;
// TODO handle uri validation
if (typeof uri === 'string') {
return new Promise<React.Component>((resolve, reject) =>
openURI(uri, { resolve, reject })
);
}
}
};

const buildRequest =
({
component,
}: {
readonly component: (src: string) => Promise<React.Component>;
}) =>
async (uri: string) => {
//const handler = completionHandler();

try {
const result = await axiosRequest({ url: uri, method: 'get' });
const { data } = result;
if (typeof data !== 'string') {
throw new Error(
`[ServerComponent]: Expected string data, encountered ${typeof data}`
);
}

component(data);
} catch (e) {
console.log(`[ServerComponent]: Build Request caught error ${e}`);
}
};

const buildURIForRSC =
({ uriRequest }: { readonly uriRequest: (uri: string) => void }) =>
(uri: string, callback: RSCPromise<React.Component>): void => {
const { resolve, reject } = callback;

Check failure on line 85 in src/component/createServerComponent.tsx

View workflow job for this annotation

GitHub Actions / lint

'resolve' is assigned a value but never used

Check failure on line 85 in src/component/createServerComponent.tsx

View workflow job for this annotation

GitHub Actions / lint

'reject' is assigned a value but never used
// TODO: handle caching and queueing here
return uriRequest(uri);
};

export default function createServerComponent({
global = defaultGlobal,
}: RSCConfig) {
//const handler = completionHandler();

const component = createComponent(global);

const uriRequest = buildRequest({ component });

const openURI = buildURIForRSC({ uriRequest });

const openRSC = buildRSC({ openURI });

const ServerComponent = (props: RSCProps) => (
<RSC {...props} openRSC={openRSC} />
);

return Object.freeze({
ServerComponent,
});
}
1 change: 1 addition & 0 deletions src/component/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as createServerComponent } from './createServerComponent';
41 changes: 22 additions & 19 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { NativeModules, Platform } from 'react-native';
// import { NativeModules, Platform } from 'react-native';

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

const ServerComponent = NativeModules.ServerComponent
? NativeModules.ServerComponent
: new Proxy(
{},
{
get() {
throw new Error(LINKING_ERROR);
},
}
);
// const ServerComponent = NativeModules.ServerComponent
// ? NativeModules.ServerComponent
// : new Proxy(
// {},
// {
// get() {
// throw new Error(LINKING_ERROR);
// },
// }
// );

export function multiply(a: number, b: number): Promise<number> {
return ServerComponent.multiply(a, b);
}
// export function multiply(a: number, b: number): Promise<number> {
// return ServerComponent.multiply(a, b);
// }

export * from './@types';
export * from './component';
58 changes: 57 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3862,6 +3862,13 @@ __metadata:
languageName: node
linkType: hard

"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be
languageName: node
linkType: hard

"available-typed-arrays@npm:^1.0.7":
version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7"
Expand All @@ -3871,6 +3878,17 @@ __metadata:
languageName: node
linkType: hard

"axios@npm:^1.6.7":
version: 1.6.8
resolution: "axios@npm:1.6.8"
dependencies:
follow-redirects: ^1.15.6
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: bf007fa4b207d102459300698620b3b0873503c6d47bf5a8f6e43c0c64c90035a4f698b55027ca1958f61ab43723df2781c38a99711848d232cad7accbcdfcdd
languageName: node
linkType: hard

"babel-core@npm:^7.0.0-bridge.0":
version: 7.0.0-bridge.0
resolution: "babel-core@npm:7.0.0-bridge.0"
Expand Down Expand Up @@ -4592,6 +4610,15 @@ __metadata:
languageName: node
linkType: hard

"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: ~1.0.0
checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c
languageName: node
linkType: hard

"command-exists@npm:^1.2.8":
version: 1.2.9
resolution: "command-exists@npm:1.2.9"
Expand Down Expand Up @@ -5374,6 +5401,13 @@ __metadata:
languageName: node
linkType: hard

"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020
languageName: node
linkType: hard

"denodeify@npm:^1.2.1":
version: 1.2.1
resolution: "denodeify@npm:1.2.1"
Expand Down Expand Up @@ -6458,6 +6492,16 @@ __metadata:
languageName: node
linkType: hard

"follow-redirects@npm:^1.15.6":
version: 1.15.6
resolution: "follow-redirects@npm:1.15.6"
peerDependenciesMeta:
debug:
optional: true
checksum: a62c378dfc8c00f60b9c80cab158ba54e99ba0239a5dd7c81245e5a5b39d10f0c35e249c3379eae719ff0285fff88c365dd446fab19dee771f1d76252df1bbf5
languageName: node
linkType: hard

"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
Expand All @@ -6484,6 +6528,17 @@ __metadata:
languageName: node
linkType: hard

"form-data@npm:^4.0.0":
version: 4.0.0
resolution: "form-data@npm:4.0.0"
dependencies:
asynckit: ^0.4.0
combined-stream: ^1.0.8
mime-types: ^2.1.12
checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c
languageName: node
linkType: hard

"formdata-polyfill@npm:^4.0.10":
version: 4.0.10
resolution: "formdata-polyfill@npm:4.0.10"
Expand Down Expand Up @@ -9433,7 +9488,7 @@ __metadata:
languageName: node
linkType: hard

"mime-types@npm:2.1.35, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34":
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
Expand Down Expand Up @@ -10816,6 +10871,7 @@ __metadata:
"@release-it/conventional-changelog": ^5.0.0
"@types/jest": ^29.5.5
"@types/react": ^18.2.44
axios: ^1.6.7
commitlint: ^17.0.2
del-cli: ^5.1.0
eslint: ^8.51.0
Expand Down

0 comments on commit 0d30ee4

Please sign in to comment.