-
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 29 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 |
---|---|---|
|
@@ -59,17 +59,6 @@ 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`: | ||
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'd mention here 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. added, but maybe it's better to vendor it? |
||
|
||
```json | ||
"devDependencies": { | ||
"copy-webpack-plugin": "^11.0.0", | ||
"html-webpack-plugin": "^5.5.0", | ||
"webpack": "^5.76.1", | ||
"webpack-dev-server": "^4.11.1", | ||
} | ||
``` | ||
|
||
## Usage | ||
|
||
### To start / develop: | ||
|
@@ -84,6 +73,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,194 @@ 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) => { | ||||||
return ctx.serve({ | ||||||
servedir: outputPath, | ||||||
// ensure that esbuild doesn't take server's port | ||||||
port: port + 1, | ||||||
}) | ||||||
}) | ||||||
.then(esbuildServer => { | ||||||
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
|
||||||
|
||||||
["SIGINT", "SIGTERM"].forEach((signal) => { | ||||||
process.on(signal, () => { | ||||||
if (server) { | ||||||
server.stopCallback(() => { | ||||||
console.log("Webpack DevServer has stopped!"); | ||||||
process.exit(); | ||||||
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; | ||||||
} | ||||||
|
||||||
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"); | ||||||
}); | ||||||
} else { | ||||||
process.exit(); | ||||||
} | ||||||
}); | ||||||
}); | ||||||
|
||||||
server.startCallback(() => console.log("Webpack DevServer has started!")); | ||||||
req.pipe(proxyReq, { end: true }) | ||||||
}) | ||||||
|
||||||
server.listen(port, error => { | ||||||
if (error) { | ||||||
return console.error(`[reshowcase] ${error}`) | ||||||
} else { | ||||||
console.timeEnd(durationLabel); | ||||||
console.log(`[reshowcase] http://localhost:${port}`) | ||||||
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.
Suggested change
|
||||||
} | ||||||
}) | ||||||
}) | ||||||
.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 think
-port
has two dashes:(also why do we need to pass it if it's the same value as the default?)
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.
we don't need to pass it, you are right. Removed