shellscape / webpack-plugin-serve

A Development Server in a Webpack Plugin
http://shellscape.org
Mozilla Public License 2.0
337 stars 53 forks source link

Serve same index.html for all requests beneath a declared route #100

Closed mindpivot closed 5 years ago

mindpivot commented 5 years ago

Documentation Is:

Please Explain in Detail...

First off, this is probably me lacking understanding in how to tie Koa middleware in properly, please close if this request is inappropriate in this forum.

I am working on a feature branch that is a React application. All URL paths under /account are controlled by Reach Router. Currently, I have to explicitly create .html files for every path in the application underneath the /account path for them to be served properly by the webpack-plugin-serve server.

I would like to have a physical file /account/index.html that is served for any request at /account/* without altering the requesting URL. Example: if during development I hit the development server at /account, or /account/welcome, or /account/overview or /account/overview/detail/1234 each request would serve /account/index.html while the client maintains the requested path.

Normally, when developing a Koa server I would just set up a router.get('/account/*') route but am unclear how to successfully accomplish this within the confines of WPS. (this is an example of the difficulties in co-locating server needs with client needs I mentioned in another issue)

Your Proposal for Changes

Please detail how to accomplish, for lack of a better term, virtual path mapping in WPS if it is possible?

matheus1lva commented 5 years ago

You can do the same things that you were used to do with koa inside the boundaries of the middleware option. There you have access to the app instance and you can add route options.

const router = require('koa-route');

module.exports = {
    middleware: (app) => {
        app.use(router.get("/accounts", await (ctx) => {
            // ...
        }))
    }
}

If i did not misunderstood your problem, this can help you through it!

ps: koa-route is installed with wps, so you might not need to reinstall it.

shellscape commented 5 years ago

Can this also be done with historyFallback @PlayMa256?

matheus1lva commented 5 years ago

Can this also be done with historyFallback @PlayMa256?

Theoretically yes, i never tested: https://github.com/bripkens/connect-history-api-fallback#rewrites

mindpivot commented 5 years ago

historyFallback was the first thing I tried as per the docs. As soon as I added app.use(builtins.historyFallback({...opts})); to my config it blew up saying "Middleware must be a function".

I've punted and moved on to using koa-route to manually get the account.html file I want to serve and assigning that to ctx.body for every route under /account. I'm currently about done with that implementation but would love to see the historyFallback way of doing it.

matheus1lva commented 5 years ago

historyFallback was the first thing I tried as per the docs. As soon as I added app.use(builtins.historyFallback({...opts})); to my config it blew up saying "Middleware must be a function".

I've punted and moved on to using koa-route to manually get the account.html file I want to serve and assigning that to ctx.body for every route under /account. I'm currently about done with that implementation but would love to see the historyFallback way of doing it.

I'm going to see if i can make it work, i never had to do something similar. As i get it work i'm gonna ping you :)

shellscape commented 5 years ago

How about using the rewrites option: https://github.com/bripkens/connect-history-api-fallback/#rewrites with a regex that covered the paths required (or even all paths)?

mindpivot commented 5 years ago

@shellscape that's exactly what I tried:

app.use(builtins.historyFallback({ rewrites: [ { from: /\/account\/*$/, to: '/account/index.html' } ] }))

and when I run my npm task my two proxy entries register properly then when it hits the historyFallback like the console spits out this error:

TypeError: middleware must be a function!

FWIW @PlayMa256's suggestion of using the included koa-route middleware is working out great. I'm not properly declaring async on the middleware function but it's working nonetheless. (i need to re-tool my config to allow for the use of async/await)

shellscape commented 5 years ago

@mindpivot I'll take a look to see if that's an error on our end, but in the mean time you don't need to use the middleware option. You can pass connect-history-api-fallback options directly in the historyFallback option https://github.com/shellscape/webpack-plugin-serve#historyfallback. Give that a shot.

shellscape commented 5 years ago

Can you all share a stack trace for that TypeError? Not sure where that's originating.

mindpivot commented 5 years ago

@shellscape (i've replaced superfluous path info with ...)

    at Application.use (/Users/.../webpack-build/node_modules/koa/lib/application.js:106:41)
    at middleware (/Users/.../webpack-build/webpack/plugins.js:83:9)
    at WebpackPluginServe.start (/Users/.../webpack-build/node_modules/webpack-plugin-serve/lib/server.js:63:9)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at findNodeScript.then.existing (/Users/.../.nvm/versions/node/v10.13.0/lib/node_modules/npm/node_modules/libnpx/index.js:268:14)

plugins.js line 83 has this code:

app.use(builtins.historyFallback({
    rewrites: [
        { from: /\/account\/*$/, to: '/account/index.html' }
    ]
}));
shellscape commented 5 years ago

Ah, that makes sense, I see where the disconnect is.

So builtins.historyFallback is already calling app.use, so that's a bit redundant. See: https://github.com/shellscape/webpack-plugin-serve/blob/68da198be1fd9f37f3406ea6123996a2d62301be/lib/middleware.js#L101-L115 And because there's no return value, that's basically saying app.use(undefined) in your plugins.js file.

builtins middleware doesn't have to be wrapped in an app.use (with the lone exception being for proxy - that's due to the need for users to have ultimate flexibility with that middleware).

mindpivot commented 5 years ago

Huh, definitely falls under today I learned. That's good info. I'll give the historyFallback one more shot.

Really simple solution to this problem for me (w/ people breathing down my neck to get stuff done) is this:

app.use(router.get('/account/*', (ctx) => {
    ctx.body = fs.readFileSync(path.resolve(__dirname, '..', 'dist/account/index.html'), { encoding: 'utf-8' });
}));
shellscape commented 5 years ago

Do give the historyFallback option (on the plugin's main options object) a try with a rewrites rule. I think it'll be the magic bullet. But it may actually just do what you've concocted, behind the scenes 😄

mindpivot commented 5 years ago

Yeah I'm not entirely sure what's going on. I validated my regex against all possible routes (my previous ones were incorrect)

I tried this in the middleware:

builtins.historyFallback({
    rewrites: [
        { from: /\/account\/.*$/, to: '/account/index.html' }
    ]
});

and tried this directly on the historyFallback options object:

{
    rewrites: [
        { from: /\/account\/.*$/, to: '/account/index.html' } // also tried without the leading / before account in the 'from' property
    ]
}

both were resulting in 404s when trying to resolve /account/overview, /account/vehicles, and /account/settings

For now, I'm going to have to stick with the app.use(router.get(...)); function inside of the middleware options object.

shellscape commented 5 years ago

Good to know. I'll investigate some more this week and let you know what I find.

davidroeca commented 5 years ago

I've had this issue before, one thing worth checking is whether or not /account/index.html resolves. If it doesn't, that often means that the static option is the issue.

Does the following work?

{
  static: [
    'dist',
  ],
  historyFallback: {
    rewrites: [
      { from: /\/account\/.*$/, to: '/account/index.html' }
    ]
  }
}
shellscape commented 5 years ago

ping @mindpivot please see previous comment and let us know if that does the trick.

mindpivot commented 5 years ago

Apologies for taking so long to reply. I did try @davidroeca's suggestion and I receive 404's for all account routes.

shellscape commented 5 years ago

No worries. I'll try and put together a working demo for you early this week.

wKovacs64 commented 5 years ago

I think I'm running into this same scenario, or very similar. I've got Reach Router using basepath="/admin" (right out of the docs) and Webpack publicPath: '/admin/'. Previously (using webpack-serve@2.0.3), I was doing the following in the add function:

app.use(convert(history({ index: publicPath })))

And everything worked as expected. I assumed the WPS equivalent would be:

static: 'dist',
historyFallback: { index: publicPath }

But everything just 404s. The closest I've been able to get is this:

  static: 'dist',
  historyFallback: {
    rewrites: [
      {
        from: /\/admin\/.*$/,
        to: context => context.parsedUrl.pathname.replace('/admin', ''),
      },
    ],
  },

But that's not quite right. It loads correctly initially, and clicking through links to other admin routes works, but going directly to an admin link (typing it in the browser's address bar) 404s.

mindpivot's koa-route solution didn't work for me either, as the page loads some assets (JS, CSS, etc.) and they were resolving to index.html (HTML response, rather than the real files). I could probably screw with it to only apply to paths without extensions and add another route for the assets, but it feels sub-optimal.

Any thoughts on what else I can try?

Thanks.

shellscape commented 5 years ago

Welcome @wKovacs64 🍺

Could you share the value of publicPath from your config? If there's any chance you could share your repo, or a small repo that reproduces this, I could probably spot the problem.

wKovacs64 commented 5 years ago

Hey @shellscape, publicPath is /admin/. I can't share the whole repo, unfortunately, but I might be able to come up with a reproduction at some point. Here's the full webpack configs for now:

Old (working) webpack.config.js used with webpack-serve ```js const path = require('path'); const webpack = require('webpack'); const WebpackChunkHash = require('webpack-chunk-hash'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const convert = require('koa-connect'); const proxy = require('http-proxy-middleware'); const history = require('connect-history-api-fallback'); const mode = process.env.NODE_ENV || 'production'; const isProd = mode === 'production'; const publicPath = '/admin/'; const apiHost = process.env.API_IP || '127.0.0.1'; const apiPort = process.env.API_PORT || '10999'; const devServerOpts = { port: 9090, hotClient: true, devMiddleware: { publicPath }, add: app => { if (!process.env.API_URL) { app.use( convert( proxy( (pathname, req) => pathname === '/' && req.headers.accept && req.headers.accept.indexOf('html') === -1, { target: `http://${apiHost}:${apiPort}`, }, ), ), ); } app.use(convert(history({ index: publicPath }))); // this does what I want }, }; module.exports = { mode, devtool: isProd ? undefined : 'cheap-module-source-map', serve: devServerOpts, entry: { app: ['tachyons', 'src/index.js'], }, output: { publicPath, filename: isProd ? '[name].[chunkhash].js' : '[name].js', }, resolve: { extensions: ['.mjs', '.js'], modules: ['src', '.', 'node_modules'], }, optimization: { minimizer: [ new TerserPlugin({ cache: true, parallel: true, sourceMap: !isProd, }), new OptimizeCSSAssetsPlugin({}), ], splitChunks: { chunks: 'all', }, }, module: { rules: [ { test: /\.css$/, use: isProd ? [MiniCssExtractPlugin.loader, 'css-loader'] : [ 'style-loader', { loader: 'css-loader', options: { sourceMap: true, }, }, ], }, { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.(jpe?g|png|gif|svg)$/i, use: [ { loader: 'file-loader', options: { name: '[name].[hash].[ext]', }, }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, }, }, ], }, { test: /\.(ttf|eot|woff|woff2)$/, loader: 'file-loader', options: { name: '[name].[hash].[ext]', }, }, ], }, plugins: [ new webpack.LoaderOptionsPlugin({ minimize: isProd, debug: !isProd }), new webpack.EnvironmentPlugin({ API_STAGE: false, API_LOGOUT_URL: '', API_URL: '/', }), new WebpackChunkHash(), new CopyWebpackPlugin([ { from: path.join('src', 'resources', 'robots.txt'), force: true, }, ]), new HtmlWebpackPlugin({ template: path.join('src', 'resources', 'index.html'), favicon: path.join('src', 'resources', 'favicon.ico'), chunksSortMode: 'dependency', inject: 'body', }), new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer', }), isProd && new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), ].filter(Boolean), }; ```
New (broken) webpack.config.js with no webpack-serve (just webpack) ```js const path = require('path'); const webpack = require('webpack'); const WebpackChunkHash = require('webpack-chunk-hash'); const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const mode = process.env.NODE_ENV || 'production'; const isProd = mode === 'production'; const publicPath = '/admin/'; const apiHost = process.env.API_IP || '127.0.0.1'; const apiPort = process.env.API_PORT || '10999'; const devServerOpts = { port: 9090, hmr: true, static: 'dist', historyFallback: { index: publicPath }, // does not work as expected middleware: (app, builtins) => { if (!process.env.API_URL) { app.use( builtins.proxy( (pathname, req) => pathname === '/' && req.headers.accept && req.headers.accept.indexOf('html') === -1, { target: `http://${apiHost}:${apiPort}`, }, ), ); } }, }; module.exports = { mode, devtool: isProd ? undefined : 'cheap-module-source-map', entry: { app: ['tachyons', 'src/index.js', 'webpack-plugin-serve/client'], }, output: { publicPath, filename: isProd ? '[name].[chunkhash].js' : '[name].js', }, resolve: { extensions: ['.mjs', '.js'], modules: ['src', '.', 'node_modules'], }, optimization: { minimizer: [ new TerserPlugin({ cache: true, parallel: true, sourceMap: !isProd, }), new OptimizeCSSAssetsPlugin({}), ], splitChunks: { chunks: 'all', }, }, module: { rules: [ { test: /\.css$/, use: isProd ? [MiniCssExtractPlugin.loader, 'css-loader'] : [ 'style-loader', { loader: 'css-loader', options: { sourceMap: true, }, }, ], }, { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.(jpe?g|png|gif|svg)$/i, use: [ { loader: 'file-loader', options: { name: '[name].[hash].[ext]', }, }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, }, }, ], }, { test: /\.(ttf|eot|woff|woff2)$/, loader: 'file-loader', options: { name: '[name].[hash].[ext]', }, }, ], }, plugins: [ new webpack.LoaderOptionsPlugin({ minimize: isProd, debug: !isProd }), new webpack.EnvironmentPlugin({ API_STAGE: false, API_LOGOUT_URL: '', API_URL: '/', }), new WebpackChunkHash(), new CopyWebpackPlugin([ { from: path.join('src', 'resources', 'robots.txt'), force: true, }, ]), new HtmlWebpackPlugin({ template: path.join('src', 'resources', 'index.html'), favicon: path.join('src', 'resources', 'favicon.ico'), chunksSortMode: 'dependency', inject: 'body', }), new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer', }), isProd && new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), !isProd && new Serve(devServerOpts), ].filter(Boolean), watch: !isProd, }; ```
shellscape commented 5 years ago

@mindpivot @wKovacs64 apologies, but I just haven't had the time to dive into React enough to be able to put together my own breaking reproduction. We're really gonna need one of you all to put together a minimal reproduction in order to debug this. I really want to solve this for you, but just going to need some help.

wKovacs64 commented 5 years ago

@shellscape No worries. I've actually migrated the app in question over to CRA since experiencing this issue, so this is no longer a problem for me. But I've created a minimal reproduction in an effort to assist:

https://github.com/wKovacs64/wps-issue-100

shellscape commented 5 years ago

Thank you! We'll use this to try and improve the module.

matheus1lva commented 5 years ago

@shellscape the problem here is that

new WebpackPluginServe({
      port: 8000,
      static: [path.resolve('./dist')],
      historyFallback: true
    })

On wps, it is served from the root of the output folder, so basically when anything is requested it tries to request to the server from the publicPath, which koa understands as a folder inside dist, for e.g: http://localhost:8000/admin/vendors~main.js and it cannot find it.

Does it makes sense to remove the publicPath in our side on before looking for the resource that it is being asked for?

shellscape commented 5 years ago

OK that makes sense. So should there be a proxy rule to proxy /admin/* to /* ? We don't want to mess with the publicPath because that's a webpack config thing, would probably confuse users.

mindpivot commented 5 years ago

I've had this working for a while but in a pretty hacky way. my requirement is to serve the same file for anything at and beneath the route /account.

My solution is this code snippet inside my middleware option block:

app.use(route.get('/account/*', (ctx) => {
        ctx.body = fs.readFileSync(path.resolve(_dirname, '..', 'dist/account/index.html'), {
            encoding: 'utf-8'
        });
}));

works great but I understand it may not be ideal for all

shellscape commented 5 years ago

@mindpivot looks like we commented at about the same time. Have you tried the proxy config for doing that?

mindpivot commented 5 years ago

@shellscape no I have not but I have the proxy doing other heavy lifting on the dev-server as i'm basically proxying page requests for an entire CMS system, API calls, etc. As a result, i'm hesitant to muck around in the proxy config much more as I have a pretty large project depending on the setup as it stands

shellscape commented 5 years ago

OK we should probably do some intelligent detection here and alert the user when the publicPath doesn't match any static paths defined. Does that seem reasonable?