Skip to content

Instantly share code, notes, and snippets.

@cshaa
Created September 16, 2026 19:32
Show Gist options
  • Select an option

  • Save cshaa/c915cbf3a7ef9d026b3bc00bf4cb54c3 to your computer and use it in GitHub Desktop.

Select an option

Save cshaa/c915cbf3a7ef9d026b3bc00bf4cb54c3 to your computer and use it in GitHub Desktop.
Astro: compile client side scripts into statically served JavaScript
<div id="clickable" style="background-color: blue">Click On Me!</div>
<script>
const el = docuement.querySelector("#clickable")!;
el.addEventListener("click", () => {
el.style.backgroundColor = "red";
});
</script>
<div id="clickable" style="background-color: blue">Click On Me!</div>
<script is:inline type="module" src="/scripts/Example.js"></script>
const el = docuement.querySelector("#clickable")!;
el.addEventListener("click", () => {
el.style.backgroundColor = "red";
});
import type { AstroIntegration } from "astro";
import { readdir } from "node:fs/promises";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
const SUFFIX = ".client.ts";
const OUT_DIR = "scripts";
async function findClientScripts(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await findClientScripts(fullPath)));
} else if (entry.name.endsWith(SUFFIX)) {
files.push(fullPath);
}
}
return files;
}
const entryName = (filePath: string) => basename(filePath, SUFFIX);
export default function clientScripts(): AstroIntegration {
let srcDir: string;
let entries: Map<string, string> = new Map();
return {
name: "clientScripts",
hooks: {
"astro:config:setup"({ config }) {
srcDir = fileURLToPath(config.srcDir);
},
async "astro:build:setup"({ target, updateConfig }) {
if (target !== "client") return;
const files = await findClientScripts(srcDir);
entries = new Map(files.map((f) => [entryName(f), f]));
updateConfig({
build: {
rollupOptions: {
input: Object.fromEntries(entries),
output: {
entryFileNames(chunk) {
if (entries.has(chunk.name)) return `${OUT_DIR}/[name].js`;
return "_astro/[name].[hash].js";
},
},
},
},
});
},
async "astro:server:setup"({ server }) {
const files = await findClientScripts(srcDir);
entries = new Map(files.map((f) => [entryName(f), f]));
server.middlewares.use(async (req, res, next) => {
const match = req.url?.match(new RegExp(`^/${OUT_DIR}/([^/]+)\\.js$`));
const filePath = match && entries.get(match[1]);
if (!filePath) return next();
try {
const result = await server.transformRequest(filePath);
if (!result) return next();
res.setHeader("Content-Type", "application/javascript");
res.end(result.code);
} catch (err) {
next(err);
}
});
},
},
};
}
import { defineConfig, envField } from "astro/config";
import clientScripts from "./src/integrations/client-scripts";
export default defineConfig({
integrations: [clientScripts()],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment