Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link
Owner Author

@mrcoles 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