How to mock NativeModules in react-native project by js side? - react-native

I am working on runnning my react-native app with expo, the problem I am facing is to mock the NativeModules exported by "react-native", my current solution is using babel-plugin-module-resolver plugin to redirect the "react-native" module to my "react-native-proxy" module, which exports a proxy object that expanding NativeModules.
Everything goes well untill I want to use expo-file-system to mock our native FileSystem api, expo-file-system is an es module, an error look like caused by module mixed use occured. I tried import * as FileSystem from 'expo-file-system'; in babe.config.js, "react-native-proxy" module, metro.config.js, both throwed an error.
How could I require an es module in babel.config.js or babel-plugin?
Or any idea of achieving the target mentioned above?
Thanks.

Solved by myself.
As I know that, the NativeModules object exported from "react-native" is created in Libraries/BatchedBridge/NativeModules.js, so I change my mock target to this file. I create a file name "NativeModulesProxy.js" which content copied from "Libraries/BatchedBridge/NativeModules.js", then use babel-plugin-module-resolver plugin to redirect the src file to this one.
// babel.config.js
const resolvePath = require('babel-plugin-module-resolver').resolvePath;
const path = require('path');
...
plugins: [
[
'module-resolver',
{
resolvePath(sourcePath, currentFile, opts) {
if (sourcePath.includes('NativeModules')) {
const sourceAbsolutePath = path.resolve(path.dirname(currentFile), sourcePath);
if (sourceAbsolutePath.endsWith('node_modules/react-native/Libraries/BatchedBridge/NativeModules')) {
return path.resolve(__dirname, 'NativeModulesProxy');
}
}
return resolvePath(sourcePath, currentFile, opts);
}
}
]
],
...
NativeModules object is a quote of global.nativeModuleProxy, base on this, I could create my own proxy object like "global.myNativeModuleProxy" which combines global.nativeModuleProxy and my mock native modules.

Related

Web3js fails to import in Vue3 composition api project

I've created a brand new project with npm init vite bar -- --template vue. I've done an npm install web3 and I can see my package-lock.json includes this package. My node_modules directory also includes the web3 modules.
So then I added this line to main.js:
import { createApp } from 'vue'
import App from './App.vue'
import Web3 from 'web3' <-- This line
createApp(App).mount('#app')
And I get the following error:
I don't understand what is going on here. I'm fairly new to using npm so I'm not super sure what to Google. The errors are coming from node_modules/web3/lib/index.js, node_modules/web3-core/lib/index.js, node_modules/web3-core-requestmanager/lib/index.js, and finally node_modules/util/util.js. I suspect it has to do with one of these:
I'm using Vue 3
I'm using Vue 3 Composition API
I'm using Vue 3 Composition API SFC <script setup> tag (but I imported it in main.js so I don't think it is this one)
web3js is in Typescript and my Vue3 project is not configured for Typescript
But as I am fairly new to JavaScript and Vue and Web3 I am not sure how to focus my Googling on this error. My background is Python, Go, Terraform. Basically the back end of the back end. Front end JavaScript is new to me.
How do I go about resolving this issue?
Option 1: Polyfill Node globals/modules
Polyfilling the Node globals and modules enables the web3 import to run in the browser:
Install the ESBuild plugins that polyfill Node globals/modules:
npm i -D #esbuild-plugins/node-globals-polyfill
npm i -D #esbuild-plugins/node-modules-polyfill
Configure optimizeDeps.esbuildOptions to use these ESBuild plugins.
Configure define to replace global with globalThis (the browser equivalent).
import { defineConfig } from 'vite'
import GlobalsPolyfills from '#esbuild-plugins/node-globals-polyfill'
import NodeModulesPolyfills from '#esbuild-plugins/node-modules-polyfill'
export default defineConfig({
⋮
optimizeDeps: {
esbuildOptions: {
2️⃣
plugins: [
NodeModulesPolyfills(),
GlobalsPolyfills({
process: true,
buffer: true,
}),
],
3️⃣
define: {
global: 'globalThis',
},
},
},
})
demo 1
Note: The polyfills add considerable size to the build output.
Option 2: Use pre-bundled script
web3 distributes a bundled script at web3/dist/web3.min.js, which can run in the browser without any configuration (listed as "pure js"). You could configure a resolve.alias to pull in that file:
import { defineConfig } from 'vite'
export default defineConfig({
⋮
resolve: {
alias: {
web3: 'web3/dist/web3.min.js',
},
// or
alias: [
{
find: 'web3',
replacement: 'web3/dist/web3.min.js',
},
],
},
})
demo 2
Note: This option produces 469.4 KiB smaller output than Option 1.
You can avoid the Uncaught ReferenceError: process is not defined error by adding this in your vite config
export default defineConfig({
// ...
define: {
'process.env': process.env
}
})
I found the best solution.
The problem is because you lose window.process variable, and process exists only on node, not the browser.
So you should inject it to browser when the app loads.
Add this line to your app:
window.process = {
...window.process,
};

