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

Improve react-native-svg for the web using @svgr/webpack #6660

Closed
kopax opened this issue Jan 2, 2020 · 22 comments
Closed

Improve react-native-svg for the web using @svgr/webpack #6660

kopax opened this issue Jan 2, 2020 · 22 comments
Labels

Comments

@kopax
Copy link
Contributor

kopax commented Jan 2, 2020

Documentation: https://docs.expo.io/versions/latest/sdk/svg/

I have installed in my project expo install react-native-svg.

I have exported the svg as a component by doing:

import Logo from './logo.svg';
export default Logo;

It can render on native but fail on the web.

bundle.js:69367 Warning: </static/media/splashScreen.0f923900.svg /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.

I have followed that configuration https://github.com/react-native-community/react-native-svg#use-with-svg-files

My metro.config.js is equal:

const { getDefaultConfig } = require('metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg'],
    },
  };
})();

My app.json got added

{
+    "packagerOpts": {
+      "config": "metro.config.js",
+      "sourceExts": ["js", "jsx", "ts", "tsx", "svg"]
+    },
}
  • react-native-svg 9.13.3
  • react-native-svg-transformer 0.14.3
  • SDK: 36
@kopax kopax changed the title react-native-svg does not work well with svg import react-native-svg does not work at all for web Jan 3, 2020
@cruzach
Copy link
Contributor

cruzach commented Jan 3, 2020

The example in the docs can be run on web without an error, so this is most likely something specific to your code. If you can share a snack that reproduces this, that might help

Closing this since it doesn't look to be a bug with the Expo SDK

@kopax
Copy link
Contributor Author

kopax commented Jan 3, 2020

Hi @kopax

Example on the link does work but they are not even a cents of the desired configured effect.

It can work with react component on the web and so far, react-native-svg allows it. Their maintainers also reported that this is expo issue. I had no issue in the past to work with @svgr/webpack and requiring react component, there's no reason that we impose that limitation while the background job as already be done.

This must be configured and should be treated as a bug. I am still investigating on it. It seems that the @expo/webpack-config is not using @svgr/webpack. I am trying to tweak it but can't get it to work, this is a reproduction:

git clone https://github.com/kopax/expo-bug-reports.git
cd expo-bug-reports
git checkout issue-6660
npm i
npm test
npm run web

A sample project correctly configured with svgr that work for native and web (it use @svgr/webpack in their webpack conf, compared to expo: https://github.com/kristerkari/react-native-svg-example)

So far we have to rewrite the whole svg in another format. Do you mind reopening it? I will forward that question on the forum, but please consider taking it as a bug.

I shouldn't have to write two components such as:

Logo.js

import React from 'react';
import Logo from './logo.svg';

export default (props) => <Logo accessibilityLabel="Logo" width="76" height="39" {...props} />;

Logo.web.js:

import React from 'react';
import logo from './logo.svg';

export default (props) => <img alt="logo" width="76" height="39" src={logo} {...props} />;

They are much different (on the web we won't have svg in the html, just an img pointing at the svg file so there's the whole scripted animation that cannot be done at this point). It work in native and does not on the web while it can.

@kopax
Copy link
Contributor Author

kopax commented Jan 3, 2020

Is it possible for the user of Expo to extend Expo's Webpack config?

Yes, it is possible as described here

That way you could add the @svgr/webpack dependency and configure it.

I have already tried and it does not work at all. (see comment here)

Basically, this is how my webpack.config.js looks like: https://github.com/kopax/expo-bug-reports/blob/issue-6660/webpack.config.js

The whole reproduction (which is an expo blank project, nothing more) can be run:

git clone https://github.com/kopax/expo-bug-reports.git
cd expo-bug-reports
git checkout issue-6660
npm i 
npm test
npm run web

image

@cruzach it would help the investigation and improve expo if an expo maintainer, which is responsible for the logic used by expo webpack config to provide the final configuration to webpack, would give a hint on why the webpack config is behaving differently in expo context.

@cruzach
Copy link
Contributor

cruzach commented Jan 6, 2020

@EvanBacon is probably most familiar with Expo when it comes to webpack & web, so he may have more insight, but when it comes to github issues it really helps if you are as clear and concise as possible. A title saying react-native-svg does not work at all on web is misleading, since it does work (see the example in our docs). You've also opened an issue on the same topic on multiple repos, so it's not clear exactly where you've determined the problem must lie

@kopax kopax changed the title react-native-svg does not work at all for web Improve react-native-svg for the web using @svgr/webpack Jan 6, 2020
@kopax
Copy link
Contributor Author

kopax commented Jan 6, 2020

You've also opened an issue on the same topic on multiple repos, so it's not clear exactly where you've determined the problem must lie

Yes, I was trying to solve the issue for expo users and get proper advice from people that actually work on those libs. Unfortunately, they all redirected me to expo maintainers. They were quite surprised too that it wasn't possible to configure it using the webpack.config.js and normal installation process.

I am willing to help, but I need some insight. What is webpack doing with .svg. when I use the expo configuration? I do not see any svg loaders preconfigured when I log the configuration of @expo/webpack-config even if it seems to use the url-loader.

@michaelgira23
Copy link

michaelgira23 commented Jan 13, 2020

Hi all,

I ran into the same issue as @kopax and I think I have a fix. This seems to be a legitimate issue when using the default webpack config for Expo Web and importing SVGs from a separate file (which is why the official Expo demo works as well as the react-native-svg-example, which creates its own webpack config from scratch). The default Expo webpack config contains this rule which transforms SVGs with url-loader. That rule prevents @svgr/webpack from working, so merely appending the new SVG rule won't do the trick.

My Solution

To prevent the upstream image loader from overriding @svgr/webpack, here is my webpack.config.js below. Essentially, it searches for the oneOf rule that transforms all the assets, removes the SVG file extension from the RegEx, then prepends our own rule just for SVGs with @svgr/webpack. Please let me know if there is a better way to accomplish this!

// webpack.config.js

const createExpoWebpackConfigAsync = require('@expo/webpack-config');

module.exports = async function (env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);

  // Remove existing rules about SVG and inject our own
  // (Inspired by https://github.com/storybookjs/storybook/issues/6758#issuecomment-495598635)
  config.module.rules = config.module.rules.map(rule => {
    if (rule.oneOf) {
      let hasModified = false;

      const newRule = {
        ...rule,
        oneOf: rule.oneOf.map(oneOfRule => {
          if (oneOfRule.test && oneOfRule.test.toString().includes('svg')) {
            hasModified = true;
            const test = oneOfRule.test.toString().replace('|svg', '');
            return { ...oneOfRule, test: new RegExp(test) };
          } else {
            return oneOfRule;
          }
        })
      };

      // Add new rule to use svgr
      // Place at the beginning so that the default loader doesn't catch it
      if (hasModified) {
        newRule.oneOf.unshift({
          test: /\.svg$/,
          exclude: /node_modules/,
          use: [
            {
              loader: '@svgr/webpack',
            }
          ]
        });
      }

      return newRule;
    } else {
      return rule;
    }
  });

  return config;
};

Here is my mentro.config.js. Notice the added getTransformOptions property:

// metro.config.js

const { getDefaultConfig } = require('metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      // This was important for me
      getTransformOptions: async () => ({
        transform: {
          experimentalImportSupport: false,
          inlineRequires: false
        }
      }),
      babelTransformerPath: require.resolve('react-native-svg-transformer')
    },
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg']
    }
  };
})();

Finally, remember to add the correct properties in your app.json:

{
  "expo": {
    "packagerOpts": {
      "config": "metro.config.js",
      "sourceExts": ["js", "jsx", "ts", "tsx", "svg"]
    }
  }
}

Hope this helps!

@kopax
Copy link
Contributor Author

kopax commented Jan 14, 2020

Thanks a lot for sharing this configuration. I've just tested it, and it seems that it works like a charm.

@cruzach perhaps bring this up to expo devs team?

Another optimization would be to import by default as a URL, and through ReactComponent, the SVG.

This way, the user can decide whether he wants to have a simple URL or a React Component.

ex:

import url, { ReactComponent } from './myfile.svg';

@hyxos
Copy link

hyxos commented Feb 9, 2020

@michaelgira23 Thank you so much for this solution. It was exactly what I needed as well to get SVGs to render properly using expo for all three platforms.

@aborchew
Copy link

@michaelgira23 thank you so much.

@walidvb
Copy link

walidvb commented Jul 21, 2020

I've had issues to put this solution in place, but somehow it's finally taken it. I'm unable however to restore the viewBox attribute that is stripped, even following their various issues, but i'm not sure how this particular config should be treated differently. 🤔
One thing i note is the absence of 'file-loader'?

@michaelgira23
Copy link

Hi @walidvb! I'm sorry I haven't worked with SVGs on Expo recently, so I don't know if anything has changed. For modifying any rule involving SVGs, you can modify this portion of the above solution:

newRule.oneOf.unshift({
  test: /\.svg$/,
  exclude: /node_modules/,
  use: [
    {
      loader: '@svgr/webpack',
    }
  ]
});

The rest of the logic in my solution just gets the default Expo config that would otherwise normally be used, then injects this custom rule in place of the default SVG rule. The content passed into .unshift() method should work like any normal Webpack config rule (in addition to whatever other rules Expo has in its default config.)

Looking at the issues you linked, you could try something like:

newRule.oneOf.unshift({
  test: /\.svg$/,
  exclude: /node_modules/,
  use: [{
    loader: '@svgr/webpack',
      options: {
        svgoConfig: {
          plugins: {
            removeViewBox: false
          }
        }
      }
    }, 'file-loader']
});

@walidvb
Copy link

walidvb commented Aug 3, 2020

Thanks @michaelgira23

I'm not sure exactly what changes I had to make, as it seemed to suddenly start working 🤔

For those needed this, here is my complete config:

// webpack.config.js
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");

module.exports = async function(env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);
  if (config.mode === 'development') {
    config.devServer.compress = false;
    config.plugins.push(
      new ReactRefreshWebpackPlugin()
    );
  }

  if (config.mode === 'production') {
    config.optimization.minimize = false;
  }

  config.module.rules.forEach(rule => {
    if(rule.oneOf){
      rule.oneOf.unshift({
        test: /\.svg$/,
        exclude: /node_modules/,
        use: [{
          loader: require.resolve('@svgr/webpack'),
          options: {
            viewBox: false,
          }
        }]
      });
    }
  })
  config.resolve.extensions.push('.svg')
  // Finally troutereturn the new config for the CLI to use.
  return config;
};

and relevant modules:

"expo": "^38.0.8",
"@expo/webpack-config": "^0.12.17",
"@svgr/webpack": "^5.4.0",
"babel-plugin-module-resolver": "^4.0.0",

@gate5th
Copy link

gate5th commented Jan 7, 2022

@michaelgira23 just want to say thank you for diving deep to figure this out - the rule.oneOf replacement solved my problem perfectly!

@ElForastero
Copy link

If anyone is looking for a config to simply copy-paste, you are welcome :)

const createExpoWebpackConfigAsync = require('@expo/webpack-config');

/**
 * @see https://github.com/expo/expo/issues/6660#issuecomment-667991626
 * @see https://github.com/kristerkari/react-native-svg-transformer/issues/135#issuecomment-1008310514
 */
const withSVGR = config => {
  config.module.rules =  config.module.rules.map(rule => {
    if (rule.oneOf) {
      rule.oneOf.unshift({
        test: /.svg$/,
        exclude: /node_modules/,
        use: [
          {
            loader: require.resolve('@svgr/webpack'),
            options: {
              expandProps: 'end',
              native: true,
            },
          },
        ],
      });
    }

    return rule;
  });

  return config;
};

module.exports = async function (env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);

  // Customize the config before returning it.
  return withSVGR(config);
};

@Estevete
Copy link

Estevete commented Mar 3, 2022

@ElForastero I'm having a compile error with your solution..
TypeError: this.getOptions is not a function at Object.svgrLoader (...\node_modules\@svgr\webpack\src\index.ts:71:24)

Versions I'm using:

  • "@svgr/webpack": "^6.2.1"
  • "expo": "43.0.3",
  • "react-native": "0.64.3"

Do you know what's going on?

@ElForastero
Copy link

@Estevete Try to check your Webpack version.

Latest SVGR works only with Webpack V5.

Maybe that's the case 🤔

@Estevete
Copy link

Estevete commented Mar 3, 2022

@ElForastero I didn't have weback installed on my project. I was using the one from Expo. I've tried to npm install it with version 5.70.0 and when building web I get an error of metro-hermes.

RuntimeError: abort(TypeError: Cannot read property 'createSnapshot' of undefined). Build with -s ASSERTIONS=1 for more info.
    at process.abort (...\node_modules\metro-hermes-compiler\src\emhermesc.js:796:13)
    at process.emit (events.js:375:28)
    at process.emit (...\npm\node_modules\expo-cli\node_modules\source-map-support\source-map-support.js:439:21)
    at processPromiseRejections (internal/process/promises.js:245:33)
    at processTicksAndRejections (internal/process/task_queues.js:96:32)

Do you know anything about it?

@joranb
Copy link

joranb commented Mar 23, 2022

Same issue here as Estevete, does not seem to work with latest Expo, webpack, react-native-svg, react-native-svg-transformer and svgr/webpack. Got

TypeError: this.getOptions is not a function
   at Object.svgrLoader

Can't get svg to work on web with this config. Without it I get the PascalCase error kopax got.

I did manage to get it working on web by adding the babel-plugin-inline-react-svg. But then I got: Invariant Violation: View config getter callback for component 'path' bla bla blah, must begin with upper case and so on on Android. Though not for all files, I had a brief moment of happiness with that config where almost every SVG worked, but after clearing cache (expo r -c) nothing worked on Android, back to Invariant Violation error again, so I guess it never really did work properly and that caching tricked me, So right now I am stuck between a solution that works for web, or one that works for iOS and Android.

A workaround has been to use Platform.OS === "web" and use <img src={<MySvg/>}, but that is ugly and is creating a lot of unneccessary code. Would be so smooth if import MySvg from "path.svg" and using it as <MySvg /> would have worked on both web and mobile apps. But I have only found solutions that works for one of them.

Edit; By searching Github I found a solution. https://github.com/HolovisSoftwareDev/react-native-svg-transformer-fix-expo
Unfortunately that package is not published, so I just installed it by doing:

yarn add git+https://github.com/HolovisSoftwareDev/react-native-svg-transformer-fix-expo.git

Can confirm it works that way with the following config:

webpack.config.js:

const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const path = require('path');

module.exports = async function (env, argv) {
    const config = await createExpoWebpackConfigAsync(
        {
            ...env,
            babel: {
// removed some unrelevant segments
            },
        },
        argv
    );
    config.module.rules.push({
        test: /\.(js|ts|jsx|tsx)$/,
        exclude: path.resolve(__dirname, "node_modules"),
        use: ['react-native-svg-transformer-fix-expo/loader'],
    });
    return config;
};

metro.config.js:

const {getDefaultConfig} = require("expo/metro-config");

module.exports = (async () => {
    const config = await getDefaultConfig(__dirname);

    const {transformer, resolver} = config;

    config.transformer = {
        ...transformer,
        babelTransformerPath: require.resolve("react-native-svg-transformer"),
        minifierConfig: {
            compress: {
                drop_console: true,
            },
        },
    };
    config.resolver = {
        ...resolver,
        assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
        sourceExts: [...resolver.sourceExts, "svg"],
    };
    return config;
})();

Now I can use SVGs in web, iOS and Android with the following code:

import React, { FunctionComponent } from "react";
import { View } from "react-native";
import MySvg from "@assets/svg/file.svg";
export const HeaderTabBackground: FunctionComponent<Props> = () => {
  return (
    <View>
         <MySvg />
    </View>
  );
};

This is on an Expo project with bare workflow with these dependencies:

react-native-svg
react-native-svg-transformer
react-native-svg-transformer-fix-expo

So the solution to get it working on Expo is there right in that repo. Hopefully someone can fix it, but not sure if this should be fixed in Expo or in react-native-svg-transformer? When that is done one could get rid of the fix dependency.

@marziolek
Copy link

To anyone still having that issue. Just downgrade your @svgr/webpack to v5 gregberge/svgr#665 (comment)

@software-2
Copy link

Dear future readers, as of today using Expo 48.0.6, react-native-svg 13.4.0, and @svgr/webpack 6.5.1, ElForastero's code works perfectly, no need to downgrade webpack anymore!

@dben89x
Copy link

dben89x commented Aug 6, 2023

@michaelgira23 @ElForastero thank you so much for this. I've been banging my head against the wall for days.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests