Skip to content
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

Merged
merged 35 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
37dbc4e
Add esbuild dep
denis-ok Nov 3, 2023
9776074
Add esbuild html plugin
denis-ok Nov 3, 2023
aa114b6
Add basic code for esbuild
denis-ok Nov 3, 2023
b60c024
Pass params to serve
denis-ok Nov 3, 2023
9c49f0f
Use full iframe url in dev mode
denis-ok Nov 3, 2023
e6c2536
Pass entry path relative to cwd to html plugin
denis-ok Nov 4, 2023
6724342
Use single html plugin
denis-ok Nov 4, 2023
d09f16d
Add file loader for some basic formats
denis-ok Nov 5, 2023
6284bb3
Add filenames settings
denis-ok Nov 5, 2023
1645f5c
Merge branch 'main' into esbuild
denis-ok Aug 2, 2024
f79ea92
Update esbuild
denis-ok Aug 2, 2024
a8605aa
Remove webpack and concurrently
denis-ok Aug 2, 2024
905fe30
Update opam files
denis-ok Aug 2, 2024
b78c91f
Support port arg, subscribe to server event to reload
denis-ok Aug 2, 2024
e4b6af2
Specify public path, use tmpdir
denis-ok Aug 2, 2024
17bd7d5
Merge branch 'main' into esbuild
rusty-key Sep 5, 2024
a13cb07
try out proxy possibility
rusty-key Sep 5, 2024
9ff7335
add socket support
rusty-key Sep 5, 2024
215425a
clarify variable names
rusty-key Sep 5, 2024
b27ba4c
do not omit headers
rusty-key Sep 5, 2024
9e3debe
ensure that esbuild doesn’t take server’s port
rusty-key Sep 5, 2024
6121837
add [reshowcase] labels to logs
rusty-key Sep 9, 2024
92874ad
add timings to logs
rusty-key Sep 9, 2024
f638cb2
add more labels to logs
rusty-key Sep 9, 2024
4d6625e
Merge pull request #32 from ahrefs/esbuild-proxy
rusty-key Sep 9, 2024
2808462
separate dune and esbuild
rusty-key Sep 9, 2024
19caa5a
remove mentions of webpack from the readme
rusty-key Sep 9, 2024
8162be9
add changelog entry
rusty-key Sep 9, 2024
3037b6b
Update CHANGES.md
rusty-key Sep 14, 2024
1138071
do not pass default port
rusty-key Sep 14, 2024
d50b38e
add mention of required extensions
rusty-key Sep 14, 2024
592039e
add graceful shutdown
rusty-key Sep 14, 2024
2a1e0f8
remove leftovers
rusty-key Sep 14, 2024
0a8b5ea
don’t use let
rusty-key Sep 14, 2024
13dd9da
improve logs
rusty-key Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 5.5.0 (2025-09-09)

- Use `esbuild` instead of `webpack`: ([@denis-ok](https://github.com/denis-ok), [@rusty-key](https://github.com/rusty-key), [@jchavarri](https://github.com/jchavarri): https://github.com/ahrefs/reshowcase/pull/19)

## 5.4.0 (2024-08-02)

- Upgrade `reason-react` to `0.14` (thanks [@jchavarri](https://github.com/jchavarri)) ([914d280](https://github.com/ahrefs/reshowcase/pull/18))
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
project_name = reshowcase

DUNE = opam exec -- dune
BUILD_DIR = _build/default

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -55,7 +56,11 @@ test: ## Run tests

.PHONY: start-example
start-example: ## Runs the example in watch mode
$(DUNE) build -w @start-example --no-buffer # --no-buffer so that reshowcase output is shown
$(DUNE) build -w example --no-buffer # --no-buffer so that reshowcase output is shown

.PHONY: serve-example
serve-example: ## Serves example on given port
$(BUILD_DIR)/commands/reshowcase start --entry=./$(BUILD_DIR)/example/example/example/Demo.js -port=8000
Copy link
Member

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:

Suggested change
$(BUILD_DIR)/commands/reshowcase start --entry=./$(BUILD_DIR)/example/example/example/Demo.js -port=8000
$(BUILD_DIR)/commands/reshowcase start --entry=./$(BUILD_DIR)/example/example/example/Demo.js --port=8000

(also why do we need to pass it if it's the same value as the default?)

Copy link
Collaborator

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


.PHONY: build-example
build-example: ## Builds the example
Expand Down
13 changes: 1 addition & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Copy link
Member

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.

Copy link
Collaborator

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?


```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:
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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 new EventSource('/esbuild')... script manually if they want to keep livereload working.


If you need a custom template, pass `--template=./path/to/template.html`.
276 changes: 180 additions & 96 deletions commands/reshowcase
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)) {
Expand Down Expand Up @@ -77,7 +75,7 @@ const outputPath = (() => {
}
})();

const config = (() => {
const customConfig = (() => {
const configDir = path.join(process.cwd(), ".reshowcase");

if (!fs.existsSync(configDir)) {
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are needed because otherwise it would use the default [dir]/[name] right? Not sure if worth a comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 SIGINT and SIGTERM signals.

For esbuild:

When you are done with a context object, you can call dispose() on the context to wait for existing builds to finish, stop watch and/or serve mode, and free up resources.

https://esbuild.github.io/api/#build

For node HTTP server:

Stops the server from accepting new connections and closes all connections connected to this server which are not sending a request or waiting for a response.

https://nodejs.org/api/http.html#serverclosecallback

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 592039e

$ make build && make serve-example
opam exec -- dune build
_build/default/commands/reshowcase start --entry=./_build/default/example/example/example/Demo.js
[reshowcase] Watch and serve started. Duration: 25.562ms
[reshowcase] http://localhost:8000
[esbuild] Rebuild finished!
^C[reshowcase] Received shutdown signal, shutting down gracefully...
[reshowcase] Server shut down gracefully


["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}`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(`[reshowcase] http://localhost:${port}`)
console.log(`[reshowcase] Server started at http://localhost:${port}`)

}
})
})
.catch((error) => {
console.error("[reshowcase] Esbuild serve start failed:", error);
process.exit(1);
});
}
3 changes: 3 additions & 0 deletions commands/ui-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
flex-grow: 1;
}
</style>
<script>
new EventSource('/esbuild').addEventListener('change', () => location.reload());
</script>
</head>

<body>
Expand Down
Loading