Shorter Way for URLs with vscode on react native

So I have a react native project, and in that project many of my urls start looking like this: import Component from '../../component/file';
So after this problem I saw this video by fireshipio with says I can shorten it by adding a jscofig.json file but it did not work when I wrote import Component from '../../component/file';
it just told me it could not find the path please tell me what I am supposed to do to make this working because if its possible my links will become so much shorter and smarter. Remember the programming rule do not repeat yourself so please help me follow that.
link to fireshipio vid: https://www.youtube.com/watch?v=WpgZKBtW_t8
You should Modify/Add your desired common path in babel.config.js file and then you can easily import any file/class without adding long paths
Here is an example of babel.config.js from one of my project.
module.exports = api => {
api.cache(true);
return {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
'#babel/plugin-proposal-optional-chaining',
'#babel/plugin-proposal-nullish-coalescing-operator',
[
'module-resolver',
{
root: ['./src'],
alias: {
'#routes': ['./src/routes.js'],
'#navigations': ['./src/navigations'],
'#components': ['./src/components'],
'#store': ['./src/store'],
'#images': ['./src/images'], //You can add your source path like this
'#utils': ['./src/utils'],
},
},
],
],
};
};
After adding the source path in babel.config.js you can import the files like this in your class.
import SampleImage from '#images/sampleImage.png'
You can import like this in your any class, No need to do '../../src/image/sampleImage.png'

How to set up environment variables in react native

I'm making a react native app that makes a request to my server hosted on heroku.
Should I be hiding the URL of my server and if so how can I add an environment variable to a react native project?
I have made a .env file and then have done this:
console.log(process.env.URL)
Which is returning undefined - I am also using expo if that makes a difference.
If you use Expo, there is an easy way to create environment variables.
In your app.json file
{
"expo": {
"extra": {
"URL": "https://..."
}
}
}
After that, you will need to install the expo-constant package.
expo install expo-constants
And, to get the info in your app:
import Constants from "expo-constants";
console.log(Constants.manifest.extra.URL);
One library that I like to use that works for bare react native and expo is react-native-dotenv
Install it npm i react-native-dotenv
Add a .env file with your variables
MY_ENV_VARIABLE=SECRET_PASSWORD
Add the babel plugin to .babelrc file.
{
"plugins": [
["module:react-native-dotenv"]
]
}
Import and use it
import { MY_ENV_VARIABLE } from "react-native-dotenv";
function doSomething() {
console.log(MY_ENV_VARIABLE);
}

how to solve the error "Require statement not part of import statement" in using storybook?

I'm using storybook in vue.js.
I tried to import my component but it was failed, because Sass which imported in my component made error.
(component which is not importing Sass can imported and displayed.)
so, I check official description and created a .storybook/main.js file to add Sass support.
But i faced a unexpected error, "Require statement not part of import statement" in using storybook" at const path = require('path').
I couldn't find example of this error related storybook, so i'm confusing.
how to solve this error?
I just started using storybook, so i don't know which file info is needed to solve this.
if need other file info, I'll add it.
My main.js
const path = require('path');
// Export a function. Accept the base config as the only param.
module.exports = {
webpackFinal: async ( config ) => {
// Make whatever fine-grained changes you need
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../'),
});
// Return the altered config
return config;
},
};
official description of using scss in storybook

pass environment variables during babel build phase for importing different files

I'm building a web (react with webpack & babel) and mobile apps (react-native with expo) for a project. I therefore created a common library for business logic and redux/api library.
Some code will be slightly different between web and mobile. In my case it's localStorage vs AsyncStorage, which I use for authentication among other things...
I'm trying to pass an environment variable for the build stage to switch import of certain files so that the correct file is loaded for each build which are simply path linked (ie no pre-build of my library, I just do import '../mylib') ex:
if(PLATFORM === 'mobile'){
import StorageModule from './mobile-storage-module`
} else {
import StorageModule from './mobile-storage-module`
}
export default StorageModule
Try 1
#babel/preset-env to say if it's mobile or web so that it imports different libraries depending on build like so:
My .babelrc has this:
{
"presets": [
[
"#babel/preset-env",
{
"platform": "mobile"
}
]
]
}
And then in local storage file I do this:
export default () => {
const platform = process.env.platform
if (platform === 'mobile') {
return import './storage-modules/storage-mobile'
}
return import './storage-modules/storage-web'
}
That didn't work, and this also didn't work for me.
Try 2
I installed react-native-dotenv and created a .env file with:
PLATFORM=mobile
And set the plugin in my .babelrc:
{
"presets": [
"babel-preset-expo",
"react-native-dotenv"
]
}
And in my example file, I tried this:
import { PLATFORM } from 'react-native-dotenv'
export default PLATFORM === 'mobile' ? import './storage-modules/storage-mobile' : import './storage-modules/storage-web'
But now my build doesn't work. Any idea how I do dynamic imports during the build process that works for babel in react-native app and webpack build (also uses babel)?
First, #babel/preset-env does not do what you think it does. This is not for specifying your own variables, it is a plugin to automatically use the right target and pollyfills for the browsers you want to support.
The easiest way to get environment variables is with the webpack define plugin (which is part of webpack, so no need to install anything extra)
Just add this to your webpack config.
plugins: [
new webpack.DefinePlugin({
'process.env': {
platform: 'mobile',
},
}),
],
Next, you can't use normal import statements inside of ifs.
import gets resolved before any code runs, either on build by webpack, or in supported environments on script load.
To import something on runtime, you need to use dynamic imports.
Here is an example of how this could look like.
export default new Promise(async resolve => {
resolve(
process.env.platform === 'mobile'
? (await import('./mobile.js')).default
: (await import('./desktop.js')).default
);
});
You can now import from this file like you normally would, but be aware that the default export is a promise.
As your question's title says "during babel build phase", I assume you would like to make different builds for desktop and mobile (not one build for both and load the needed modules dynamically run-time). So I would go like this:
Define the run scripts in package.json for desktop and mobile:
"scripts": {
"devmobile": "cross-env NODE_ENV=development PLATFORM=mobile webpack --progress",
"dev": "cross-env NODE_ENV=development webpack --progress",
}
... or you can create two different webpack.config.js files for desktop and mobile builds but I think the above is easier...
Then npm run devmobile to build for mobile and npm run dev for desktop.
Since I'm on Windows I use the cross-env package but this is the recommended way to be OS independent.
Then I would use Webpack's NormalModuleReplacementPlugin:
(based on this exmaple)
In your webpack.config.js:
// defining the wanted platform for the build (comfing form the npm run script)
const targetPlatform = process.env.PLATFORM || 'desktop';
// then use the plugin like this
plugins: [
new webpack.NormalModuleReplacementPlugin(/(.*)-PLATFORM(\.*)/, function(resource) {
resource.request = resource.request.replace(/-PLATFORM/, `-${targetPlatform}`);
}),
]
...then if you have these two files:
./storage-modules/storage-mobile.js
./storage-modules/storage-desktop.js
import the needed one in your script like this:
import './storage-modules/storage-PLATFORM';
This way the generated build will only contain the needed file for the current PLATFORM used for the build process.
Another possible solution could be the ifdef-loader but I haven't tested it. Maybe worth to try, seems easy.
If you want one build though and import the needed module dynamically, you could do something like this in your app.js (or whatever):
// this needs to have defined when the app is running
const targetPlatform = process.env.PLATFORM || 'desktop';
import(
/* webpackChunkName: "[request]" */
`./storage-modules/storage-${targetPlatform}`
).then(storageModule => {
// use the loaded module
});
or:
(async () => {
const storageModule = await import(
/* webpackChunkName: "[request]" */
`./storage-modules/storage-${targetPlatform}`
);
// use the loaded module
})();
For this to work Babel has to be configured.
More on Webpack with dynamic imports here.
You can use transform-inline-environment-variablesto pass platform to babel
"build-mobile": "PLATFORM=mobile ...",
"build-app": "PLATFORM=app ...",