-
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
Switch from webpack
to esbuild
#19
Changes from all commits
37dbc4e
9776074
aa114b6
b60c024
9c49f0f
e6c2536
6724342
d09f16d
6284bb3
1645f5c
f79ea92
a8605aa
905fe30
b78c91f
e4b6af2
17bd7d5
a13cb07
9ff7335
215425a
b27ba4c
9e3debe
6121837
92874ad
f638cb2
4d6625e
2808462
19caa5a
8162be9
3037b6b
1138071
d50b38e
592039e
2a1e0f8
0a8b5ea
13dd9da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,13 +60,9 @@ opam pin add reshowcase.dev git+https://github.com/ahrefs/reshowcase.git#main | |
This will make the NodeJS script `reshowcase` available in your opam switch. | ||
|
||
To make sure this script works, add the following dependencies to your application `package.json`: | ||
|
||
```json | ||
"devDependencies": { | ||
"copy-webpack-plugin": "^11.0.0", | ||
"html-webpack-plugin": "^5.5.0", | ||
"webpack": "^5.76.1", | ||
"webpack-dev-server": "^4.11.1", | ||
"@craftamap/esbuild-plugin-html": "https://github.com/denis-ok/esbuild-plugin-html#79f512f447eb98efa6b6786875f617a095eaaf09" | ||
} | ||
``` | ||
|
||
|
@@ -84,6 +80,6 @@ $ reshowcase start --entry=path/to/Demo.js | |
$ reshowcase build --entry=path/to/Demo.js --output=path/to/bundle | ||
``` | ||
|
||
If you need custom webpack options, create the `.reshowcase/config.js` and export the webpack config, plugins and modules will be merged. | ||
If you need custom esbuild options, create the `.reshowcase/config.js` and export the esbuild config. Plugins and modules will be merged. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should mention here that if users use their own template, they have to add the |
||
|
||
If you need a custom template, pass `--template=./path/to/template.html`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,11 @@ | ||
#!/usr/bin/env node | ||
|
||
const webpack = require("webpack"); | ||
const WebpackDevServer = require("webpack-dev-server"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const os = require("os"); | ||
const HtmlWebpackPlugin = require("html-webpack-plugin"); | ||
const CopyWebpackPlugin = require("copy-webpack-plugin"); | ||
const child_process = require("child_process"); | ||
const esbuild = require("esbuild"); | ||
const http = require("http"); | ||
const { htmlPlugin } = require("@craftamap/esbuild-plugin-html"); | ||
|
||
const toAbsolutePath = (filepath) => { | ||
if (path.isAbsolute(filepath)) { | ||
|
@@ -77,7 +75,7 @@ const outputPath = (() => { | |
} | ||
})(); | ||
|
||
const config = (() => { | ||
const customConfig = (() => { | ||
const configDir = path.join(process.cwd(), ".reshowcase"); | ||
|
||
if (!fs.existsSync(configDir)) { | ||
|
@@ -102,108 +100,228 @@ const config = (() => { | |
} | ||
})(); | ||
|
||
const compiler = webpack({ | ||
// https://github.com/webpack/webpack-dev-server/issues/2758#issuecomment-813135032 | ||
// target: "web" (probably) can be removed after upgrading to webpack-dev-server v4 | ||
target: "web", | ||
mode: isBuild ? "production" : "development", | ||
entry: { | ||
index: entryPath, | ||
const watchPlugin = { | ||
name: "watchPlugin", | ||
setup: (build) => { | ||
build.onEnd((_buildResult) => console.log("[esbuild] Rebuild finished!")); | ||
}, | ||
output: { | ||
path: outputPath, | ||
filename: "reshowcase[fullhash].js", | ||
globalObject: "this", | ||
chunkLoadingGlobal: "reshowcase__d", | ||
}; | ||
|
||
// entryPoint passed to htmlPlugin must be relative to the current working directory | ||
const entryPathRelativeToCwd = path.relative(process.cwd(), entryPath); | ||
|
||
const defaultConfig = { | ||
entryPoints: [entryPath], | ||
entryNames: "[name]-[hash]", | ||
assetNames: "[name]-[hash]", | ||
chunkNames: "[name]-[hash]", | ||
Comment on lines
+115
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are needed because otherwise it would use the default There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually do not no, they were here before me. Will check |
||
bundle: true, | ||
outdir: outputPath, | ||
publicPath: "/", | ||
format: "esm", | ||
minify: isBuild, | ||
metafile: true, | ||
splitting: true, | ||
treeShaking: true, | ||
logLevel: "warning", | ||
loader: Object.fromEntries( | ||
[ | ||
".css", | ||
".jpg", | ||
".jpeg", | ||
".png", | ||
".gif", | ||
".svg", | ||
".ico", | ||
".avif", | ||
".webp", | ||
".woff", | ||
".woff2", | ||
".json", | ||
".mp4", | ||
].map((ext) => [ext, "file"]) | ||
), | ||
define: { | ||
USE_FULL_IFRAME_URL: JSON.stringify(isBuild ? useFullframeUrl : true), | ||
}, | ||
module: config.module, | ||
plugins: [ | ||
...(config.plugins ? config.plugins : []), | ||
new CopyWebpackPlugin({ | ||
patterns: [{ from: path.join(__dirname, "./favicon.png"), to: "" }], | ||
}), | ||
new HtmlWebpackPlugin({ | ||
filename: "index.html", | ||
template: path.join(__dirname, "./ui-template.html"), | ||
}), | ||
new HtmlWebpackPlugin({ | ||
filename: "./demo/index.html", | ||
template: process.argv.find((item) => item.startsWith("--template=")) | ||
? path.join( | ||
process.cwd(), | ||
process.argv | ||
.find((item) => item.startsWith("--template=")) | ||
.replace(/--template=/, "") | ||
htmlPlugin({ | ||
files: [ | ||
{ | ||
filename: "index.html", | ||
entryPoints: [entryPathRelativeToCwd], | ||
htmlTemplate: path.join(__dirname, "./ui-template.html"), | ||
scriptLoading: "module", | ||
}, | ||
{ | ||
filename: "./demo/index.html", | ||
entryPoints: [entryPathRelativeToCwd], | ||
htmlTemplate: process.argv.find((item) => | ||
item.startsWith("--template=") | ||
) | ||
: path.join(__dirname, "./demo-template.html"), | ||
}), | ||
new webpack.DefinePlugin({ | ||
USE_FULL_IFRAME_URL: JSON.stringify(useFullframeUrl), | ||
? path.join( | ||
process.cwd(), | ||
process.argv | ||
.find((item) => item.startsWith("--template=")) | ||
.replace(/--template=/, "") | ||
) | ||
: path.join(__dirname, "./demo-template.html"), | ||
scriptLoading: "module", | ||
}, | ||
], | ||
}), | ||
...(isBuild ? [] : [watchPlugin]), | ||
], | ||
}); | ||
}; | ||
|
||
if (isBuild) { | ||
console.log("Building Reshowcase bundle..."); | ||
compiler.run((err, stats) => { | ||
// https://webpack.js.org/api/node/#error-handling | ||
if (err) { | ||
console.error("Build failed. Webpack fatal errors:\n", err); | ||
process.exit(1); | ||
const getPort = () => { | ||
const defaultPort = 8000; | ||
const prefix = "--port="; | ||
const arg = process.argv.find((item) => item.startsWith(prefix)); | ||
if (arg === undefined) { | ||
return defaultPort; | ||
} else { | ||
const portStr = arg.replace(prefix, ""); | ||
if (portStr === "") { | ||
return defaultPort; | ||
} else { | ||
const info = stats.toJson(); | ||
if (stats.hasErrors && info.errors.length > 0) { | ||
console.error( | ||
"Build failed. Webpack complilation errors:\n", | ||
info.errors | ||
); | ||
process.exit(1); | ||
} else { | ||
console.log( | ||
stats.toString({ assets: true, chunks: true, colors: true }) | ||
); | ||
console.log("Reshowcase build finished successfully."); | ||
} | ||
const parsed = parseInt(portStr, 10); | ||
return isNaN(parsed) ? defaultPort : parsed; | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
const {pathRewrites, ...esCustomConfig} = customConfig; | ||
|
||
const config = { | ||
...defaultConfig, | ||
...esCustomConfig, | ||
define: { ...defaultConfig.define, ...(customConfig.define || {}) }, | ||
plugins: [...defaultConfig.plugins, ...(customConfig.plugins || [])], | ||
}; | ||
|
||
if (isBuild) { | ||
const durationLabel = "[reshowcase] Build finished. Duration"; | ||
console.time(durationLabel); | ||
|
||
esbuild | ||
.build(config) | ||
.then((_buildResult) => { | ||
console.timeEnd(durationLabel); | ||
}) | ||
.catch((error) => { | ||
console.error("[reshowcase] Esbuild build failed:", error); | ||
process.exit(1); | ||
}); | ||
} else { | ||
const port = parseInt( | ||
process.argv.find((item) => item.startsWith("--port=")) | ||
? process.argv | ||
.find((item) => item.startsWith("--port=")) | ||
.replace(/--port=/, "") | ||
: 9000, | ||
10 | ||
); | ||
const durationLabel = "[reshowcase] Watch and serve started. Duration"; | ||
console.time(durationLabel); | ||
const port = getPort(); | ||
|
||
const server = new WebpackDevServer( | ||
{ | ||
compress: true, | ||
port: port, | ||
historyApiFallback: { | ||
index: "/index.html", | ||
}, | ||
devMiddleware: { | ||
publicPath: "/", | ||
stats: "errors-warnings", | ||
}, | ||
...(config.devServer || {}), | ||
}, | ||
compiler | ||
); | ||
esbuild | ||
.context(config) | ||
.then((ctx) => { | ||
return ctx.watch().then(() => ctx); | ||
}) | ||
.catch((error) => { | ||
console.error("[reshowcase] Esbuild watch start failed:", error); | ||
process.exit(1); | ||
}) | ||
.then((ctx) => { | ||
const server = ctx.serve({ | ||
servedir: outputPath, | ||
// ensure that esbuild doesn't take server's port | ||
port: port + 1, | ||
}); | ||
return {esbuildServer: server, esbuildCtx: ctx} | ||
}) | ||
.then(({esbuildServer, esbuildCtx}) => { | ||
const server = http.createServer((req, res) => { | ||
let options = { | ||
path: req.url, | ||
method: req.method, | ||
headers: req.headers, | ||
}; | ||
Comment on lines
+239
to
+244
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a minimal graceful shutdown for esbuild server and node http server? We'll listen for For esbuild:
https://esbuild.github.io/api/#build For node HTTP server:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done: 592039e
|
||
|
||
const rewrite = pathRewrites?.find(rewrite => req.url.startsWith(rewrite.context)) | ||
|
||
if (rewrite) { | ||
if (rewrite.socketPath) { | ||
options.socketPath = rewrite.socketPath; | ||
console.info(`[reshowcase] forwarding ${req.url} to ${options.socketPath}`) | ||
} else { | ||
const url = new URL(rewrite.target); | ||
options.host = url.hostname; | ||
options.port = url.port; | ||
options.headers = { | ||
...options.headers, | ||
...(rewrite.changeOrigin ? { host: `${options.host}:${options.port}` } : {}) | ||
}; | ||
console.info(`[reshowcase] forwarding ${req.url} to ${options.host}:${options.port}`); | ||
} | ||
} else { | ||
options.host = esbuildServer.host; | ||
options.port = esbuildServer.port; | ||
} | ||
|
||
["SIGINT", "SIGTERM"].forEach((signal) => { | ||
process.on(signal, () => { | ||
if (server) { | ||
server.stopCallback(() => { | ||
console.log("Webpack DevServer has stopped!"); | ||
process.exit(); | ||
const proxyReq = http.request(options, proxyRes => { | ||
res.writeHead(proxyRes.statusCode, proxyRes.headers) | ||
proxyRes.pipe(res, { end: true }) | ||
}) | ||
|
||
proxyReq.on("error", err => { | ||
console.error("[reshowcase] Proxy request error:", err); | ||
res.writeHead(500, { "Content-Type": "text/plain" }); | ||
res.end("Internal Server Error"); | ||
}); | ||
|
||
req.pipe(proxyReq, { end: true }) | ||
}) | ||
|
||
server.listen(port, error => { | ||
if (error) { | ||
return console.error(`[reshowcase] ${error}`) | ||
} else { | ||
console.timeEnd(durationLabel); | ||
console.log(`[reshowcase] Server started at: http://localhost:${port}`) | ||
} | ||
}); | ||
|
||
const shutdown = () => { | ||
console.log('[reshowcase] Received shutdown signal, shutting down gracefully...'); | ||
server.close((err) => { | ||
if (err) { | ||
console.error('[reshowcase] Error shutting down node server:', err); | ||
process.exit(1); | ||
} | ||
}); | ||
} else { | ||
process.exit(); | ||
} | ||
}); | ||
}); | ||
|
||
server.startCallback(() => console.log("Webpack DevServer has started!")); | ||
if (!esbuildCtx) { | ||
console.log('[reshowcase] Server shut down gracefully'); | ||
process.exit(0); | ||
} | ||
|
||
esbuildCtx.dispose() | ||
.then(() => { | ||
console.log('[reshowcase] Server shut down gracefully'); | ||
process.exit(0); | ||
}).catch((err) => { | ||
console.error('[reshowcase] Error shutting down esbuild server:', err); | ||
process.exit(1); | ||
}); | ||
|
||
setTimeout(() => { | ||
console.error('[reshowcase] Forcing shutdown due to timeout'); | ||
process.exit(1); | ||
}, 5000); | ||
}; | ||
|
||
// Handle termination signals | ||
process.on('SIGTERM', shutdown); | ||
process.on('SIGINT', shutdown); | ||
}) | ||
.catch((error) => { | ||
console.error("[reshowcase] Esbuild serve start failed:", error); | ||
process.exit(1); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd mention here
esbuild
and the@craftamap/esbuild-plugin-html
plugin.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added, but maybe it's better to vendor it?