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(NODE-4938): improve react native bundle experience #578

Merged
merged 6 commits into from
Jun 7, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ bson.sublime-workspace

.vscode

lib
/lib
.nyc_output/
coverage/
*.d.ts
Expand Down
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,16 @@ try {

## React Native

BSON requires that `TextEncoder`, `TextDecoder`, `atob`, `btoa`, and `crypto.getRandomValues` are available globally. These are present in most Javascript runtimes but require polyfilling in React Native. Polyfills for the missing functionality can be installed with the following command:
BSON vendors the required polyfills for `TextEncoder`, `TextDecoder`, `atob`, `btoa` imported from React Native and therefore doesn't expect users to polyfill these. One additional polyfill, `crypto.getRandomValues` is recommended and can be installed with the following command:

```sh
npm install --save react-native-get-random-values text-encoding-polyfill base-64
npm install --save react-native-get-random-values
```

The following snippet should be placed at the top of the entrypoint (by default this is the root `index.js` file) for React Native projects using the BSON library. These lines must be placed for any code that imports `BSON`.

```typescript
// Required Polyfills For ReactNative
import {encode, decode} from 'base-64';
if (global.btoa == null) {
global.btoa = encode;
}
if (global.atob == null) {
global.atob = decode;
}
import 'text-encoding-polyfill';
import 'react-native-get-random-values';
```

Expand All @@ -214,7 +207,7 @@ Finally, import the `BSON` library like so:
import { BSON, EJSON } from 'bson';
```

This will cause React Native to import the `node_modules/bson/lib/bson.cjs` bundle (see the `"react-native"` setting we have in the `"exports"` section of our [package.json](./package.json).)
This will cause React Native to import the `node_modules/bson/lib/bson.rn.cjs` bundle (see the `"react-native"` setting we have in the `"exports"` section of our [package.json](./package.json).)

### Technical Note about React Native module import

Expand Down
30 changes: 30 additions & 0 deletions etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import MagicString from 'magic-string';

const REQUIRE_POLYFILLS =
`const { TextEncoder, TextDecoder } = require('../vendor/text-encoding');
const { encode: btoa, decode: atob } = require('../vendor/base64');\n`

export class RequireVendor {
/**
* Take the compiled source code input; types are expected to already have been removed.
* Add the TextEncoder, TextDecoder, atob, btoa requires.
*
* @param {string} code - source code of the module being transformed
* @param {string} id - module id (usually the source file name)
* @returns {{ code: string; map: import('magic-string').SourceMap }}
*/
transform(code, id) {
if (!id.includes('web_byte_utils')) {
return;
}

// MagicString lets us edit the source code and still generate an accurate source map
const magicString = new MagicString(code);
magicString.prepend(REQUIRE_POLYFILLS);

return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true })
};
}
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"lib",
"src",
"bson.d.ts",
"etc/prepare.js"
"etc/prepare.js",
"vendor"
],
"types": "bson.d.ts",
"version": "5.3.0",
Expand Down Expand Up @@ -84,7 +85,7 @@
"types": "./bson.d.ts",
"default": "./lib/bson.cjs"
},
"react-native": "./lib/bson.cjs",
"react-native": "./lib/bson.rn.cjs",
"browser": "./lib/bson.mjs"
},
"compass:exports": {
Expand Down
12 changes: 12 additions & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import { RequireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs';
import { RequireVendor } from './etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs';

/** @type {typescript.RollupTypescriptOptions} */
const tsConfig = {
Expand Down Expand Up @@ -58,6 +59,17 @@ const config = [
format: 'esm',
sourcemap: true
}
},
{
input,
plugins: [typescript(tsConfig), new RequireVendor(), nodeResolve({ resolveOnly: [] })],
output: {
file: 'lib/bson.rn.cjs',
format: 'commonjs',
exports: 'named',
sourcemap: true
},
treeshake: false
}
];

Expand Down
18 changes: 18 additions & 0 deletions test/load_bson.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ const commonGlobals = {
}
};

const rnGlobals = {
require: require
};

function loadReactNativeCJSModuleBSON(globals) {
const filename = path.resolve(__dirname, `../lib/bson.rn.cjs`);
const code = fs.readFileSync(filename, { encoding: 'utf8' });
const context = vm.createContext({
exports: Object.create(null),
...rnGlobals,
...globals
});

vm.runInContext(code, context, { filename });
return { context, exports: context.exports };
}

function loadCJSModuleBSON(globals) {
const filename = path.resolve(__dirname, `../lib/bson.cjs`);
const code = fs.readFileSync(filename, { encoding: 'utf8' });
Expand Down Expand Up @@ -59,5 +76,6 @@ async function loadESModuleBSON(globals) {

module.exports = {
loadCJSModuleBSON,
loadReactNativeCJSModuleBSON,
loadESModuleBSON
};
40 changes: 38 additions & 2 deletions test/node/byte_utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { types, inspect } from 'node:util';
import { expect } from 'chai';
import { isBufferOrUint8Array } from './tools/utils';
import { Binary } from '../../src';
import { ByteUtils } from '../../src/utils/byte_utils';
import { nodeJsByteUtils } from '../../src/utils/node_byte_utils';
import { webByteUtils } from '../../src/utils/web_byte_utils';
import * as sinon from 'sinon';
import { loadCJSModuleBSON, loadESModuleBSON } from '../load_bson';
import { loadCJSModuleBSON, loadReactNativeCJSModuleBSON, loadESModuleBSON } from '../load_bson';
import * as crypto from 'node:crypto';

type ByteUtilTest<K extends keyof ByteUtils> = {
Expand Down Expand Up @@ -658,7 +659,7 @@ describe('ByteUtils', () => {
}
};
consoleWarnSpy = sinon.spy(fakeConsole, 'warn');
const { context, exports } = loadCJSModuleBSON({
const { context, exports } = loadReactNativeCJSModuleBSON({
crypto: null,
// if we don't add a copy of Math here then we cannot spy on it for the test
Math: {
Expand Down Expand Up @@ -693,6 +694,41 @@ describe('ByteUtils', () => {
expect(randomSpy).to.have.callCount(16);
});
});

describe('react native uses vendored serialization', function () {
let bsonWithNoCryptoAndRNProductMod;
before(function () {
const fakeConsole = {
warn: () => {
// ignore
}
};
const { exports } = loadReactNativeCJSModuleBSON({
crypto: null,
// if we don't add a copy of Math here then we cannot spy on it for the test
Math: {
pow: Math.pow,
floor: Math.floor,
random: Math.random
},
console: fakeConsole,
navigator: { product: 'ReactNative' }
});

bsonWithNoCryptoAndRNProductMod = exports;
});

after(function () {
bsonWithNoCryptoAndRNProductMod = null;
});

it('successfully serializes UTF8 and Base 64', () => {
const serialize = bsonWithNoCryptoAndRNProductMod.BSON.serialize;
expect(() => {
serialize({ text: '😀', binary: new Binary('1234').toString('base64') });
}).to.not.throw;
});
});
});

for (const [byteUtilsName, byteUtils] of utils) {
Expand Down
14 changes: 13 additions & 1 deletion test/node/release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const REQUIRED_FILES = [
'lib/bson.cjs.map',
'lib/bson.mjs',
'lib/bson.mjs.map',
'lib/bson.rn.cjs',
'lib/bson.rn.cjs.map',
'package.json',
'src/binary.ts',
'src/bson_value.ts',
Expand Down Expand Up @@ -44,7 +46,17 @@ const REQUIRED_FILES = [
'src/utils/byte_utils.ts',
'src/utils/node_byte_utils.ts',
'src/utils/web_byte_utils.ts',
'src/validate_utf8.ts'
'src/validate_utf8.ts',
'vendor/base64/base64.js',
'vendor/base64/package.json',
'vendor/base64/LICENSE-MIT.txt',
'vendor/base64/README.md',
'vendor/text-encoding/lib/encoding-indexes.js',
'vendor/text-encoding/lib/encoding.js',
'vendor/text-encoding/index.js',
'vendor/text-encoding/package.json',
'vendor/text-encoding/LICENSE.md',
'vendor/text-encoding/README.md'
].map(f => `package/${f}`);

describe(`Release ${packFile}`, function () {
Expand Down
20 changes: 20 additions & 0 deletions vendor/base64/LICENSE-MIT.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright Mathias Bynens <https://mathiasbynens.be/>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
112 changes: 112 additions & 0 deletions vendor/base64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# base64 [![Build status](https://travis-ci.org/mathiasbynens/base64.svg?branch=master)](https://travis-ci.org/mathiasbynens/base64) [![Code coverage status](http://img.shields.io/coveralls/mathiasbynens/base64/master.svg)](https://coveralls.io/r/mathiasbynens/base64)

_base64_ is a robust base64 encoder/decoder that is fully compatible with [`atob()` and `btoa()`](https://html.spec.whatwg.org/multipage/webappapis.html#atob), written in JavaScript. The base64-encoding and -decoding algorithms it uses are fully [RFC 4648](https://tools.ietf.org/html/rfc4648#section-4) compliant.

## Installation

Via [npm](https://www.npmjs.com/):

```bash
npm install base-64
```

In a browser:

```html
<script src="base64.js"></script>
```

In [Narwhal](http://narwhaljs.org/), [Node.js](https://nodejs.org/), and [RingoJS](http://ringojs.org/):

```js
var base64 = require('base-64');
```

In [Rhino](http://www.mozilla.org/rhino/):

```js
load('base64.js');
```

Using an AMD loader like [RequireJS](http://requirejs.org/):

```js
require(
{
'paths': {
'base64': 'path/to/base64'
}
},
['base64'],
function(base64) {
console.log(base64);
}
);
```

## API

### `base64.version`

A string representing the semantic version number.

### `base64.encode(input)`

This function takes a byte string (the `input` parameter) and encodes it according to base64. The input data must be in the form of a string containing only characters in the range from U+0000 to U+00FF, each representing a binary byte with values `0x00` to `0xFF`. The `base64.encode()` function is designed to be fully compatible with [`btoa()` as described in the HTML Standard](https://html.spec.whatwg.org/multipage/webappapis.html#dom-windowbase64-btoa).

```js
var encodedData = base64.encode(input);
```

To base64-encode any Unicode string, [encode it as UTF-8 first](https://github.com/mathiasbynens/utf8.js#utf8encodestring):

```js
var base64 = require('base-64');
var utf8 = require('utf8');

var text = 'foo © bar 𝌆 baz';
var bytes = utf8.encode(text);
var encoded = base64.encode(bytes);
console.log(encoded);
// → 'Zm9vIMKpIGJhciDwnYyGIGJheg=='
```

### `base64.decode(input)`

This function takes a base64-encoded string (the `input` parameter) and decodes it. The return value is in the form of a string containing only characters in the range from U+0000 to U+00FF, each representing a binary byte with values `0x00` to `0xFF`. The `base64.decode()` function is designed to be fully compatible with [`atob()` as described in the HTML Standard](https://html.spec.whatwg.org/multipage/webappapis.html#dom-windowbase64-atob).

```js
var decodedData = base64.decode(encodedData);
```

To base64-decode UTF-8-encoded data back into a Unicode string, [UTF-8-decode it](https://github.com/mathiasbynens/utf8.js#utf8decodebytestring) after base64-decoding it:

```js
var encoded = 'Zm9vIMKpIGJhciDwnYyGIGJheg==';
var bytes = base64.decode(encoded);
var text = utf8.decode(bytes);
console.log(text);
// → 'foo © bar 𝌆 baz'
```

## Support

_base64_ is designed to work in at least Node.js v0.10.0, Narwhal 0.3.2, RingoJS 0.8-0.9, PhantomJS 1.9.0, Rhino 1.7RC4, as well as old and modern versions of Chrome, Firefox, Safari, Opera, and Internet Explorer.

## Unit tests & code coverage

After cloning this repository, run `npm install` to install the dependencies needed for development and testing. You may want to install Istanbul _globally_ using `npm install istanbul -g`.

Once that’s done, you can run the unit tests in Node using `npm test` or `node tests/tests.js`. To run the tests in Rhino, Ringo, Narwhal, and web browsers as well, use `grunt test`.

To generate the code coverage report, use `grunt cover`.

## Author

| [![twitter/mathias](https://gravatar.com/avatar/24e08a9ea84deb17ae121074d0f17125?s=70)](https://twitter.com/mathias "Follow @mathias on Twitter") |
|---|
| [Mathias Bynens](https://mathiasbynens.be/) |

## License

_base64_ is available under the [MIT](https://mths.be/mit) license.