17

ANSWER for now

This was tough for me to get exactly right. Very little in the way of guidance via Google. I hope this helps others.

As Dan Cornilescu pointed out, the handlers accept the first match. So we go from more specific to less specific. I've solved this by following the folder structure created by npm run build: 1. Handle sub-directories for js, css, and media. 2. Handle json and ico requests for manifest.json and fav.ico. 3. Route all other traffic to index.html.

handlers:
  - url: /static/js/(.*)
    static_files: build/static/js/\1
    upload: build/static/js/(.*)
  - url: /static/css/(.*)
    static_files: build/static/css/\1
    upload: build/static/css/(.*)
  - url: /static/media/(.*)
    static_files: build/static/media/\1
    upload: build/static/media/(.*)
  - url: /(.*\.(json|ico))$
    static_files: build/\1
    upload: build/.*\.(json|ico)$
  - url: /
    static_files: build/index.html
    upload: build/index.html
  - url: /.*
    static_files: build/index.html
    upload: build/index.html

More efficient answers welcome.

Original Question:

Setting GAE app.yaml for react-router routes produces "Unexpected token <" errors.

In the development enviroment, all routes work when called directly. localhost:5000 and localhost:5000/test produce expected results.

In GAE standard app.yaml functions for the root directory when the URL www.test-app.com is called directly. www.test-app.com/test produces a 404 error.

app.yaml #1

runtime: nodejs8
instance_class: F1
automatic_scaling:
  max_instances: 1

handlers:
  - url: /
    static_files: build/index.html
    upload: build/index.html

Configuring app.yaml to accept wildcard routes fails for all paths. www.test-app.com/ and www.test-app.com/test produce an error "Unexpected token <". It appears that it is serving .js files as index.html.

app.yaml #2

runtime: nodejs8
instance_class: F1
automatic_scaling:
  max_instances: 1

handlers:
  - url: /.*
    static_files: build/index.html
    upload: build/index.html

Steps to reproduce this issue:

Node: 10.15.0 npm: 6.4.1

gcloud init via Google Cloud SDK

npm init react-app test-app

npm install react-router-dom

Add router to index.js:

index.js

import {BrowserRouter as Router, Route} from 'react-router-dom';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';


ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById('root'));
serviceWorker.unregister();

Add routing to app.js:

app.js

import {Route} from 'react-router-dom'
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div>
        <Route exact path="/"
          render={() =>  <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <p>
                Edit <code>src/App.js</code> and save to reload.
              </p>
              <a
                className="App-link"
                href="https://reactjs.org"
                target="_blank"
                rel="noopener noreferrer"
              >
                Learn React
              </a>
            </header>
          </div>} />
        <Route exact path="/test"
          render={() =>  <div className="App">
            Hello, World!
          </div>} />
      </div>
    );
  }
}

export default App;

No changes to package.json:

package.json

{
  "name": "test-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.1.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

npm run build

gcloud app deploy

How do we convince app engine to allow a react SPA to process routes directly?

Community
  • 1
  • 1
Nathan Shepherd
  • 171
  • 1
  • 6
  • At the runtime it says it's nodejs8 does that mean the nodejs that loads its react index.html? – Chance Jan 18 '19 at 06:03
  • To the best of my knowledge that is correct. node.js is serving react via index.html. – Nathan Shepherd Jan 18 '19 at 06:10
  • Yes I just checked in doc in GAE, so to spa / react the nodejs have to load the file with the route set to point all to index.html Ex `router.get('/*', (req, res) => { res.sendFile(path.join(__dirname, '../../build', 'index.html')) }); ` . Now how to set this in your GAE settings I no longer know, in my deploys with nodejs and reactjs I always set this manually. – Chance Jan 18 '19 at 06:15
  • Thank you. And this is similar to the responses I've found around. But I don't know where this code goes. That looks to me like server-side code, like express. This is a point of confusion for me. I have a service that runs express, but my react content is not served from there. – Nathan Shepherd Jan 18 '19 at 06:24
  • Yes I understand, reading the doc from GEA seems to me that it will be this, nodejs runs an express and loads its index.html, the point is to change the configuration inside the server. – Chance Jan 18 '19 at 06:27
  • The yaml appears to be what handles this from what I can see. So at this time we're back to churning through different yaml settings to try and force this to work. – Nathan Shepherd Jan 18 '19 at 14:10
  • @NathanShepherd I love you I love you I love you. There are two things to note about this question: 1) It contains an answer! An amazing answer! (The handlers in the very first code block, for those who skimmed it like me.) 2) People should refine it carefully based on what kind of files end up in their build directory (for example, I had an images folder and some different file types that I added in). I was stuck on this problem for hours before I found this. – Andrew Puglionesi Jul 07 '19 at 01:35

4 Answers4

24

I took the answers from Nathan Shephard and Dan Cornilescu and condensed them into the app.yaml below. It seems to be working for my React SPA on GAE Standard. No application server (ex: serve.js, ExpressJS, etc) is necessary.

env: standard
runtime: nodejs10
service: default

handlers:
  - url: /static
    static_dir: build/static

  - url: /(.*\.(json|ico|js))$
    static_files: build/\1
    upload: build/.*\.(json|ico|js)$

  - url: .*
    static_files: build/index.html
    upload: build/index.html
John Michelau
  • 743
  • 4
  • 10
2

When a request comes in its URL path is compared against the patterns specified in the handlers section of the app.yaml file, in the order in which they are specified. The first match wins and whatever the matching handler specifies is executed. Once you grasp this there's no more need to guess what's going on.

So you're making requests to www.test-app.com and www.test-app.com/test, which translate to the / and /test URL paths, respectively.

With your 1st app.yaml (note that your patterns are identical in it, the 2nd one will never be matched):

  • / matches the 1st pattern, the static build/index.html from your app directory (not a .js file!) will be returned
  • /test doesn't match any pattern, 404 will be returned

With your 2nd app.yaml:

  • / matches the 2nd pattern, the static build dir will be served (which I presume triggers your node app, but I'm not certain - I'm not a node user)
  • /test matches the 1st pattern, again that static build/index.html file will be returned

I suspect the build/index.html file from your app directory from where you made your deployment (that entire build dir was uploaded as a static dir) had HTML syntax errors at the time when you made the deployment (that's how it was frozen - it's static), causing the error message you see.

You might have fixed the local copy of the file since the deployment, which could explain why it appears to be working locally now. If so a re-deployment of the app should fix the problems on GAE as well.

UPDATE:

I don't think you'd want a static handler for your build/index.html - it'll always serve whatever content was in that file in your local workspace at deployment time.

I'd follow the non-static example from the official app.yaml Configuration File (updated with a .* pattern instead of the /.* original one to be certain it matches / as well):

handlers:
- url: .*
  script: auto

It might even be possible to drop the handlers section altogether, I don't see one in several Node.js samples repo examples:

Dan Cornilescu
  • 37,297
  • 11
  • 54
  • 89
  • I have updated the app.yaml files in the main post based on what I gleaned from your answer. All paths, I believe, are intended to go to the same place. Since it is a single page application with routing, all paths go to the same build/index.html. When the pattern is / it works just fine. When the pattern is /.* it should match everything and produce the same result? But it isn't so there lies my confusion. – Nathan Shepherd Jan 18 '19 at 16:48
  • I'm not sure `/.*` matches `/`. I'd just use `.*` instead, it does match `/`. – Dan Cornilescu Jan 18 '19 at 17:43
1

I spent some time on this and this works perfectly for my React CRA on GAE Standard.

runtime: nodejs10
service: bbi-staging
env: standard
instance_class: F2
handlers:
  - url: /(.*\.(gif|media|json|ico|eot|ttf|woff|woff2|png|jpg|css|js))$
    static_files: build/\1
    upload: build/(.*)
  - url: /(.*)
    static_files: build/index.html
    upload: build/index.html
David Buck
  • 3,439
  • 29
  • 24
  • 31
0

After spending 1 splendid day enjoying with Google App Engine, webpack and app.yaml, here is the configuration that worked for me:

webpack.js (only the json parts that are relevant, i.e. module's rules):

  {
    test: /\.(png|jpg|gif)$/,
    use: [
    {
      loader: 'url-loader',
      options: {
        name: '[name].[ext]',
        outputPath: 'media/',
        limit: 8192
      }
    }
    ]
  },
  {
    test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
    use: [
    {
      loader: 'file-loader',
      options: {
        name: '[name].[ext]',
        outputPath: 'fonts/'
      }
    }
    ]
  },

app.yaml

runtime: nodejs10 
handlers:
  - url: /(.*\.(png|ico|jpg|gif))$
    static_files: dist/\1
    upload: dist/.*\.(png|ico|jpg|gif)$
  - url: /fonts/(.*\.(woff|woff2))$
    static_files: dist/fonts/\1
    upload: dist/fonts/.*\.(woff|woff2)$
  - url: /html.js
    static_files: dist/html.js
    upload: dist/html.js
  - url: /javascript.js
    static_files: dist/javascript.js
    upload: dist/javascript.js
  - url: /.*
    static_files: dist/index.html
    upload: dist/index.html
    secure: always
    redirect_http_response_code: 301
  - url: /
    static_dir: dist

Note: my output directory is dist, so make sure you change it to build if that is the case for you

Sahar
  • 319
  • 3
  • 12