parcel-bundler / parcel

The zero configuration build tool for the web. 📦🚀
https://parceljs.org
MIT License
43.48k stars 2.27k forks source link

Parcel works with typescript/react-router-dom in Dev but doesn't follow router paths in Prod #3117

Closed tagyoureit closed 5 years ago

tagyoureit commented 5 years ago

🐛 bug report

Hopefully this is just a setup issue, but I don't know where to start. I looked at a lot of other issues but didn't see anything relevant.

In short, with Dev environment:

With env=Production,

🎛 Configuration (.babelrc, package.json, cli command)

My current repo can be see here: https://github.com/tagyoureit/nodejs-poolController/tree/6.0-DEV

// .babelrc
{
    "plugins" : ["@babel/plugin-proposal-class-properties"]
}
// tsconfig.json
{
    "compilerOptions": {
        "baseUrl": ".",
       // "paths": { "*": [ "@types/*" ] },
        "target":"es6",
        "module":"commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "outDir": "dist",
        "sourceMap": true,
        "moduleResolution": "node",
        "allowJs": true,
        "allowSyntheticDefaultImports": false,
        "esModuleInterop": true,
        "jsx": "react"
    },
    "include": [
        "src/**/*", 
         "@types" 
    ],
    "exclude": [
        "dist/**/*"
    ]
}
// package.json
{
    "name": "nodejs-poolcontroller",
    "version": "6.0.0",
    "description": "NodeJS program to read and write to the the pool equipment serial bus.",
    "main": "src/index.js",
    "scripts": {
        "start": "npm run build && NODE_ENV=production node dist/index.js",
        "dev": "TS_NODE_FILES=true node -r ts-node/register src/index.ts",
        "build": "tsc && parcel build www/pages/index.html -d dist/www",
        "start:capture": "node dist/index.js $* --capturePackets",
        "start:replay": "npm run build && NODE_ENV=production node dist/index.js $* --suppressWrite",
        "test": "snyk test && npm run build && NODE_ENV=production  TS_NODE_FILES=true mocha --recursive './specs/**/*.spec.ts' -r ts-node/register  --trace-warnings",
        "test:one": "npm run build && NODE_ENV=production  TS_NODE_FILES=true mocha  -r ts-node/register",
        "test:one:cover": "npm run build && NODE_ENV=production nyc mocha -- ./specs/assets/config/config.json ./specs/index.spec.js --include-all-sources  ./specs  --require ./specs/helpers/chai.js --exit",
        "test:cover": "npm run build && NODE_ENV=production  TS_NODE_FILES=true nyc mocha --recursive './specs/**/*.spec.ts' -r ts-node/register  --trace-warnings",
        "test:replay": "mocha  ./specs/assets/config/config.json --require ./specs/helpers/chai.js ./specs/index.spec.js ./misc/replay.js --exit --delay",
        "start:cover": "npm run build && NODE_ENV=production nyc --handle-sigint src/index.js",
        "open:cover": "open coverage/lcov-report/index.html",
        "coveralls": "nyc report --reporter=text-lcov | coveralls",
        "snyk-protect": "snyk protect",
        "prepublishOnly": "npm run snyk-protect",
        "configTester": "node misc/configTester.js"
    },
    "repository": {
        "url": "git+https://github.com/tagyoureit/nodejs-poolController.git"
    },
    "keywords": [
        "Pentair",
        "Hayward",
        "pool",
        "home automation",
        "solar",
        "heat",
        "temperature",
        "pump",
        "chlorinator",
        "IoT"
    ],
    "author": "Russell Goldin",
    "license": "AGPL-3.0",
    "bugs": {
        "url": "https://github.com/tagyoureit/nodejs-poolController/issues"
    },
    "homepage": "https://github.com/tagyoureit/nodejs-poolController#readme",
    "dependencies": {
        "bluebird": "^3.5.4",
        "bootstrap": "^3.4.1",
        "dateformat": "^3.0.3",
        "deep-diff": "^1.0.1",
        "dequeue": "^1.0.5",
        "express": "^4.16.4",
        "getmac": "^1.4.3",
        "helmet": "^3.18.0",
        "http-auth": "^3.2.3",
        "http-shutdown": "^1.2.0",
        "influx": "^5.0.7",
        "ip": "^1.1.5",
        "jquery": "^3.4.1",
        "jquery-ui-dist": "^1.12.1",
        "logform": "^2.1.2",
        "multicast-dns": "^7.0.0",
        "mysql": "^2.17.1",
        "node-ip": "^0.1.2",
        "node-ssdp": "^4.0.0",
        "parcel-bundler": "^1.12.3",
        "popper.js": "^1.15.0",
        "react": "^16.8.6",
        "react-data-grid": "^6.1.0",
        "react-dom": "^16.8.6",
        "react-infinite-calendar": "^2.3.1",
        "react-popper": "^1.3.3",
        "react-rangeslider": "^2.2.0",
        "react-router-dom": "^5.0.0",
        "react-timekeeper": "^1.1.0",
        "reactstrap": "^8.0.0",
        "request": "^2.87.0",
        "request-promise": "^4.2.4",
        "serialport": "^7.1.5",
        "socket.io": "^2.2.0",
        "socket.io-client": "^2.2.0",
        "source-map-support": "^0.5.12",
        "underscore": "^1.9.1",
        "validator": "^10.11.0",
        "winston": "^3.2.1",
        "yargs-parser": "^13.1.0"
    },
    "devDependencies": {
        "@babel/core": "^7.4.4",
        "@babel/plugin-proposal-class-properties": "^7.4.4",
        "@types/bluebird": "^3.5.26",
        "@types/chai": "^4.1.7",
        "@types/dateformat": "^3.0.0",
        "@types/deep-diff": "^1.0.0",
        "@types/express": "^4.16.1",
        "@types/helmet": "0.0.43",
        "@types/ip": "^1.1.0",
        "@types/mocha": "^5.2.6",
        "@types/nock": "^10.0.3",
        "@types/node": "^11.13.10",
        "@types/node-ssdp": "^3.3.0",
        "@types/parcel-bundler": "^1.12.0",
        "@types/react": "^16.8.17",
        "@types/react-data-grid": "^4.0.2",
        "@types/react-dom": "^16.8.4",
        "@types/react-infinite-calendar": "^2.3.2",
        "@types/react-rangeslider": "^2.2.0",
        "@types/react-router-dom": "^4.3.3",
        "@types/reactstrap": "^8.0.1",
        "@types/request-promise": "^4.1.43",
        "@types/serialport": "^7.0.3",
        "@types/sinon": "^7.0.11",
        "@types/socket.io": "^2.1.2",
        "@types/socket.io-client": "^1.4.32",
        "@types/triple-beam": "^1.3.0",
        "@types/underscore": "^1.8.17",
        "@types/validator": "^10.11.0",
        "chai": "^4.2.0",
        "coveralls": "^3.0.3",
        "mocha": "^6.1.4",
        "nock": "^10.0.6",
        "nyc": "^14.1.1",
        "sinon": "^7.3.2",
        "sinon-chai": "^3.3.0",
        "snyk": "^1.164.0",
        "ts-lint": "^4.5.1",
        "ts-mockito": "^2.3.1",
        "ts-node": "^8.1.0",
        "ts-sinon": "^1.0.17",
        "typescript": "^3.4.5"
    },
    "snyk": true,
    "nyc": {
        "check-coverage": false,
        "per-file": true,
        "include": [
            "src/**/*.js",
            "specs/**/*.js"
        ],
        "exclude": [
            "node_modules/**",
            "www/**"
        ],
        "reporter": [
            "lcov",
            "text-summary",
            "html"
        ],
        "cache": false,
        "all": true,
        "sourceMap": true,
        "instrument": true
    }
}

My main app page (app.tsx) looks like this:

import * as React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import NodeJSPoolController from '../components/PoolController'
import PacketSnifferController from '../components/utilities/PacketSnifferController' 
import PacketTester from '../components/utilities/PacketTester' 
import UtilitiesLayout from '../components/utilities/UtilitiesLayout'
import Replay from '../components/utilities/Replay'
const App = () => {
    return (
            <NodeJSPoolController />
    )
}

const Utilities = () =>
{
    return (
        <UtilitiesLayout />
    )
}

const packetSniffer = () =>
{
    return (
        <PacketSnifferController />
    )
}

const packetTester = () =>
{
    return (
        <PacketTester />
    )
}
const replay = () =>
{
    return (
        <Replay />
    )
}
ReactDOM.render(
    <Router>
        <Route exact path="/" component={App}/>
        <Route path="/packetSniffer" component={packetSniffer} />
        <Route path="/utilities" component={Utilities} />
        <Route path='/packetTester' component={packetTester} />
        <Route path='/replay' component={replay} />
    </Router>,
    document.getElementById('root')
  );

Where my Express server is setup, I use Parcel with Dev environment like this:


if ( dev )
            {
                // Parcel: absolute path to entry point
                const file = path.join( process.cwd(), '/www/pages/index.html' )
                logger.debug( `Parcel serving files from: ${ file }` )
                // Parcel: set options
                const options = {};
                // Parcel: Initialize a new bundler
                servers[ type ].parcel = new Bundler( file, options )
            }
            else
            {
                servers[ type ].parcel = {}
            }
// ... and later
        if ( dev )
        {
            // Parcel: middleware
            app.use( bundlerParcel.middleware() )
        }

For the CLI, (see package.json above) dev: npm run dev production: npm start

🤔 Expected Behavior

I would expect to be able to browse to http://localhost:3000/utilities in Production just like with Dev.

😯 Current Behavior

When I browse to http://localhost:3000 everything works. However, when I try to go to http://localhost:3000/utilities the browser gives me the message Cannot GET /utilities.

💁 Possible Solution

Not sure where to look! Please help. :)

🔦 Context

Trying to allow users to use the app with all the benefits of production (less memory, faster, no need for HRM, etc).

💻 Code Sample

Current repo: https://github.com/tagyoureit/nodejs-poolController/tree/6.0-DEV

🌍 Your Environment

Software Version(s)
Parcel 1.12.3
Node 8.16.0
npm/Yarn npm 6.4.1
Operating System Mac Mojave 10.14.5
Typescript : 3.4.5
mischnic commented 5 years ago

Could you please reduce that example? E.g.: removing everything from the server except parcel itself, only include one subpage (utilities...).

tagyoureit commented 5 years ago

Hi, I created a repo with a greatly simplified setup: https://github.com/tagyoureit/parcel-router-prod.

I think this comes down to how Express is routing to the "static" pages once Parcel builds them. Currently, I have

    app.use( express.static( path.join( process.cwd(), '/dist/www' ), { maxAge: '14d' } ) );

as my express routes. Parcel does a great job of handling the dynamic routes that are created. I think my issue is how to have Express handle the same routes with production compiling.

Is there a good way to do this or do I need to somehow hardcode the routes in Express? Much thanks!

mischnic commented 5 years ago

For Single Page (React) Apps (means client side routing), the server needs to be configured to serve the index file for any requests that aren't found. So you need to do this:

import * as http from "http";
import express = require("express");
const path = require("path").posix;
import Bundler = require("parcel-bundler");

let app: express.Application = express();

if (process.env.NODE_ENV !== "production") {
    const entry = path.join(process.cwd(), "/www/index.html");
    let parcel = new Bundler(entry, {
        outDir: "./dist/dev"
    });
    app.use(parcel.middleware());
} else {
    app.use(
        express.static(path.join(process.cwd(), "/dist/www"), { maxAge: "14d" })
    );

    // Single Page App: fallback to index.html
    app.get("*", (req, res) =>
        res.sendFile(path.join(__dirname + "/www/index.html"))
    );
}

http.createServer(app).listen(3000, () => {
    console.log(`http server started on port 3000`);
});
tagyoureit commented 5 years ago

Wow! I spent a few hours looking for an answer here and was ready to start implementing isomorphic routing. This is so simple and you saved me so much time (and a lot of hair being pulled out from my head). Thanks!

Just out of curiosity, is this documented somewhere that I should have been able to find it? Seems like such a basic setup that I should have come across it in my Googling.

mischnic commented 5 years ago

🎉

Just out of curiosity, is this documented somewhere that I should have been able to find it? Seems like such a basic setup that I should have come across it in my Googling.

I'll add it to the docs somewhere: https://github.com/parcel-bundler/website/issues/453