Skip to content

Commit 7e16636

Browse files
authored
feat(NODE-4938): improve react native bundle experience (#578)
1 parent 81227bf commit 7e16636

18 files changed

+4193
-17
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ bson.sublime-workspace
1818

1919
.vscode
2020

21-
lib
21+
/lib
2222
.nyc_output/
2323
coverage/
2424
*.d.ts

README.md

+4-11
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,16 @@ try {
188188

189189
## React Native
190190

191-
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:
191+
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:
192+
192193
```sh
193-
npm install --save react-native-get-random-values text-encoding-polyfill base-64
194+
npm install --save react-native-get-random-values
194195
```
195196

196197
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`.
197198

198199
```typescript
199200
// Required Polyfills For ReactNative
200-
import {encode, decode} from 'base-64';
201-
if (global.btoa == null) {
202-
global.btoa = encode;
203-
}
204-
if (global.atob == null) {
205-
global.atob = decode;
206-
}
207-
import 'text-encoding-polyfill';
208201
import 'react-native-get-random-values';
209202
```
210203

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

217-
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).)
210+
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).)
218211

219212
### Technical Note about React Native module import
220213

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import MagicString from 'magic-string';
2+
3+
const REQUIRE_POLYFILLS =
4+
`const { TextEncoder, TextDecoder } = require('../vendor/text-encoding');
5+
const { encode: btoa, decode: atob } = require('../vendor/base64');\n`
6+
7+
export class RequireVendor {
8+
/**
9+
* Take the compiled source code input; types are expected to already have been removed.
10+
* Add the TextEncoder, TextDecoder, atob, btoa requires.
11+
*
12+
* @param {string} code - source code of the module being transformed
13+
* @param {string} id - module id (usually the source file name)
14+
* @returns {{ code: string; map: import('magic-string').SourceMap }}
15+
*/
16+
transform(code, id) {
17+
if (!id.includes('web_byte_utils')) {
18+
return;
19+
}
20+
21+
// MagicString lets us edit the source code and still generate an accurate source map
22+
const magicString = new MagicString(code);
23+
magicString.prepend(REQUIRE_POLYFILLS);
24+
25+
return {
26+
code: magicString.toString(),
27+
map: magicString.generateMap({ hires: true })
28+
};
29+
}
30+
}

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"lib",
1111
"src",
1212
"bson.d.ts",
13-
"etc/prepare.js"
13+
"etc/prepare.js",
14+
"vendor"
1415
],
1516
"types": "bson.d.ts",
1617
"version": "5.3.0",
@@ -84,7 +85,7 @@
8485
"types": "./bson.d.ts",
8586
"default": "./lib/bson.cjs"
8687
},
87-
"react-native": "./lib/bson.cjs",
88+
"react-native": "./lib/bson.rn.cjs",
8889
"browser": "./lib/bson.mjs"
8990
},
9091
"compass:exports": {

rollup.config.mjs

+12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { nodeResolve } from '@rollup/plugin-node-resolve';
22
import typescript from '@rollup/plugin-typescript';
33
import { RequireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs';
4+
import { RequireVendor } from './etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs';
45

56
/** @type {typescript.RollupTypescriptOptions} */
67
const tsConfig = {
@@ -58,6 +59,17 @@ const config = [
5859
format: 'esm',
5960
sourcemap: true
6061
}
62+
},
63+
{
64+
input,
65+
plugins: [typescript(tsConfig), new RequireVendor(), nodeResolve({ resolveOnly: [] })],
66+
output: {
67+
file: 'lib/bson.rn.cjs',
68+
format: 'commonjs',
69+
exports: 'named',
70+
sourcemap: true
71+
},
72+
treeshake: false
6173
}
6274
];
6375

test/load_bson.js

+18
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ const commonGlobals = {
2323
}
2424
};
2525

26+
const rnGlobals = {
27+
require: require
28+
};
29+
30+
function loadReactNativeCJSModuleBSON(globals) {
31+
const filename = path.resolve(__dirname, `../lib/bson.rn.cjs`);
32+
const code = fs.readFileSync(filename, { encoding: 'utf8' });
33+
const context = vm.createContext({
34+
exports: Object.create(null),
35+
...rnGlobals,
36+
...globals
37+
});
38+
39+
vm.runInContext(code, context, { filename });
40+
return { context, exports: context.exports };
41+
}
42+
2643
function loadCJSModuleBSON(globals) {
2744
const filename = path.resolve(__dirname, `../lib/bson.cjs`);
2845
const code = fs.readFileSync(filename, { encoding: 'utf8' });
@@ -59,5 +76,6 @@ async function loadESModuleBSON(globals) {
5976

6077
module.exports = {
6178
loadCJSModuleBSON,
79+
loadReactNativeCJSModuleBSON,
6280
loadESModuleBSON
6381
};

test/node/byte_utils.test.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { types, inspect } from 'node:util';
22
import { expect } from 'chai';
33
import { isBufferOrUint8Array } from './tools/utils';
4+
import { Binary } from '../../src';
45
import { ByteUtils } from '../../src/utils/byte_utils';
56
import { nodeJsByteUtils } from '../../src/utils/node_byte_utils';
67
import { webByteUtils } from '../../src/utils/web_byte_utils';
78
import * as sinon from 'sinon';
8-
import { loadCJSModuleBSON, loadESModuleBSON } from '../load_bson';
9+
import { loadCJSModuleBSON, loadReactNativeCJSModuleBSON, loadESModuleBSON } from '../load_bson';
910
import * as crypto from 'node:crypto';
1011

1112
type ByteUtilTest<K extends keyof ByteUtils> = {
@@ -658,7 +659,7 @@ describe('ByteUtils', () => {
658659
}
659660
};
660661
consoleWarnSpy = sinon.spy(fakeConsole, 'warn');
661-
const { context, exports } = loadCJSModuleBSON({
662+
const { context, exports } = loadReactNativeCJSModuleBSON({
662663
crypto: null,
663664
// if we don't add a copy of Math here then we cannot spy on it for the test
664665
Math: {
@@ -693,6 +694,41 @@ describe('ByteUtils', () => {
693694
expect(randomSpy).to.have.callCount(16);
694695
});
695696
});
697+
698+
describe('react native uses vendored serialization', function () {
699+
let bsonWithNoCryptoAndRNProductMod;
700+
before(function () {
701+
const fakeConsole = {
702+
warn: () => {
703+
// ignore
704+
}
705+
};
706+
const { exports } = loadReactNativeCJSModuleBSON({
707+
crypto: null,
708+
// if we don't add a copy of Math here then we cannot spy on it for the test
709+
Math: {
710+
pow: Math.pow,
711+
floor: Math.floor,
712+
random: Math.random
713+
},
714+
console: fakeConsole,
715+
navigator: { product: 'ReactNative' }
716+
});
717+
718+
bsonWithNoCryptoAndRNProductMod = exports;
719+
});
720+
721+
after(function () {
722+
bsonWithNoCryptoAndRNProductMod = null;
723+
});
724+
725+
it('successfully serializes UTF8 and Base 64', () => {
726+
const serialize = bsonWithNoCryptoAndRNProductMod.BSON.serialize;
727+
expect(() => {
728+
serialize({ text: '😀', binary: new Binary('1234').toString('base64') });
729+
}).to.not.throw;
730+
});
731+
});
696732
});
697733

698734
for (const [byteUtilsName, byteUtils] of utils) {

test/node/release.test.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const REQUIRED_FILES = [
1717
'lib/bson.cjs.map',
1818
'lib/bson.mjs',
1919
'lib/bson.mjs.map',
20+
'lib/bson.rn.cjs',
21+
'lib/bson.rn.cjs.map',
2022
'package.json',
2123
'src/binary.ts',
2224
'src/bson_value.ts',
@@ -44,7 +46,17 @@ const REQUIRED_FILES = [
4446
'src/utils/byte_utils.ts',
4547
'src/utils/node_byte_utils.ts',
4648
'src/utils/web_byte_utils.ts',
47-
'src/validate_utf8.ts'
49+
'src/validate_utf8.ts',
50+
'vendor/base64/base64.js',
51+
'vendor/base64/package.json',
52+
'vendor/base64/LICENSE-MIT.txt',
53+
'vendor/base64/README.md',
54+
'vendor/text-encoding/lib/encoding-indexes.js',
55+
'vendor/text-encoding/lib/encoding.js',
56+
'vendor/text-encoding/index.js',
57+
'vendor/text-encoding/package.json',
58+
'vendor/text-encoding/LICENSE.md',
59+
'vendor/text-encoding/README.md'
4860
].map(f => `package/${f}`);
4961

5062
describe(`Release ${packFile}`, function () {

vendor/base64/LICENSE-MIT.txt

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright Mathias Bynens <https://mathiasbynens.be/>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

vendor/base64/README.md

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# 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)
2+
3+
_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.
4+
5+
## Installation
6+
7+
Via [npm](https://www.npmjs.com/):
8+
9+
```bash
10+
npm install base-64
11+
```
12+
13+
In a browser:
14+
15+
```html
16+
<script src="base64.js"></script>
17+
```
18+
19+
In [Narwhal](http://narwhaljs.org/), [Node.js](https://nodejs.org/), and [RingoJS](http://ringojs.org/):
20+
21+
```js
22+
var base64 = require('base-64');
23+
```
24+
25+
In [Rhino](http://www.mozilla.org/rhino/):
26+
27+
```js
28+
load('base64.js');
29+
```
30+
31+
Using an AMD loader like [RequireJS](http://requirejs.org/):
32+
33+
```js
34+
require(
35+
{
36+
'paths': {
37+
'base64': 'path/to/base64'
38+
}
39+
},
40+
['base64'],
41+
function(base64) {
42+
console.log(base64);
43+
}
44+
);
45+
```
46+
47+
## API
48+
49+
### `base64.version`
50+
51+
A string representing the semantic version number.
52+
53+
### `base64.encode(input)`
54+
55+
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).
56+
57+
```js
58+
var encodedData = base64.encode(input);
59+
```
60+
61+
To base64-encode any Unicode string, [encode it as UTF-8 first](https://github.com/mathiasbynens/utf8.js#utf8encodestring):
62+
63+
```js
64+
var base64 = require('base-64');
65+
var utf8 = require('utf8');
66+
67+
var text = 'foo © bar 𝌆 baz';
68+
var bytes = utf8.encode(text);
69+
var encoded = base64.encode(bytes);
70+
console.log(encoded);
71+
// → 'Zm9vIMKpIGJhciDwnYyGIGJheg=='
72+
```
73+
74+
### `base64.decode(input)`
75+
76+
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).
77+
78+
```js
79+
var decodedData = base64.decode(encodedData);
80+
```
81+
82+
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:
83+
84+
```js
85+
var encoded = 'Zm9vIMKpIGJhciDwnYyGIGJheg==';
86+
var bytes = base64.decode(encoded);
87+
var text = utf8.decode(bytes);
88+
console.log(text);
89+
// → 'foo © bar 𝌆 baz'
90+
```
91+
92+
## Support
93+
94+
_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.
95+
96+
## Unit tests & code coverage
97+
98+
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`.
99+
100+
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`.
101+
102+
To generate the code coverage report, use `grunt cover`.
103+
104+
## Author
105+
106+
| [![twitter/mathias](https://gravatar.com/avatar/24e08a9ea84deb17ae121074d0f17125?s=70)](https://twitter.com/mathias "Follow @mathias on Twitter") |
107+
|---|
108+
| [Mathias Bynens](https://mathiasbynens.be/) |
109+
110+
## License
111+
112+
_base64_ is available under the [MIT](https://mths.be/mit) license.

0 commit comments

Comments
 (0)