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

Resizing images usage #63

Closed
fandy opened this issue Feb 26, 2019 · 6 comments
Closed

Resizing images usage #63

fandy opened this issue Feb 26, 2019 · 6 comments

Comments

@fandy
Copy link
Contributor

fandy commented Feb 26, 2019

Expectation

I should be able to resize images in development after installing responsive-loader and sharp, with no additional configuration in next.config.js.

Result

Images are not optimized in dev when I do:

<img src={`${src}?resize&size=300`} />

or using require gives me Cannot find module error:

<img src={require(`${src}?resize&size=300`)} />
@fandy
Copy link
Contributor Author

fandy commented Feb 26, 2019

@cyrilwanner could you shed some light on this? 🙏

@cyrilwanner
Copy link
Owner

cyrilwanner commented Feb 26, 2019

Hi @fandy
Your first implementation (<img src={`${src}?resize&size=300`} />) bypasses this plugin as it will only pass down the source string to the browser where this plugin doesn't do anything. So we need to always call it with require (or import) so webpack can resolve it and call this plugin to resize it.

Now, it would help if you can tell me what your src variable contains.
If it is an URL from the internet (not an image within your project), it can't work as webpack can only access local files. If it is a file from within your project, I think webpack will only look in your current directory for this file. You can also read more in #16 about how webpack resolves dynamic requires and what options there are.

So, to help you more, it would be nice to have a bit more information: What does the src variable contain, where are your images and where are you requiring them. Thank you :)

@fandy
Copy link
Contributor Author

fandy commented Feb 26, 2019

Wow, thanks for the quick reply and explanation @cyrilwanner!

I'm mapping over my data which looks like this:

const listings = [
  {
    breed: "Chocolate Point Siamese",
    gender: "Male",
    src: "/static/images/listings/1/preview.jpg",
    id: "2384799"
  },
  {
    breed: "Seal Point Himalayan",
    gender: "Male",
    src: "/static/images/listings/2/preview.jpg",
    id: "573927"
  },
  {
    breed: "Chocolate Point Siamese",
    gender: "Female",
    src: "/static/images/listings/3/preview.jpg",
    id: "1894379"
  },
  {
    breed: "Orange Tabby",
    gender: "Male",
    src: "/static/images/listings/4/preview.jpg",
    id: "9184422"
  }
];

I guess this is a known bug that next-optimized-images doesn't work in any of the places that consume src:

    const listingsMarkup = listings.map(({ src, breed, gender, id }) => (
      <Preview
        src={require(src)}
        key={src}
      />
    ));

Now I get an error Unhandled Rejection (Error): Cannot find module '/static/images/listings/1/preview.jpg', which is confusing because it should be calling require with the same string.

So I've tried just doing require("/static/images/listings/1/preview.jpg?resize&size=300") in my data, which throws Module not found: Can't resolve '/static/images/listings/1/preview.jpg?resize&size=300'. Any idea why it can't find the module after I add the query params? I have all the optimize modules installed as well as sharp. I have a simple next.config.js:

const path = require("path");
const withTypescript = require("@zeit/next-typescript");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const withPlugins = require("next-compose-plugins");
const optimizedImages = require("next-optimized-images");
const withFonts = require("next-fonts");

const env = process.env.NODE_ENV || "development";

module.exports = withPlugins([
  [withFonts, {}],
  [
    withTypescript,
    {
      webpack(config, options) {
        // only add plugin in development to client webpack config .
        if (env === "development" && !options.isServer) {
          config.plugins.push(new ForkTsCheckerWebpackPlugin());
        }

        return config;
      }
    }
  ],
  [
    optimizedImages,
    {
      optimizeImagesInDev: true
    }
  ]
]);

@cyrilwanner
Copy link
Owner

I think there are actually two problems.
The first one is, that you use absolute paths (/static/...) instead of relative ones (../../static/...). When importing an image, the same rules apply as when importing a js file. You also can't import a regular js file with an absolute path (/components/MyComp.js), this is simply a restriction by webpack and also applies to images. Someone else had the same problem just recently: #62

Now, there are two solutions to solve this: you could just change the paths to relative ones (but you may end up with a lot ../../ depending where you are) or you add a path alias to webpack (as mentioned in the referenced issue above). So, adding something like this to your next.config.js allows you to use almost absolute paths, you just have to remove the first slash (static/... instead of /static/...):

webpack(config, options) {
  config.resolve.alias['static'] = path.join(__dirname, 'static')
  return config
}

This should solve the first problem. Then, the second one is, as you also wrote in your last comment, the same as in issue #16. Although, this is also a webpack restriction, next-optimized-images can't avoid this. And when you also want to use a query string (?resize) together with a dynamic path, it gets even more complicated and you have to use a require context (#16 (comment)).

So, I think something like this could work:

const listings = [
  {
    src: "./1/preview.jpg",
  },
  {
    src: "./2/preview.jpg",
  },
  {
    src: "./3/preview.jpg",
  },
  {
    src: "./4/preview.jpg",
  }
];

//                                              v this requires you to define the path alias in webpack, otherwise, you can also use a relative path here
const requireResizedImage = require.context('static/images/listings/?resize&size=300', false, /\.jpg$/);


const listingsMarkup = listings.map(({ src, breed, gender, id }) => (
  <Preview
    src={requireResizedImage(src)}
    key={src}
  />
));

There is just one downside: Because webpack doesn't know which images you need at runtime, it resizes all images in the directory you pass in to the require.context function (means in static/images/listings/). If you have many images in there, the build could take much longer. If this is the case, I recommend creating a new directory which only contains the images you actually need resized (e.g. static/images/listings-thumbnails/).

I hope that this helps solving your issue :)

@fandy
Copy link
Contributor Author

fandy commented Mar 3, 2019

Thanks @cyrilwanner! I'm trying to add the absolute imports webpack config using next-compose-plugins (which I know you also happen to author). However it doesn't seem to work with TypeScript. I get this generic error:

 ERROR  Failed to compile with 2 errors                                                                       4:04:47 PM

 error  in ./pages/_error.tsx

Module parse failed: The keyword 'interface' is reserved (3:0)
You may need an appropriate loader to handle this file type.
| import React from "react";
| 
> interface Props {
|   res: string;
|   err: string;

 @ multi ./pages/_error.tsx

 error  in ./pages/_app.tsx

Module parse failed: Unexpected token (31:6)
You may need an appropriate loader to handle this file type.
| 
|     return (
>       <Container>
|         <Helmet
|           defaultTitle="Foo"

 @ multi ./pages/_app.tsx

> Ready on http:https://localhost:3000

Here's my config:

module.exports = withPlugins([
  [withFonts, {}],
  [
    withTypescript,
    {
      webpack(config, options) {
        // only add plugin in development to client webpack config .
        if (env === "development" && !options.isServer) {
          config.plugins.push(new ForkTsCheckerWebpackPlugin());
        }

        return config;
      }
    }
  ],
  [
    optimizedImages,
    {
      optimizeImagesInDev: true,
      webpack(config, options) {
        config.resolve.alias["static"] = path.join(__dirname, "static");
        return config;
      }
    }
  ]
]);

Removing the webpack config in the optimizedImages block makes it compile correctly. Doesn't seem like a syntax error since it works for my TypeScript config...

@cyrilwanner
Copy link
Owner

Okay, I think this error now comes from defining the two webpack configs as plugin options. It may work with only a single one, but when using more than one, it actually overwrites the other one (and all other webpack config changes the plugins do internally, so the typescript plugin doesn't to anything anymore).
It has to do how next parses the config file: It doesn't merge duplicate properties, it just overwrites them. I'm not sure if I can hook into that from within this plugin, I will try it but the current version can't handle it.

To solve it, you can move the webpack configs out of the plugin configs into the global config object (2. param):

module.exports = withPlugins([
  [withFonts, {}],
  [withTypescript],
  [
    optimizedImages,
    {
      optimizeImagesInDev: true,
    }
  ]
], {
  webpack(config, options) {
    // only add plugin in development to client webpack config .
    if (env === "development" && !options.isServer) {
      config.plugins.push(new ForkTsCheckerWebpackPlugin());
    }

    // add a resolve alias for the static folder
    config.resolve.alias["static"] = path.join(__dirname, "static");

    return config;
  }
});

I'll try to change the plugin to also work with your definition of the plugins in the future, I like that way of overwriting the webpack config so they are next to the relevant plugin :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants