Skip to content

Commit

Permalink
feat(Platform): Add support for react-native platform (#593)
Browse files Browse the repository at this point in the history
  • Loading branch information
nugmanoff authored Jun 9, 2024
1 parent b6cecb1 commit 2980a60
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 3 deletions.
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"web": [
"./dist/src/platform/lib.d.ts"
],
"react-native": [
"./dist/src/platform/lib.d.ts"
],
"web.bundle": [
"./dist/src/platform/lib.d.ts"
],
Expand All @@ -32,6 +35,7 @@
"deno": "./dist/src/platform/deno.js",
"types": "./dist/src/platform/lib.d.ts",
"browser": "./dist/src/platform/web.js",
"react-native": "./dist/src/platform/react-native.js",
"default": "./dist/src/platform/web.js"
},
"./agnostic": {
Expand All @@ -42,6 +46,10 @@
"types": "./dist/src/platform/lib.d.ts",
"default": "./dist/src/platform/web.js"
},
"./react-native": {
"types": "./dist/src/platform/lib.d.ts",
"default": "./dist/src/platform/react-native.js"
},
"./web.bundle": {
"types": "./dist/src/platform/lib.d.ts",
"default": "./bundle/browser.js"
Expand Down Expand Up @@ -75,14 +83,15 @@
"test": "npx jest --verbose",
"lint": "npx eslint ./src",
"lint:fix": "npx eslint --fix ./src",
"clean": "npx rimraf ./dist/src ./dist/package.json ./bundle/browser.js ./bundle/browser.js.map ./bundle/browser.min.js ./bundle/browser.min.js.map ./bundle/node.cjs ./bundle/node.cjs.map ./bundle/cf-worker.js ./bundle/cf-worker.js.map ./deno",
"build": "npm run clean && npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod && npm run bundle:cf-worker",
"clean": "npx rimraf ./dist/src ./dist/package.json ./bundle/browser.js ./bundle/browser.js.map ./bundle/browser.min.js ./bundle/browser.min.js.map ./bundle/node.cjs ./bundle/node.cjs.map ./bundle/cf-worker.js ./bundle/cf-worker.js.map ./bundle/react-native.js ./bundle/react-native.js.map ./deno",
"build": "npm run clean && npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod && npm run bundle:cf-worker && npm run bundle:react-native",
"build:parser-map": "node ./dev-scripts/gen-parser-map.mjs",
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
"build:esm": "npx tspc",
"build:deno": "npx cpy ./src ./deno && npx esbuild ./src/utils/DashManifest.tsx --keep-names --format=esm --platform=neutral --target=es2020 --outfile=./deno/src/utils/DashManifest.js && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.ts';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./deno -r",
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/browser.js --platform=browser",
"bundle:react-native": "npx esbuild ./dist/src/platform/react-native.js --bundle --target=es2020 --keep-names --format=esm --platform=neutral --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/react-native.js",
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
"bundle:cf-worker": "npx esbuild ./dist/src/platform/cf-worker.js --banner:js=\"/* eslint-disable */\" --bundle --target=es2020 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/cf-worker.js --platform=node",
"prepare": "npm run build",
Expand Down
1 change: 1 addition & 0 deletions src/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ We provide shims for the following platforms:
- Modern Browsers
- Node.js
- Deno
- [React-Native](./react-native.md)

## Contributing Support for a New Platform

Expand Down
98 changes: 98 additions & 0 deletions src/platform/react-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
Making the `Youtube.js` work in React-Native involves polyfilling number of APIs and using MMKV as an underlying storage for cache.
Below configuration is tested with `"react-native": "0.73.2`

Following polyfills are required to be installed:

```
"base-64": "^1.0.0",
"event-target-polyfill": "^0.0.4",
"react-native-mmkv": "^2.11.0",
"react-native-url-polyfill": "^2.0.0",
"text-encoding-polyfill": "^0.6.7",
"web-streams-polyfill": "^3.3.2"
```

And following `devDependencies`:
```
"@types/base-64": "^1.0.2",
"@babel/plugin-syntax-import-attributes": "^7.23.3",
"@babel/plugin-transform-export-namespace-from": "^7.23.4",
```

Adding `unstable_enablePackageExports: true` flag to Metro is required as well, because `Youtube.js` uses package exports feature in `package.json`.

`metro.config.js`:
```js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const config = {
resolver: {
sourceExts: ['jsx', 'js', 'ts', 'tsx', 'cjs', 'json', 'd.ts'],
unstable_enablePackageExports: true,
},
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```

`babel.config.js`:
```js
module.exports = {
plugins: [
['@babel/plugin-syntax-import-attributes', {deprecatedAssertSyntax: true}],
'@babel/plugin-transform-export-namespace-from',
],
presets: ['module:@react-native/babel-preset'],
};
```

Below is the sample file that loads all of the polyfills, makes `MMKV` storage globally available (to be used by `Cache` in `Youtube.js`) and inits the `Innertube` instance.

```typescript
// === START === Making Youtube.js work
import 'event-target-polyfill';
import 'web-streams-polyfill';
import 'text-encoding-polyfill';
import 'react-native-url-polyfill/auto';
import {decode, encode} from 'base-64';

if (!global.btoa) {
global.btoa = encode;
}

if (!global.atob) {
global.atob = decode;
}

import {MMKV} from 'react-native-mmkv';
// @ts-expect-error to avoid typings' fuss
global.mmkvStorage = MMKV as any;

// See https://github.com/nodejs/node/issues/40678#issuecomment-1126944677
class CustomEvent extends Event {
#detail;

constructor(type: string, options?: CustomEventInit<any[]>) {
super(type, options);
this.#detail = options?.detail ?? null;
}

get detail() {
return this.#detail;
}
}

global.CustomEvent = CustomEvent as any;

// === END === Making Youtube.js work

import Innertube, {UniversalCache} from 'youtubei.js';

let innertube: Promise<Innertube> = Innertube.create({
cache: new UniversalCache(false),
generate_session_locally: true,
});

export default innertube;

```
79 changes: 79 additions & 0 deletions src/platform/react-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// React Native Platform Support
import type { ICache } from '../types/Cache.js';
import { Platform } from '../utils/Utils.js';
import sha1Hash from './polyfills/web-crypto.js';
import package_json from '../../package.json' assert { type: 'json' };
import evaluate from './jsruntime/jinter.js';

class Cache implements ICache {
#persistent_directory: string;
#persistent: boolean;

constructor(persistent = false, persistent_directory?: string) {
this.#persistent_directory = persistent_directory || '';
this.#persistent = persistent;
}

get cache_dir() {
return this.#persistent ? this.#persistent_directory : '';
}

#getStorage() {
const storage = new ((globalThis as any).mmkvStorage as any)({ id: 'InnertubeCache' });
return storage;
}

async get(key: string) {
const storage = this.#getStorage();
return storage.getBuffer(key)?.buffer;
}

async set(key: string, value: ArrayBuffer) {
const storage = this.#getStorage();
storage.set(key, new Uint8Array(value));
}

async remove(key: string) {
const storage = this.#getStorage();
storage.delete(key);
}
}

Platform.load({
runtime: 'react-native',
server: false,
info: {
version: package_json.version,
bugs_url: package_json.bugs.url,
repo_url: package_json.homepage.split('#')[0]
},
Cache: Cache,
sha1Hash,
uuidv4() {
if (globalThis.crypto?.randomUUID()) {
return globalThis.crypto.randomUUID();
}

// See https://stackoverflow.com/a/2117523
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (cc) => {
const c = parseInt(cc);
return (
c ^
(window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16);
});
},
eval: evaluate,
fetch: globalThis.fetch,
Request: globalThis.Request,
Response: globalThis.Response,
Headers: globalThis.Headers,
FormData: globalThis.FormData,
File: globalThis.File,
ReadableStream: globalThis.ReadableStream,
CustomEvent: globalThis.CustomEvent
});

export * from './lib.js';
import Innertube from './lib.js';
export default Innertube;
2 changes: 1 addition & 1 deletion src/types/PlatformShim.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ICacheConstructor } from './Cache.js';

export type Runtime = 'deno' | 'node' | 'browser' | 'cf-worker' | 'unknown';
export type Runtime = 'deno' | 'node' | 'browser' | 'cf-worker' | 'unknown' | 'react-native';

export type FetchFunction = typeof fetch;

Expand Down

0 comments on commit 2980a60

Please sign in to comment.