Created
July 21, 2022 18:21
-
-
Save tgrushka/fcfbbb5588b75eddc1a4f8f7956d8b2f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Adapted from https://github.com/jfortunato/esbuild-plugin-manifest/blob/develop/src/index.ts | |
// Plugin I modified to make esbuild generate a manifest for Spring Boot application. No longer used. | |
import fs from "fs" | |
import path from "path" | |
import util from "util" | |
export default (options = {}) => ({ | |
name: "manifest", | |
setup(build) { | |
build.initialOptions.metafile = true | |
// assume that the user wants to hash their files by default, | |
// but don't override any hashing format they may have already set. | |
if (options.hash !== false && !build.initialOptions.entryNames) { | |
build.initialOptions.entryNames = "[dir]/[name]-[hash]" | |
} | |
build.onEnd((result) => { | |
// we'll map the input entry point filename to the output filename | |
const mappings = new Map() | |
const sourceRoot = build.initialOptions.sourceRoot || path.dirname(build.initialOptions.sourceRoot) | |
const outdir = build.initialOptions.outdir || path.dirname(build.initialOptions.outfile) | |
if (!result.metafile) { | |
throw new Error("Expected metafile, but it does not exist.") | |
} | |
const addMapping = (fullInputFilename, fullOutputFilename) => { | |
let inputFilename = fullInputFilename.replace(sourceRoot, "") | |
let outputFilename = fullOutputFilename.replace(outdir, "") | |
// Tom G. - 2022-07-15 - modified to work with com.github.dra11y:asset-thymeleaf-taglib | |
// Get the filename without the hash, i.e. /css/main-COMBIPD5.css => /css/main.css | |
let input = outputFilename.replace(/-[A-Z0-9]{8}/i, "") | |
// Original plugin: | |
// check if the shortNames option is being used on the input or output | |
// let input = shouldModify("input", options.shortNames) ? shortName(inputFilename) : inputFilename | |
let output = shouldModify("output", options.shortNames) ? shortName(outputFilename) : outputFilename | |
// check if the extensionless option is being used on the input or output | |
// input = shouldModify("input", options.extensionless) ? extensionless(input) : input | |
output = shouldModify("output", options.extensionless) ? extensionless(output) : output | |
// When shortNames are enabled, there can be conflicting filenames. | |
// For example if the entry points are ['src/pages/home/index.js', 'src/pages/about/index.js'] both of the | |
// short names will be 'index.js'. We'll just throw an error if a conflict is detected. | |
// | |
// There are also other scenarios that can cause a conflicting filename so we'll just ensure that the key | |
// we're trying to add doesn't already exist. | |
if (mappings.has(input)) { | |
throw new Error("There is a conflicting manifest key for '" + input + "'.") | |
} | |
mappings.set(input, output) | |
} | |
for (const outputFilename in result.metafile.outputs) { | |
const outputInfo = result.metafile.outputs[outputFilename] | |
// skip all outputs that don't have an entrypoint | |
if (!outputInfo.entryPoint) { | |
continue | |
} | |
addMapping(outputInfo.entryPoint, outputFilename) | |
// Check if this entrypoint has a "sibling" css file | |
// When esbuild encounters js files that import css files, it will gather all the css files referenced from the | |
// entrypoint and bundle it into a single sibling css file that follows the same naming structure as the entrypoint. | |
// So what we can do is simply check the outputs for a sibling file that matches the naming structure. | |
const siblingCssFile = findSiblingCssFile(result.metafile, outputFilename) | |
if (siblingCssFile !== undefined) { | |
// a sibling css file will always be given the same base name as its .js entrypoint, | |
// so it will always cause a conflict when used with the extensionless option | |
if (options.extensionless === true || options.extensionless === "input") { | |
throw new Error(`The extensionless option cannot be used when css is imported.`) | |
} | |
addMapping(siblingCssFile.input, siblingCssFile.output) | |
} | |
} | |
if (build.initialOptions.outdir === undefined && build.initialOptions.outfile === undefined) { | |
throw new Error("You must specify an 'outdir' when generating a manifest file.") | |
} | |
const filename = options.filename || "manifest.json" | |
const fullPath = path.resolve(`${outdir}/${filename}`) | |
const entries = fromEntries(mappings) | |
const resultObj = options.generate ? options.generate(entries) : entries | |
const text = typeof resultObj === "string" ? resultObj : JSON.stringify(resultObj, null, 2) | |
// With the esbuild write=false option, nothing will be written to disk. Instead, the build | |
// result will have an "outputFiles" property containing all the files that would have been written. | |
// We want to add the manifest file as one of those "outputFiles". | |
if (build.initialOptions.write === false) { | |
result.outputFiles?.push({ | |
path: fullPath, | |
contents: new util.TextEncoder().encode(text), | |
get text() { | |
return text | |
}, | |
}) | |
return | |
} | |
return fs.promises.writeFile(fullPath, text) | |
}) | |
}, | |
}) | |
const shouldModify = (inputOrOutput, optionValue) => { | |
return optionValue === inputOrOutput || optionValue === true | |
} | |
const shortName = (value) => { | |
return path.basename(value) | |
} | |
const extensionless = (value) => { | |
const parsed = path.parse(value) | |
const dir = parsed.dir !== "" ? `${parsed.dir}/` : "" | |
return `${dir}${parsed.name}` | |
} | |
const findSiblingCssFile = (metafile, outputFilename) => { | |
if (!outputFilename.endsWith(".js")) { | |
return | |
} | |
// we need to determine the difference in filenames between the input and output of the entrypoint, so that we can | |
// use that same logic to match against a potential sibling file | |
const entry = metafile.outputs[outputFilename].entryPoint | |
// "example.js" => "example" | |
const entryWithoutExtension = path.parse(entry).name | |
// "example-GQI5TWWV.js" => "example-GQI5TWWV" | |
const outputWithoutExtension = path.basename(outputFilename).replace(/\.js$/, "") | |
// "example-GQI5TWWV" => "-GQI5TWWV" | |
const diff = outputWithoutExtension.replace(entryWithoutExtension, "") | |
// esbuild uses [A-Z0-9]{8} as the hash, and that is not currently configurable so we should be able | |
// to match that exactly in the diff and replace it with the regex so we're left with: | |
// "-GQI5TWWV" => "-[A-Z0-9]{8}" | |
const hashRegex = new RegExp(diff.replace(/[A-Z0-9]{8}/, "[A-Z0-9]{8}")) | |
// the sibling entry is expected to be the same name as the entrypoint just with a css extension | |
const potentialSiblingEntry = path.parse(entry).dir + "/" + path.parse(entry).name + ".css" | |
const potentialSiblingOutput = outputFilename.replace(hashRegex, "").replace(/\.js$/, ".css") | |
const found = Object.keys(metafile.outputs).find(output => output.replace(hashRegex, "") === potentialSiblingOutput) | |
return found ? {input: potentialSiblingEntry, output: found} : undefined | |
} | |
const fromEntries = (map) => { | |
return Array.from(map).reduce((obj, [key, value]) => { | |
obj[key] = value | |
return obj | |
}, {}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import glob from "tiny-glob" | |
import path from "path" | |
import util from "util" | |
import {build} from "esbuild" | |
import {sassPlugin} from "esbuild-sass-plugin" | |
import manifestPlugin from "./manifest.esbuild.plugin.mjs" | |
import {esbuildCommands} from "esbuild-plugin-commands" | |
const isProd = process.env.NODE_ENV === "production" | |
const sourceRoot = "src/main/resources" | |
const entryPoints = await glob(`${sourceRoot}/{js,css}/**`, { | |
filesOnly: true, | |
}) | |
const templatesDir = `${sourceRoot}/templates` | |
var templates = await glob(`${templatesDir}/**`, {}) | |
const templateDirs = [templatesDir, `${templatesDir}/fragments`] | |
await build({ | |
entryPoints, | |
sourceRoot, | |
outdir: sourceRoot + "/static", | |
watch: process.argv.includes("--watch"), | |
logLevel: "info", | |
bundle: true, | |
treeShaking: true, | |
minify: isProd, | |
sourcemap: !isProd, | |
format: "esm", | |
target: "es2020", | |
plugins: [ | |
sassPlugin(), | |
manifestPlugin({ | |
filename: "manifest.json", | |
}), | |
esbuildCommands({ | |
onSuccess: "./gradlew assets", | |
}), | |
{ | |
name: "additional-watch-files", | |
setup(build) { | |
build.onLoad({filter: /.+/}, async (args) => { | |
return { | |
watchFiles: templates, | |
watchDirs: templateDirs, | |
} | |
}) | |
}, | |
}, | |
], | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment