-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TODO: proxy for dev server #53
Comments
Made it async and some other things: import type Http = require('node:http');
import http = require('node:http');
import fs = require('node:fs');
import url = require('node:url');
import util = require('node:util');
import path = require('node:path');
import browsersList = require('browserslist');
import esbuild = require('esbuild');
import normalizePort = require('./utils/normalizePort');
import getPaths = require('./utils/getPaths');
import copyPublicFolder = require('./utils/copyPublicFolder');
import buildHtml = require('./utils/buildHtml');
// DEFINE MAIN
const serve = async (
proc: NodeJS.Process,
proxy: { host: string; port: string }
) => {
// SHUTDOWN
/**
* Shut down server
*/
const shutdown = (): void => {
server.close(handleServerClosedEvent);
};
/**
* Quit properly on docker stop
*/
const handleSigterm: NodeJS.SignalsListener = (signal: NodeJS.Signals) => {
fs.writeSync(
proc.stderr.fd,
util.format('\n' + 'Got', signal, '- Gracefully shutting down...', '\n'),
null,
'utf-8'
);
shutdown();
};
/**
* Quit on ctrl-c when running docker in terminal
*/
const handleSigint: NodeJS.SignalsListener = (signal: NodeJS.Signals) => {
fs.writeSync(
proc.stderr.fd,
util.format('\n' + 'Got', signal, '- Gracefully shutting down...', '\n'),
null,
'utf-8'
);
shutdown();
};
const handleBeforeExit: NodeJS.BeforeExitListener = (code: number) => {
fs.writeSync(
proc.stdout.fd,
util.format('Process exiting with code', code, '\n'),
null,
'utf-8'
);
shutdown();
};
const handleExit: NodeJS.ExitListener = (code: number) => {
fs.writeSync(
proc.stdout.fd,
util.format('Process exited with code', code, '\n'),
null,
'utf-8'
);
shutdown();
};
proc.on('beforeExit', handleBeforeExit);
proc.on('exit', handleExit);
proc.on('SIGTERM', handleSigterm);
proc.on('SIGINT', handleSigint);
const warnings: Error[] = [];
const errors: Error[] = [];
if (proc.env['NODE_ENV'] === undefined) {
warnings.push(
new Error(
"'NODE_ENV' should be set to one of: 'developent', 'production', or 'test'; but, it was 'undefined'"
)
);
}
// CONSOLE
const console = global.console;
// PATHS
const paths = getPaths(proc);
// ENV
const isNotLocalTestEnv =
proc.env['NODE_ENV'] !== 'test' && `${paths.dotenv}.local`;
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles: string[] = [];
dotenvFiles.push(`${paths.dotenv}.${proc.env['NODE_ENV']}.local`);
dotenvFiles.push(`${paths.dotenv}.${proc.env['NODE_ENV']}`);
if (isNotLocalTestEnv) dotenvFiles.push(`${paths.dotenv}.local`);
dotenvFiles.push(paths.dotenv);
dotenvFiles.forEach((dotenvFile) => {
if (fs.existsSync(dotenvFile.toString())) {
proc.loadEnvFile(dotenvFile); // throws internally, or changes 'proc.env'
//
} else {
const error = new Error("no '.env' file found");
errors.push(error);
}
});
// SERVER
const hostname = proc.env['HOST'] || '127.0.0.1';
const port = normalizePort(proc.env['PORT'] || '3000').toString();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleServerClosedEvent = (...args: any[]) => {
if (args) {
if (typeof args === typeof Error) {
args.forEach((err) => console.error(err));
}
// proc.exit(1);
process.exitCode = 1;
}
// proc.exit(0);
process.exitCode = 0;
};
/**
* Event listener for HTTP server "error" event.
*/
const handleServerErrorEvent = (error: any) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES': {
console.error(bind + ' requires elevated privileges');
break;
}
case 'EADDRINUSE': {
console.error(bind + ' is already in use');
break;
}
default: {
throw error;
}
}
proc.exit(1);
};
const handleServerRequestEvent: Http.RequestListener<
typeof Http.IncomingMessage,
typeof Http.ServerResponse
> = (request, response) => {
const date = new Date();
console.log(date.toISOString(), request.method, request.url);
const options = {
hostname: proxy.host,
port: proxy.port,
path: request.url,
method: request.method,
headers: request.headers,
};
// Forward each incoming request to esbuild
const proxyRequest = http.request(options, (proxyResponse) => {
// If esbuild returns "not found", send a custom 404 page
if (proxyResponse.statusCode === 404) {
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end('<h1>A custom 404 page</h1>');
return;
}
// Otherwise, forward the response from esbuild to the client
response.writeHead(
proxyResponse.statusCode || 500,
proxyResponse.headers
);
proxyResponse.pipe(response, { end: true });
});
// Forward the body of the request to esbuild
request.pipe(proxyRequest, { end: true });
};
const handleServerListenEvent = (): void => {
const address = new url.URL(`http://${hostname}:${port}`);
console.log();
console.log(
util.styleText('white', 'Server running at'),
util.styleText('yellow', address.href)
);
console.log(
util.styleText('white', 'To exit:'),
util.styleText('yellow', 'Ctrl + c')
);
console.log();
};
const server: Http.Server<
typeof Http.IncomingMessage,
typeof Http.ServerResponse
> = http.createServer();
server.on('listening', handleServerListenEvent);
server.on('error', handleServerErrorEvent);
server.on('request', handleServerRequestEvent);
server.on('close', handleServerClosedEvent);
server.listen(parseInt(port, 10), hostname);
};
// RUN MAIN
if (require.main === module) {
(async (proc: NodeJS.Process) => {
const paths = getPaths(proc);
const getClientEnvironment = (proc: NodeJS.Process) => {
const NODE: RegExp = /^NODE_/i;
const envDefaults: {
NODE_ENV: 'development' | 'test' | 'production';
PUBLIC_URL: string;
WDS_SOCKET_HOST: string | undefined;
WDS_SOCKET_PATH: string | undefined;
WDS_SOCKET_PORT: string | undefined;
FAST_REFRESH: 'true' | 'false';
} = {
NODE_ENV: proc.env.NODE_ENV || 'development',
PUBLIC_URL: proc.env.PUBLIC_URL || '/', // 'publicUrl',
WDS_SOCKET_HOST: proc.env.WDS_SOCKET_HOST || undefined, // window.location.hostname,
WDS_SOCKET_PATH: proc.env.WDS_SOCKET_PATH || undefined, // '/esbuild',
WDS_SOCKET_PORT: proc.env.WDS_SOCKET_PORT || undefined, // window.location.port,
FAST_REFRESH: proc.env.FAST_REFRESH || 'false', // !== 'false',
};
const raw: NodeJS.ProcessEnv = Object.keys(proc.env)
.filter((key) => NODE.test(key))
.reduce<NodeJS.ProcessEnv>((env, key) => {
env[key] = proc.env[key];
return env;
}, envDefaults);
const stringified: {
'process.env': NodeJS.ProcessEnv;
} = {
'process.env': Object.keys(raw)
.filter((key) => NODE.test(key))
.reduce<NodeJS.ProcessEnv>((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, raw),
};
return {
raw,
stringified,
};
};
const isEnvDevelopment: boolean = proc.env['NODE_ENV'] === 'development';
const isEnvProduction: boolean = proc.env['NODE_ENV'] === 'production';
const isEnvProductionProfile =
isEnvProduction && proc.argv.includes('--profile');
const supportedTargets = [
'chrome',
'deno',
'edge',
'firefox',
'hermes',
'ie',
'ios',
'node',
'opera',
'rhino',
'safari',
];
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const useTypeScript: boolean = fs.existsSync(paths.projectTsConfig);
const wdsSocketPath = proc.env['WDS_SOCKET_PATH'] || '/esbuild';
const wdsSocketHost =
proc.env['WDS_SOCKET_HOST'] || 'window.location.hostname';
copyPublicFolder({
appBuild: paths.projectBuild,
appHtml: paths.projectHtml,
appPublic: paths.projectPublic,
});
// Start esbuild's server on a random local port
const ctx = await esbuild.context({
// ... your build options go here ...
absWorkingDir: paths.projectPath,
publicPath: paths.projectPublic,
entryPoints: [paths.projectIndexJs],
outbase: paths.projectSrc,
outdir: paths.projectBuild,
tsconfig: paths.projectTsConfig,
format: 'esm',
// platform: 'browser',
target: browsersList(
isEnvProduction
? ['>0.2%', 'not dead', 'not op_mini all']
: [
'last 1 chrome version',
'last 1 firefox version',
'last 1 safari version',
]
)
.filter((testTarget) => {
const targetToTest = testTarget.split(' ')[0];
if (targetToTest && supportedTargets.includes(targetToTest))
return true;
return false;
})
.map<string>((browser) => {
return browser.replaceAll(' ', '');
}),
loader: {
// 'file' loaders will be prepending by 'publicPath',
// i.e., 'https://www.publicurl.com/icon.png'
'.jsx': 'jsx',
'.js': 'js',
'.tsx': 'tsx',
'.ts': 'ts',
'.svg': 'file',
'.png': 'file',
'.ico': 'file',
},
entryNames: 'static/[ext]/index',
chunkNames: 'static/[ext]/[name].chunk',
assetNames: 'static/media/[name]',
splitting: isEnvDevelopment,
banner: {
js:
proc.env['FAST_REFRESH'] === 'false'
? ''
: `
const reload = () => window.location.reload();
const eventSource = new EventSource('/esbuild');
eventSource.addEventListener('change',reload,{once:true});`,
}, // `new EventSource('/esbuild').addEventListener('change', () => window.location.reload(),{once:true});`
treeShaking: isEnvProduction,
minify: isEnvProduction,
sourcemap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
color: proc.stdout.isTTY,
resolveExtensions: paths.moduleFileExtensions
.map((ext) => `.${ext}`)
.filter((ext) => useTypeScript || !ext.includes('ts')),
define: {
'process.env': JSON.stringify(
getClientEnvironment(proc).stringified['process.env']
),
},
nodePaths: (proc.env['NODE_PATH'] || '')
.split(path.delimiter)
.filter((folder) => folder && !path.isAbsolute(folder))
.map((folder) => path.resolve(paths.projectPath, folder)),
//
});
// enable watch mode
await ctx.watch();
// The return value tells us where esbuild's local server is
await ctx
.serve({
servedir: paths.projectBuild,
})
.then(async (proxyResult) => {
await serve(proc, {
port: proxyResult.port.toString(),
host: proxyResult.host,
}).then(async (serverResult) => {
await buildHtml(proc, {
appHtml: paths.projectHtml,
appBuild: paths.projectBuild,
});
return serverResult;
});
return proxyResult;
});
})(global.process);
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Got the proxy server from the esbuild documentation working, in another project.
I used the exact same helper functions - only some variable names were changed slightly, but are otherwise copy/pasted from
esbuild-scripts
.Compared to the current build script:
WDS_*
vars already exposedcopyPublicFolder
andbuildHtml
have been externalized, and are now being called in the main loop, outside the script definition, in an async contextBefore I can possibly drag/drop this into
esbuild-scripts
, it's currently missing:parseArgs()
Just point
tsx
at it with aNODE_ENV
already set.The text was updated successfully, but these errors were encountered: