Skip to content

Commit

Permalink
Merge pull request #19 from ahrefs/esbuild
Browse files Browse the repository at this point in the history
Switch from `webpack` to `esbuild`
  • Loading branch information
rusty-key authored Sep 14, 2024
2 parents f83b610 + 13dd9da commit d10ea8e
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 2,569 deletions.
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

.PHONY: build-example
build-example: ## Builds the example
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

Expand All @@ -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.

If you need a custom template, pass `--template=./path/to/template.html`.
310 changes: 214 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,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]",
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,
};

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);
});
}
Loading

0 comments on commit d10ea8e

Please sign in to comment.