Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
5

Configure Feed

Select the types of activity you want to include in your feed.

Use content-hashed filenames

Grace Kind (Jul 11, 2026, 12:12 AM -0500) 25e698c5 0a0fdad0

+59 -43
+48 -38
eleventy.config.js
··· 1 1 import { linkHtml } from "./modulepreload.js"; 2 - import pkg from "./package.json" with { type: "json" }; 3 2 import { MIME } from "./scripts/serve-static.js"; 3 + import crypto from "node:crypto"; 4 4 import fs from "node:fs"; 5 5 import path from "node:path"; 6 + import { pathToFileURL } from "node:url"; 6 7 7 8 async function transformGlob(pattern, replacer) { 8 9 await Promise.all( 9 10 fs.globSync(pattern).map(async (filePath) => { 10 11 const content = await fs.promises.readFile(filePath, "utf-8"); 11 - const updated = replacer(content); 12 + const updated = await replacer(content); 12 13 if (content !== updated) await fs.promises.writeFile(filePath, updated); 13 14 }), 14 15 ); ··· 109 110 }, 110 111 }); 111 112 112 - // Auto-generate modulepreload tags 113 - eleventyConfig.addTransform( 114 - "modulepreload", 115 - async function (content, outputPath) { 116 - if (outputPath.endsWith(".html")) { 117 - const baseUrl = new URL("src", import.meta.url); 118 - return await linkHtml(content, { baseUrl, exclude: ["/lib/hls.js"] }); 119 - } 120 - return content; 121 - }, 122 - ); 113 + // Cache busting via content-hashed filenames 114 + eleventyConfig.on("eleventy.after", async ({ dir }) => { 115 + if (isDev) return; 116 + 117 + const buildBaseUrl = pathToFileURL(path.resolve(dir.output) + path.sep); 118 + 119 + const hashedUrlPath = (filePath) => { 120 + const hash = crypto 121 + .createHash("sha256") 122 + .update(fs.readFileSync(filePath)) 123 + .digest("hex") 124 + .slice(0, 10); 125 + const urlPath = 126 + "/" + path.relative(dir.output, filePath).split(path.sep).join("/"); 127 + return [urlPath, urlPath.replace(/(\.[^.]+)$/, `.${hash}$1`)]; 128 + }; 123 129 124 - // Cache busting query params 125 - eleventyConfig.on("eleventy.after", async ({ dir }) => { 126 - const bust = `?v=${pkg.version}`; 127 - const addBust = (_, before, ref, after) => `${before}${ref}${bust}${after}`; 130 + const imports = {}; 131 + for (const filePath of fs.globSync(`${dir.output}/js/**/*.js`)) { 132 + const [urlPath, hashed] = hashedUrlPath(filePath); 133 + imports[urlPath] = hashed; 134 + } 135 + const [cssUrlPath, hashedCssUrlPath] = hashedUrlPath( 136 + path.join(dir.output, "css", "style.css"), 137 + ); 128 138 129 - // JS module refs: `import ... from "x.js"`, `export ... from "x.js"`, 130 - // bare `import "x.js"`, and dynamic `import("x.js")`. 131 - const jsModuleRefs = 132 - /(\b(?:import|export)\b[^'"`;]*?from\s+['"]|\bimport\s*\(\s*['"]|\bimport\s+['"])(?!https?:\/\/|\/\/)([^'"`\n]+?\.m?js)(['"])/g; 133 - // CSS `@import "x.css"` 134 - const cssImports = 135 - /(@import\s+['"])(?!https?:\/\/|\/\/)([^'"\n]+?\.css)(['"])/g; 136 - // HTML attribute refs: <script src>, <link href>, etc. 137 - const htmlAttrRefs = 138 - /((?:src|href)\s*=\s*["'])(?!https?:\/\/|\/\/)([^"']+?\.(?:m?js|css))(["'])/g; 139 + const importMapTag = `<script type="importmap">${JSON.stringify({ imports })}</script>`; 140 + await transformGlob(`${dir.output}/*.html`, async (content) => { 141 + // linkHtml crawls the un-hashed files on disk, so it must run before renaming 142 + const linked = await linkHtml(content, { 143 + baseUrl: buildBaseUrl, 144 + exclude: ["/lib/hls.js"], 145 + urlMap: imports, 146 + }); 147 + return linked 148 + .replace("<head>", `<head>${importMapTag}`) 149 + .replace(`href="${cssUrlPath}"`, `href="${hashedCssUrlPath}"`); 150 + }); 139 151 140 - await Promise.all([ 141 - transformGlob(`${dir.output}/**/*.js`, (content) => 142 - content.replace(jsModuleRefs, addBust), 143 - ), 144 - transformGlob(`${dir.output}/**/*.css`, (content) => 145 - content.replace(cssImports, addBust), 146 - ), 147 - transformGlob(`${dir.output}/**/*.html`, (content) => 148 - content.replace(htmlAttrRefs, addBust).replace(jsModuleRefs, addBust), 149 - ), 150 - ]); 152 + for (const [urlPath, hashed] of [ 153 + ...Object.entries(imports), 154 + [cssUrlPath, hashedCssUrlPath], 155 + ]) { 156 + fs.renameSync( 157 + path.join(dir.output, "." + urlPath), 158 + path.join(dir.output, "." + hashed), 159 + ); 160 + } 151 161 }); 152 162 153 163 return {
+10 -4
modulepreload.js
··· 21 21 noFetch, 22 22 exclude, 23 23 includeDynamic, 24 + urlMap, 24 25 }) { 25 26 this.imports = imports; 26 27 this.baseUrl = baseUrl; ··· 29 30 this.exclude = exclude; 30 31 this.includeDynamic = includeDynamic; 31 32 this.importMap = importMap; 33 + this.urlMap = urlMap; 32 34 } 33 35 async visit(specifier, parent) { 34 36 const doExclude = this.exclude.some((e) => specifier.includes(e)); ··· 83 85 async collect() { 84 86 const parent = new URL("./index.js", this.baseUrl); 85 87 await Promise.all(this.imports.map((entry) => this.visit(entry, parent))); 86 - return [...this.dependencies].map((dep) => 87 - dep.replace(this.baseUrl.href, "/"), 88 - ); 88 + return [...this.dependencies].sort().map((dep) => { 89 + const urlPath = dep.replace(this.baseUrl.href, "/"); 90 + return this.urlMap[urlPath] ?? urlPath; 91 + }); 89 92 } 90 93 } 91 94 ··· 132 135 export async function getDependencies( 133 136 contents, 134 137 baseUrl, 135 - { noFetch, exclude = [], includeDynamic = false } = {}, 138 + { noFetch, exclude = [], includeDynamic = false, urlMap = {} } = {}, 136 139 ) { 137 140 if (!baseUrl) { 138 141 throw new Error("baseUrl is required"); ··· 151 154 noFetch, 152 155 exclude, 153 156 includeDynamic, 157 + urlMap, 154 158 }); 155 159 return collector.collect(); 156 160 } ··· 190 194 exclude, 191 195 includeComments, 192 196 includeDynamic, 197 + urlMap, 193 198 } = {}, 194 199 ) { 195 200 let html = htmlContentsOrUrl; ··· 202 207 exclude, 203 208 noFetch, 204 209 includeDynamic, 210 + urlMap, 205 211 }); 206 212 return injectPreloads(html, dependencies, { includeComments }); 207 213 }
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.17.131", 3 + "version": "0.17.132", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",