gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.26k stars 10.32k forks source link

Use canvas in gatsby #17661

Closed sayjeyhi closed 5 years ago

sayjeyhi commented 5 years ago

Recently I was working on a project , which uses node-canvas , and it is working fine in development mode. but if I want to build package and run gatsby build canvas will got some error on last steps with message :

Generating SSR bundle failed

Unexpected character '�' (1:0)

File: node_modules/canvas/build/Release/canvas.node:1

⠹ Building static HTML for pages

I used loadImage function from node canvas in an async function like :

import { loadImage } from 'canvas';

/**
 * Load image source to use in canvas
 * @param src
 * @returns {Promise<Image | void>}
 */
export default async function getImage(src) {
  return loadImage(src)
    .then(image => image)
    .catch(err => console.log(err));
}
// etc.

and use it in some componenet to load image in react-konva :

import React, { useEffect, useState } from 'react';
import { Image, Group, Rect } from 'react-konva';
import { Motion, spring } from 'react-motion';
import { getImage, getPlayerCoordinates } from './../../selectors/utility';
import avatars from './../../images/avatars/avatars';

const Player = props => {
  const {
    player: { id, pos, avatar, boxPosition },
    current: { id: currentPlayerId },
    grid: {
      box: { width },
    },
    grid,
  } = props;

  const { x, y } = getPlayerCoordinates(pos, grid, boxPosition);
  const isCurrent = id === currentPlayerId;

  let [playerSource, setPlayerSource] = useState();

  useEffect(() => {
    getImage(avatars[avatar - 1]).then(image => setPlayerSource(image));

    /* eslint-disable-next-line */
  }, []);

  const minifiedWidth = width - 10;
  return (
    <Motion style={{ x: spring(x), y: spring(y), stiffness: 211, dumping: 33 }}>
      {({ x, y }) => (
        <Group>
          <Rect
            x={x - minifiedWidth / 2}
            y={y - minifiedWidth / 2}
            width={minifiedWidth}
            height={minifiedWidth}
            cornerRadius={10}
            scale={{ x: 0.92, y: 0.92 }}
            fill={isCurrent ? 'rgba(255,255,255,0.9)' : 'transparent'}
          />
          <Image
            x={x - minifiedWidth / 2}
            y={y - minifiedWidth / 2}
            width={minifiedWidth}
            height={minifiedWidth}
            image={playerSource}
          />
        </Group>
      )}
    </Motion>
  );
};

export default Player;

There was a issue in node-canvas repo , which says add this configuration to webpack , and I did it , but it pass build level and make some errors on browser console :

exports.onCreateWebpackConfig = ({
  stage,
  rules,
  loaders,
  plugins,
  actions,
}) => {
  actions.setWebpackConfig({
  externals: {
    canvas: "commonjs canvas" // Important (2)
    }
  })
}

after build , we got this error on console :

external "canvas":1 Uncaught (in promise) ReferenceError: require is not defined
    at Object.<anonymous> (external "canvas":1)
    at u (bootstrap:84)
    at Module.<anonymous> (component---src-web-pages-index-js-edb51a559818a5f7befe.js:1)
    at u (bootstrap:84)

My project is in this repo : https://github.com/jafar-rezaei/snakeAndLadders.

File contents (if changed)

gatsby-config.js: N/A package.json: N/A gatsby-node.js: N/A gatsby-browser.js: N/A gatsby-ssr.js: N/A

jonniebigodes commented 5 years ago

Gatsby is a ssr(server side framework), meaning that all the build process is done in node. With that won't have access to certain apis, like canvas. And it generates that error. As a workaround you could probably use react- loadable, for instance, let me see if I can make a reproduction of both packages and report back, do you mind waiting a bit?

sayjeyhi commented 5 years ago

@jonniebigodes Yeah , thatswhy I used node-canvas , to bring canvas to SSR , but I could not make it to build the gatsby static files.. Sure, thanks for your feedback 💯

LekoArts commented 5 years ago

Hi!

Sorry to hear you're running into an issue. To help us best begin debugging the underlying cause, it is incredibly helpful if you're able to create a minimal reproduction. This is a simplified example of the issue that makes it clear and obvious what the issue is and how we can begin to debug it.

If you're up for it, we'd very much appreciate if you could provide a minimal reproduction and we'll be able to take another look.

Thanks for using Gatsby! 💜

sayjeyhi commented 5 years ago

@LekoArts I translate the game and add it on a sandbox here : https://codesandbox.io/s/snakeandladders-ricvr

jonniebigodes commented 5 years ago

@jafar-rezaei i've cloned your codesandbox and it seems that i have a solution for you. Going to test out a couple of things and report back.

jonniebigodes commented 5 years ago

@jafar-rezaei i think i have a solution for your issue. Below are the steps i took to try and solve it.

const windowGlobal = typeof window !== 'undefined' && window;

const devtools = process.env.NODE_ENV === 'development' && windowGlobal.devToolsExtension ? window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION() : f => f;

const store = createStore( rootReducer, compose( applyMiddleware(logger, sagaMiddleware), devtools ) ); sagaMiddleware.run(rootSaga) export default store;


This small change will check for both mode(development/production) and window and load the devtools accordingly.
- Issued `gatsby clean && gatsby build && gatsby serve`, to set a a clean slate on the build, issue a production build and emulate a production server and i'm presented with the following:
![canvas_build_1](https://user-images.githubusercontent.com/22988955/65082982-3d1f4e00-d99f-11e9-9b4c-8daa4d24f9cf.png)

- Opened up a new browser window to `http://localhost:9000` and started a new game and i'm presented with the following:
![canvas_build_2](https://user-images.githubusercontent.com/22988955/65083029-5cb67680-d99f-11e9-830f-c48a50fbab0b.png)
- Issued `gatsby clean && gatsby develop`, once again purge the folders associated to the build process(.cache and public) to generate a development build, just to check if the redux devtools as being loaded correctly.
- Opened up `http://localhost:8000` and started a new game and i'm presented with the following:
![canvas_build_3](https://user-images.githubusercontent.com/22988955/65083265-01d14f00-d9a0-11e9-8c91-f78a81de39fe.png)

The devtools are being loaded and also the package aswell.

Feel free to provide feedback so that we can close this issue or continue to work on it until we find a suitable solution.
sayjeyhi commented 5 years ago

Big thanks @jonniebigodes , the problem solved

jonniebigodes commented 5 years ago

@jafar-rezaei no need to thank. Glad that i was able to solve your issue.

LinusU commented 5 years ago

@jafar-rezaei @jonniebigodes

The canvas package is meant to work both in the browser and on Node.js, with the use of separate files for each platform. In our package.json, we've added a line to tell bundlers to use the file browser.js when building for browsers, and index.js when running on Node.js.

https://github.com/Automattic/node-canvas/blob/7baaecfe13638fb8406f5277e97bb99e26afef51/package.json#L7

I thought that WebPack would automatically pick up on this 🤔 are you using any configuration that would make that not work?

jonniebigodes commented 5 years ago

@LinusU i don't know how familiar you are with gatsby but all of the build is done on the node side of things, from my testing and based on the code @jafar-rezaei supplied the bundler/webpack configuration is the default that gatsby offers out of the box, with a small caveat and that is to ignore the canvas library during the build process, as outlined in these lines:

exports.onCreateWebpackConfig = ({
  stage,
  rules,
  loaders,
  plugins,
  actions,
}) => {
  if (stage === "build-html") {
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /canvas/,
            use: loaders.null(),
          },
        ],
      },
    })
  }
};

It could be that the property browser you mentioned isn't being picked up by gatsby's webpack out of the box. I'm not knowledgeable of the webpack workflow of gatsby, but it could be that with some work and some changes this can work with gatsby. I'll ping the @gatsbyjs/core for some insight on this, on a personal level if the two, with some work can be better integrated i think that the community would benefit a bit more.

larryg727 commented 4 years ago

Thank for this. As a side note we also had this issue during gatsby develop as well. When running gatsby develop the stage is develop-html. So if you are getting this error during development you can always add this as an or conditional in the stage check in the above fix.

jonniebigodes commented 4 years ago

@larryg727 when this was written it was intended for production build with Gatsby, with that i ommited any configurations relevant to that. But nonetheless nice touch to include it and expand the response.

JGeiser9 commented 4 years ago

Thank for this. As a side note we also had this issue during gatsby develop as well. When running gatsby develop the stage is develop-html. So if you are getting this error during development you can always add this as an or conditional in the stage check in the above fix.

@larryg727 you saved me a huge headache. Thank you

Ubanna commented 4 years ago

I have an eraser effect with canvas on my Gatsby application. On the home page users can erase the canvas to see the video underneath the canvas.

Everything works as normal on my local machine but when I serve the app, the canvas and video do not load initially. However, if I navigate to any page within the app and back to home page, the canvas and video load and work properly.

I am new to Gatsby, please how can I resolve this.

Please see below code:

const HomePage = () => {
  let canvas = useRef(null)
  const size = useWindowSize()
  const { currentTheme } = useGlobalStateContext()

  useEffect(() => {
    let renderingElement = canvas.current
    let drawingElement = renderingElement.cloneNode()

    let drawingCtx = drawingElement.getContext("2d")
    let renderingCtx = renderingElement.getContext("2d")

    let lastX
    let lastY

    let moving = false

    renderingCtx.globalCompositeOperation = "source-over"
    renderingCtx.fillStyle = currentTheme === "dark" ? "#000000" : "#ffffff"
    renderingCtx.fillRect(0, 0, size.width, size.height)

    renderingElement.addEventListener("mouseover", e => {
      moving = true
      lastX = e.pageX - renderingElement.offsetLeft
      lastY = e.pageY - renderingElement.offsetTop
    })

    renderingElement.addEventListener("mouseup", e => {
      moving = false
      lastX = e.pageX - renderingElement.offsetLeft
      lastY = e.pageY - renderingElement.offsetTop
    })

    renderingElement.addEventListener("mousemove", e => {
      if (moving) {
        drawingCtx.globalCompositeOperation = "source-over"
        renderingCtx.globalCompositeOperation = "destination-out"
        let currentX = e.pageX - renderingElement.offsetLeft
        let currentY = e.pageY - renderingElement.offsetTop
        drawingCtx.lineJoin = "round"
        drawingCtx.moveTo(lastX, lastY)
        drawingCtx.lineTo(currentX, currentY)
        drawingCtx.closePath()
        drawingCtx.lineWidth = 120
        drawingCtx.stroke()
        lastX = currentX
        lastY = currentY

        renderingCtx.drawImage(drawingElement, 0, 0)
      }
    })
  }, [currentTheme])

  return (
    <Container>
      <Video>
        <video
          height="100%"
          width="100%"
          loop
          autoPlay
          muted
          src={require("../assets/somevideo.mp4")}
        />
      </Video>
      <Canvas
        width={size.width}
        height={size.height}
        ref={canvas}
      />
    </Container>
  )
}
nerdess commented 4 years ago

i got the same error as @sayjeyhi and just like @larryg727 it happens during gatsby develop...

@jonniebigodes so your approach is to mute the canvas plugin, did i understand this correctly? with

 actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /canvas/,
            use: loaders.null(),
          },
        ],
      },
    })

your are essentially doing the same as writing

 actions.setWebpackConfig({
      externals: ['canvas']
    })

but with canvas muted, an error is thrown in the browser now.

i get a TypeError: Object(...) is not a function because this import: import { createCanvas } from 'canvas'; is importing nothing...which makes sense, since canvas is muted. but then again this is bad because i actually want to use it :D

it would be great if someone could nudge me in the right direction how to fix this paradox...

nerdess commented 4 years ago

I am sorry, should've read the whole thread more thouroughly. I ended up with this Webpack config in gatsby-node.js which works just fine (the part with the resolve/alias is not relevant to this issue, the if-condition is the relevant thing):

exports.onCreateWebpackConfig = ({
  stage,
  rules,
  loaders,
  plugins,
  actions
}) => {

  actions.setWebpackConfig({
    resolve: {
      alias: {
        '~components': path.resolve(__dirname, 'src/components'),
        '~images': path.resolve(__dirname, 'src/images'),
        '~hooks': path.resolve(__dirname, 'src/lib/hooks')
      },
    }
  });

  if (stage === "develop-html") {
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /canvas/,
            use: loaders.null(),
          },
        ],
      },
    })
  }
};
martinjuhasz commented 2 years ago

weird but why does the proposed workaround not work for me? onCreateWebpackConfig gets called, stage is build-html, still it get this error:

exports.onCreateWebpackConfig = ({
  stage,
  rules,
  loaders,
  plugins,
  actions,
}) => {
  console.log("onCreateWebpackConfig", { stage })
  // see: https://github.com/gatsbyjs/gatsby/issues/17661
  if (stage === "build-html") {
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /canvas/,
            use: loaders.null(),
          },
        ],
      },
    })
  }
}
success Writing page-data.json files to public directory - 0.187s - 381/381 2033.69/s
onCreateWebpackConfig { stage: 'build-html' }

 ERROR #98124  WEBPACK

Generating SSR bundle failed

Can't resolve 'canvas' in '/dir/node_modules/konva/lib'

anyone has an idea why this is still happening here?

gatsby: 3.14.6