Skip to content

Instantly share code, notes, and snippets.

@mrcoles
Last active December 4, 2019 12:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrcoles/ff979c09719a5e827eb2d122aa2c6332 to your computer and use it in GitHub Desktop.
Save mrcoles/ff979c09719a5e827eb2d122aa2c6332 to your computer and use it in GitHub Desktop.
A simple proxy script for controlling URL rewriting when running Parcel with multiple entry points (including https support)
const chalk = require('chalk');
const fs = require('fs');
const http = require('http');
const https = require('https');
const yargs = require('yargs');
// Configuration
const DEFAULT_PARCEL_PORT = 4321;
const DEFEAULT_PROXY_PORT = 1234;
const DEFAULT_FILE = '.localproxy.json';
const DEFAULT_EXTS = [
'html',
'css',
'js',
'map',
'png',
'jpg',
'svg',
'webp',
'mp4',
'webm',
'webmanifest',
'json',
'yml',
'pdf'
];
// re-use the parcel self-signed certs if they exist
const CERTS = {
key: '.cache/private.pem',
cert: '.cache/primary.crt'
};
// Functions
const startProxy = async (cfg, parcelPort, proxyPort, isHttps, verbose) => {
if (isHttps) {
// HACK - allow self-signed certs
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
}
if (verbose) {
const proxyUrl = chalk.magenta(
`${isHttps ? 'https' : 'http'}://localhost:${proxyPort}`
);
console.log(
chalk.bold(`\n** Starting parcel local proxy on ${proxyUrl} **\n`)
);
}
const mainPath = cfg.default || '/index.html';
const paths = cfg.paths || [];
const defaultExts = cfg.resetExts === true ? [] : DEFAULT_EXTS;
const cfgExts = cfg.exts || [];
const extsSet = new Set([...defaultExts, ...cfgExts]);
const module = isHttps ? https : http;
if (isHttps) {
const options = await loadCerts(verbose);
module.createServer(options, onRequest).listen(proxyPort);
} else {
module.createServer(onRequest).listen(proxyPort);
}
function onRequest(client_req, client_res) {
const baseUrl = client_req.url.split('?')[0];
const extSp = baseUrl
.split('/')
.pop()
.split('.');
const ext = extSp.length > 1 ? extSp[extSp.length - 1] : '';
let newPath = client_req.url;
if (paths[baseUrl]) {
newPath = paths[baseUrl];
} else if (!ext || !extsSet.has(ext)) {
newPath = mainPath;
}
if (verbose >= 2) {
const extra = newPath !== client_req.url ? ` -> ${newPath}` : '';
console.log(`serve: ${client_req.url}${extra}`);
}
if (verbose >= 3) {
console.log(`headers: ${JSON.stringify(client_req.headers, null, 2)}`);
}
const options = {
hostname: 'localhost',
port: parcelPort,
path: newPath,
method: client_req.method,
headers: client_req.headers
};
const proxy = module.request(options, function(res) {
client_res.writeHead(res.statusCode, res.headers);
res.pipe(
client_res,
{ end: true }
);
});
client_req.pipe(
proxy,
{ end: true }
);
}
};
const loadCerts = async verbose => {
const results = {};
while (true) {
let isOk = true;
for (let [key, filepath] of Object.entries(CERTS)) {
if (results[key] === undefined) {
try {
const contents = await readFile(filepath);
results[key] = contents;
} catch (err) {
if (verbose) {
console.log(`Unable to load ${filepath}: ${err.message}`);
}
await sleep(1000);
isOk = false;
break;
}
}
}
if (isOk) {
break;
}
}
return results;
};
// Helpers
const sleep = delay =>
new Promise(resolve => {
setTimeout(() => resolve(), delay);
});
const readFile = path =>
new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, contents) => {
return err ? reject(err) : resolve(contents);
});
});
const readFileSync = path => fs.readFileSync(path, 'utf8');
// Main
const main = async () => {
return yargs
.usage('Usage: $0 <command> [options]')
.command(
'$0 <parcel-port> [proxy-port]',
'Start proxy server',
yargs =>
yargs
.positional('parcel-port', {
type: 'string',
demand: true,
help: 'the port that parcel is running on'
})
.positional('proxy-port', {
type: 'string',
default: DEFEAULT_PROXY_PORT,
help: 'the port that this proxy is running on'
})
.count('verbose')
.alias('v', 'verbose')
.option('https', { type: 'boolean', default: false })
.option('file', {
alias: 'f',
help: 'specify an alternate config file path',
default: DEFAULT_FILE
}),
async argv => {
console.log('FILE?', argv.file);
const cfgContents = readFileSync(argv.file);
const cfg = JSON.parse(cfgContents);
return startProxy(
cfg,
argv.parcelPort,
argv.proxyPort,
argv.https,
argv.verbose
);
}
)
.demandCommand(1, 1)
.alias('h', 'help')
.help().argv;
};
//
if (require.main === module) {
main().catch(err => {
console.error(err);
process.exit(1);
});
}
@mrcoles
Copy link
Author

mrcoles commented Nov 5, 2019

What is this?

This script creates a proxy server that maps URLs that you request to the ones that Parcel server expects. This is intended to help you out when running parcel with multiple entry files, e.g., normally:

Going to http://localhost:1234/folder-1/ won't work, instead you will need to explicitly point to the file http://localhost:1234/folder-1/index.html.

However, with this proxy, you can—among other things—specify that "/folder-1/" should proxy to "/folder-1/index.html".

Also, since I’m using parcel with the --https flag, I added support for --https to this proxy too.

Setup

  1. Copy parcel-local-proxy.js into your project

  2. Install dependencies:

    npm i -D chalk yargs npm-run-all
  3. Update your package.json scripts:

    "scripts": {
      "start": "npm-run-all -p start:*",
      "start:parcel": "parcel --https -p 4321 src/index.html src/folder-1/index.html",
      "start:proxy": "node parcel-local-proxy.js 4321 --https -v",
    }

    Make sure the hardcoded port for parcel and proxy both match. Optionally, enable https on both. For more run node parcel-local-proxy.js --help

  4. Add a .localproxy.json file to the root directory of your project. The structure is:

    {
      // any un-matched path will serve this path from parcel (default: "/index.html")
      "default": "/index.html",
    
      // mapping of paths into parcel (optional)
      "paths": {
        "/folder-1/": "/folder-1/index.html"
      },
    
      // known filename extensions do not get their URL rewritten
    
      // set to true if you want to not use the defaults (see `DEFAULT_EXTS`)
      "resetExts": false,
    
      // additional filename extensions to request directly without rewriting (optional)
      "exts": [ "foo", "bar" ]
    }

Run

Now you can just do:

npm start

And the local dev server will operate how you expect!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment