Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add function for creating server component #1

Merged
merged 4 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading