From bc46ffd2044015f768ea08869d8fb73c1f7ff47b Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Fri, 29 Mar 2024 16:22:05 +1100 Subject: [PATCH] Preserve path in referenced file manifest keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This means that if there is a “images/a/a.png” and a “images/b/b.png”, they’ll both have consistent manifest keys (“a/a.png” and “b/b.png” respectively), even in cases where esbuild is directly bundling one of the two files. --- dist/esbuild-plugin.js | 44 ++++++++++++++++++++++++++++++----- src/esbuild-plugin.ts | 47 +++++++++++++++++++++++++++++++++----- test/hanami-assets.test.ts | 14 +++++++----- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/dist/esbuild-plugin.js b/dist/esbuild-plugin.js index d7020a5..6ac8768 100644 --- a/dist/esbuild-plugin.js +++ b/dist/esbuild-plugin.js @@ -108,13 +108,45 @@ const hanamiEsbuild = (options) => { } // Add files already bundled by esbuild into the manifest const fileHashRegexp = /(-[A-Z0-9]{8})(\.\S+)$/; - for (const outputFile of outputFiles(outputs)) { - // Convert "public/assets/app-2TLUHCQ6.js" to "app.js" - let sourceUrl = outputFile - .replace(options.destDir + "/", "") - .replace(fileHashRegexp, "$2"); + const sourceAssetsDir = path.join(options.sourceDir, assetsDirName); // TODO make better + for (const outputFile in outputs) { + // Ignore esbuild's generated map files + if (outputFile.endsWith(".map")) { + continue; + } + const outputAttrs = outputs[outputFile]; + const inputFiles = Object.keys(outputAttrs.inputs); + // Determine the manifest key for the esbuild output file + let manifestKey; + if (!(outputFile.endsWith(".js") || outputFile.endsWith(".css")) && + inputFiles.length == 1 && + inputFiles[0].startsWith(sourceAssetsDir + path.sep)) { + // A non-JS/CSS output with a single input will be an asset file that has been been + // referenced from JS/CSS. + // + // In this case, preserve the original input file's path in the manifest key, so it + // matches any other files copied over from that path via processAssetDirectory. + // + // For example, given the input file "app/assets/images/icons/some-icon.png", return a + // manifest key of "icons/some-icon.png". + manifestKey = inputFiles[0] + .substring(sourceAssetsDir.length + 1) // + 1 to account for the sep + .split(path.sep) + .slice(1) + .join(path.sep); + } + else { + // For all other outputs, determine the manifest key based on the output file name, + // stripping away the hash suffix added by esbuild. + // + // For example, given the output "public/assets/app-2TLUHCQ6.js", return an manifest + // key of "app.js". + manifestKey = outputFile + .replace(options.destDir + path.sep, "") + .replace(fileHashRegexp, "$2"); + } const destinationUrl = calulateDestinationUrl(outputFile); - assetsManifest[sourceUrl] = prepareAsset(outputFile, destinationUrl); + assetsManifest[manifestKey] = prepareAsset(outputFile, destinationUrl); } // Add copied assets into the manifest for (const copiedAsset of copiedAssets) { diff --git a/src/esbuild-plugin.ts b/src/esbuild-plugin.ts index 204221c..c357241 100644 --- a/src/esbuild-plugin.ts +++ b/src/esbuild-plugin.ts @@ -170,15 +170,50 @@ const hanamiEsbuild = (options: PluginOptions): Plugin => { // Add files already bundled by esbuild into the manifest const fileHashRegexp = /(-[A-Z0-9]{8})(\.\S+)$/; - for (const outputFile of outputFiles(outputs)) { - // Convert "public/assets/app-2TLUHCQ6.js" to "app.js" - let sourceUrl = outputFile - .replace(options.destDir + "/", "") - .replace(fileHashRegexp, "$2"); + const sourceAssetsDir = path.join(options.sourceDir, assetsDirName); // TODO make better + for (const outputFile in outputs) { + // Ignore esbuild's generated map files + if (outputFile.endsWith(".map")) { + continue; + } + + const outputAttrs = outputs[outputFile]; + const inputFiles = Object.keys(outputAttrs.inputs); + + // Determine the manifest key for the esbuild output file + let manifestKey: string; + if ( + !(outputFile.endsWith(".js") || outputFile.endsWith(".css")) && + inputFiles.length == 1 && + inputFiles[0].startsWith(sourceAssetsDir + path.sep) + ) { + // A non-JS/CSS output with a single input will be an asset file that has been been + // referenced from JS/CSS. + // + // In this case, preserve the original input file's path in the manifest key, so it + // matches any other files copied over from that path via processAssetDirectory. + // + // For example, given the input file "app/assets/images/icons/some-icon.png", return a + // manifest key of "icons/some-icon.png". + manifestKey = inputFiles[0] + .substring(sourceAssetsDir.length + 1) // + 1 to account for the sep + .split(path.sep) + .slice(1) + .join(path.sep); + } else { + // For all other outputs, determine the manifest key based on the output file name, + // stripping away the hash suffix added by esbuild. + // + // For example, given the output "public/assets/app-2TLUHCQ6.js", return an manifest + // key of "app.js". + manifestKey = outputFile + .replace(options.destDir + path.sep, "") + .replace(fileHashRegexp, "$2"); + } const destinationUrl = calulateDestinationUrl(outputFile); - assetsManifest[sourceUrl] = prepareAsset(outputFile, destinationUrl); + assetsManifest[manifestKey] = prepareAsset(outputFile, destinationUrl); } // Add copied assets into the manifest diff --git a/test/hanami-assets.test.ts b/test/hanami-assets.test.ts index 0e57fb3..aa1767a 100644 --- a/test/hanami-assets.test.ts +++ b/test/hanami-assets.test.ts @@ -134,21 +134,23 @@ describe("hanami-assets", () => { test("handles references to files outside js/ and css/ directories", async () => { const entryPoint = path.join(dest, "app/assets/js/app.js"); await fs.writeFile(entryPoint, 'import "../css/app.css";'); + // const entryPoint2 = path.join(dest, "app/assets/js/nested/app.js"); + // await fs.writeFile(entryPoint2, ""); const cssFile = path.join(dest, "app/assets/css/app.css"); await fs.writeFile( cssFile, - '@font-face { font-family: "comic-mono"; src: url("../fonts/comic-mono.ttf"); }', + '@font-face { font-family: "comic-mono"; src: url("../fonts/comic-mono/comic-mono.ttf"); }', ); - const fontFile = path.join(dest, "app/assets/fonts/comic-mono.ttf"); + await fs.ensureDir(path.join(dest, "app/assets/fonts/comic-mono")); + const fontFile = path.join(dest, "app/assets/fonts/comic-mono/comic-mono.ttf"); await fs.writeFile(fontFile, ""); await assets.run({ root: dest, argv: ["--path=app", "--dest=public/assets"] }); const entryPointExists = await fs.pathExists(path.join("public/assets/app-6PW7FGD5.js")); expect(entryPointExists).toBe(true); - const cssExists = await fs.pathExists(path.join("public/assets/app-LI4JR7XG.css")); + const cssExists = await fs.pathExists(path.join("public/assets/app-GIY6HCGO.css")); expect(cssExists).toBe(true); - // NOT comic-mono-E3B0C442.ttf - it is a duplicate; this is what our manual asset copying creates. const fontExists = await fs.pathExists(path.join("public/assets/comic-mono-55DNWN2R.ttf")); expect(fontExists).toBe(true); @@ -162,11 +164,11 @@ describe("hanami-assets", () => { "app.js": { url: "/assets/app-6PW7FGD5.js", }, - "comic-mono.ttf": { + "comic-mono/comic-mono.ttf": { url: "/assets/comic-mono-55DNWN2R.ttf", }, "app.css": { - url: "/assets/app-LI4JR7XG.css", + url: "/assets/app-GIY6HCGO.css", }, }); });