TheE
TheE

Reputation: 388

Import .js file as text and use it inside WebView injectedJavaScript in react-native expo

I have a file content.js that includes some JavaScript code that I want to inject inside a WebView using injectedJavaScript.

I tried:

    fetch('./content.js').then((result) => {
      result = result.text();
      this.setState(previousState => (
        {contentScript: result}
      ));
    });

But it doesn't get the right file.

const contentScript = require('./content.js');

This works, but it evals the JavaScript straight away and I can't seem to find a way to convert it to string before it gets executed.

A solution is to just make copy the code of content.js into a string, but that would be pretty annoying when I want to edit the code...

Does anyone know a better solution for this?
I still have no solution to this for almost a week. :(

Upvotes: 9

Views: 2291

Answers (3)

Umair A.
Umair A.

Reputation: 6873

UPDATE: I ran into some issues and switched to using Rollup instead of Webpack. I now have 2 rollup config files for each platform.

I created a separate project and then used npm install project-scripts to achieve that. In the new npm project, I used webpack and a custom webpack plugin which reads the contents of all files in src/.js and src/.d.ts and exports them to dist/index.js and dist/index.d.ts.

Here's the plugin.

const path = require('path');

class FileContentsPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('FileContentsPlugin', (compilation, callback) => {
            let content = '';
            for (let fileName of Object.keys(compilation.assets)) {
                if (fileName.endsWith('.js')) {
                    content += `export const ${path.parse(fileName).name} = \`${compilation.assets[fileName]._value}\`;\n`;
                }
            }
            compilation.assets['index.js'] = {
                source: () => content,
                size: () => content.length,
            };

            callback();
        });
    }
}

module.exports = FileContentsPlugin;

And webpack.config.js

const path = require('path');
const glob = require('glob');
const FileContentsPlugin = require('./file-contents-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const entryFiles = glob
    .sync('./src/*.js')
    .reduce((entryObj, currentValue) => {
        const parsedPath = path.parse(path.relative('./src', currentValue));
        const entryValue = `${parsedPath.dir}${path.sep}${parsedPath.name}`;
        return {
            ...entryObj,
            [entryValue]: currentValue,
        };
    }, {});

module.exports = {
    mode: process.env.NODE_ENV || 'development',
    entry: entryFiles,
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ['@babel/preset-env', { targets: "defaults" }]
                        ],
                        plugins: ['@babel/plugin-transform-runtime']
                    }
                },
            },
        ],
    },
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                { from: './package.json', to: '.' },
                { from: '**/*.d.ts', to: '.', context: path.resolve(__dirname, './src'), },
            ],
        }),
        new FileContentsPlugin(),
    ],
};

Now run npx webpack to generate dist/ files.

Note: React Native and/or Expo don't work with npm linked packages so I had to add it to package.json and then use the cp command manually. Once your project is in production, you won't need to do that.

cd current-project
cp -r ../project-scripts/dist/* node_modules/project-scripts

Now you can import your scripts as strings for webview.

import { Script1 } from 'project-scripts'

<WebView
    source={{ uri: 'https://google.com' }}
    javaScriptEnabled={true}
    injectedJavaScript={Script1}
    onMessage={console.log}
></WebView>

In production, you need to use npmjs.com or verdaccio.org if you want local and private experience.

Upvotes: 0

James Batchelor
James Batchelor

Reputation: 129

Important update re: the below

I have egg on my face, it turns out the below does not work in the default Metro bundler (I thought it was bult on top of Webpack, it isn't) and in my situation, it gave the illusion for a while it was working. The following will work if using Webpack to bundle for the web, which React Native does support, (but would be pointless for this scenario as WebView doesn't support the web anyway), or if using another bundler based on webpack with support for loaders, like the currently in development RePack (Note: this doesn't support Expo yet). I will leave my answer below for reference in case anyone using RePack or a future bundler comes along.

My workaround for now involves making a custom build pipeline in my package.json to copy all my files to a build folder and then transform files with a .raw.js extension to a string and precede it with export default using a script before executing the real build on that folder.

(June 2023) An expansion of @deathangel908's answer

For those that don't want to specify the loader every time they import a given file, you can use pattern matching in your webpack.config.js file. I renamed the JS file I wanted to inject to myscript.raw.js - your editor, eslint, etc will still see it as a regular js file but you can now configure webpack to recognise any filename ending with .raw.js and use the raw loader:

(make sure you install it with npm i --save-dev raw-loader)

// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.raw.js$/i,
        use: "raw-loader",
      },
    ],
  },
}

You can now import the file as you would with any other JS file, but it will be treated as a string:

// App.js

import myscript from "./myscript.raw.js" // myscript is just a string!

You may recieve a strange error at runtime if using the string directly: error while updating property injectedJavaScript, to counteract this, just wrap the variable in a string literal:

// App.js

<WebView
      // ...
      injectedJavaScript={`${myscript}`}
/>

Important info for TypeScript users

First, you'll want to add your .raw.js files to your typescript exclude block as these injectable files can only be pure JS and you don't want TypeScript to throw annyoing errors:

// tsconfig.json

{
    // ...
    "exclude": [
        "node_modules",
        "babel.config.js",
        "metro.config.js",
        "*.raw.js"
    ]
    // ...
}

Secondly, TypeScript will throw an error on your import: Could not find a declaration file for module 'myscript'. '/path/to/myscript.raw.js' implicitly has an 'any' type To fix this you must create a declarations (.d.ts) file somewhere in your source, I just made a file in the root of my repository called declarations.d.ts:

// declarations.d.ts

declare module "*.raw.js" {
  const contents: string
  export default contents
}

Upvotes: 2

deathangel908
deathangel908

Reputation: 9709

Since expo is using webpack you can customize it. Webpack has a loaders section you might be interested in. And the loader you need is called raw-loader. So when you require some file, webpack runs this file against the chain of loaders it has in config. And by default all .js files are bundled into index.js that gets executed when you run your app. Instead you need the content of the file that raw-loader exactly does. It will insert something like

module.contentScript = function() { return "console.log('Hello world!')"}

instead of:

module.contentScript = function() { console.log('Hello World'}}

So you need:

npm install raw-loader --save-dev

and inside your code:

require('raw-loader!./content.js');

Upvotes: 3

Related Questions