Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: craftamap/esbuild-plugin-html
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.8.0
Choose a base ref
...
head repository: craftamap/esbuild-plugin-html
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.9.0
Choose a head ref
  • 9 commits
  • 9 files changed
  • 1 contributor

Commits on Dec 29, 2024

  1. chore: update jsdom

    craftamap committed Dec 29, 2024
    Copy the full SHA
    133ead8 View commit details
  2. Copy the full SHA
    936eca5 View commit details
  3. bump node version to 18

    craftamap committed Dec 29, 2024
    Copy the full SHA
    2ee7d90 View commit details
  4. feat: Set metafile automagically

    craftamap committed Dec 29, 2024
    Copy the full SHA
    d3bae1d View commit details
  5. fix: actually set metafile

    craftamap committed Dec 29, 2024
    Copy the full SHA
    866835f View commit details
  6. feat: log when there is an entrypoint specified that couldnt be resol…

    …ved sucessfully
    craftamap committed Dec 29, 2024
    Copy the full SHA
    757708e View commit details

Commits on Feb 2, 2025

  1. feat: support favicons with other extensions

    craftamap committed Feb 2, 2025
    Copy the full SHA
    b69ce36 View commit details

Commits on Mar 16, 2025

  1. upgrade jsdom to 26.0.0

    craftamap committed Mar 16, 2025
    Copy the full SHA
    940370a View commit details
  2. v0.9.0

    craftamap committed Mar 16, 2025
    Copy the full SHA
    a80c8a6 View commit details
Showing with 950 additions and 578 deletions.
  1. +0 −36 .eslintrc.json
  2. +6 −6 README.md
  3. +33 −0 eslint.config.mjs
  4. +50 −27 lib/cjs/index.js
  5. +10 −8 package.json
  6. +33 −17 src/index.ts
  7. +108 −4 test/e2e/index.test.mjs
  8. +2 −2 tsconfig.json
  9. +708 −478 yarn.lock
36 changes: 0 additions & 36 deletions .eslintrc.json

This file was deleted.

12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -14,12 +14,11 @@ Is any feature missing?

## Requirements

This plugin requires at least `esbuild` v0.12.26. Development was done on
node.js 16, node.js 14 should also work though.
This plugin requires at least `esbuild` v0.12.26. The minimum node version
supported is Node.js 18.

There is currently no deno version of this plugin - however, if there is need
for it, I will add one -
[just open a issue.](https://github.com/craftamap/esbuild-plugin-html/issues/new)
Deno is officially not supported - however, it has been reported that the plugin
does work with Deno.

## Installation

@@ -47,7 +46,8 @@ data into the template.
esbuild script:

- `outdir` must be set. The html files are generated within the `outdir`.
- `metafile` must be set to `true`.
- `metafile` must be set to `true` (the plugin does this automatically, if it's
not set to `false` on purpose).

⚠️: you can set a specific output name for resources using esbuild's
`entryNames` feature. While this plugin tries to support this as best as it
33 changes: 33 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
...[
eslint.configs.recommended,
...tseslint.configs.recommended,
].map(conf => ({
...conf,
files: ['**/*.ts'],
})),
{
files: ['**/*.ts'],
rules: {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
]
},
},
];
77 changes: 50 additions & 27 deletions lib/cjs/index.js
Original file line number Diff line number Diff line change
@@ -41,7 +41,10 @@ const htmlPlugin = (configuration = { files: [], }) => {
});
let logInfo = false;
function collectEntrypoints(htmlFileConfiguration, metafile) {
const entryPoints = Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([, value]) => {
if (!metafile) {
throw new Error('metafile is missing!');
}
const entryPoints = Object.entries(metafile?.outputs || {}).filter(([, value]) => {
if (!value.entryPoint) {
return false;
}
@@ -50,10 +53,16 @@ const htmlPlugin = (configuration = { files: [], }) => {
// Flatten the output, instead of returning an array, let's return an object that contains the path of the output file as path
return { path: outputData[0], ...outputData[1] };
});
if (entryPoints.length < htmlFileConfiguration.entryPoints.length) {
for (const htmlFileEntry of htmlFileConfiguration.entryPoints) {
if (!entryPoints.some(ep => ep.entryPoint === htmlFileEntry)) {
console.log('⚠️ for "%s", entrypoint "%s" was requested, but not found.', htmlFileConfiguration.filename, htmlFileEntry);
}
}
}
return entryPoints;
}
function findNameRelatedOutputFiles(entrypoint, metafile, entryNames) {
var _a, _b;
const pathOfMatchedOutput = path_1.default.parse(entrypoint.path);
// Search for all files that are "related" to the output (.css and map files, for example files, as assets are dealt with otherwise).
if (entryNames) {
@@ -69,13 +78,13 @@ const htmlPlugin = (configuration = { files: [], }) => {
.replace('\\[dir\\]', REGEXES.DIR_REGEX);
const findVariablesRegex = new RegExp(findVariablesRegexString);
const match = findVariablesRegex.exec(joinedPathOfMatch);
const name = (_a = match === null || match === void 0 ? void 0 : match.groups) === null || _a === void 0 ? void 0 : _a['name'];
const dir = (_b = match === null || match === void 0 ? void 0 : match.groups) === null || _b === void 0 ? void 0 : _b['dir'];
return Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([pathOfCurrentOutput,]) => {
const name = match?.groups?.['name'];
const dir = match?.groups?.['dir'];
return Object.entries(metafile?.outputs || {}).filter(([pathOfCurrentOutput,]) => {
if (entryNames) {
// if a entryName is set, we need to parse the output filename, get the name and dir,
// and find files that match the same criteria
const findFilesWithSameVariablesRegexString = escapeRegExp(entryNames.replace('[name]', name !== null && name !== void 0 ? name : '').replace('[dir]', dir !== null && dir !== void 0 ? dir : ''))
const findFilesWithSameVariablesRegexString = escapeRegExp(entryNames.replace('[name]', name ?? '').replace('[dir]', dir ?? ''))
.replace('\\[hash\\]', REGEXES.HASH_REGEX);
const findFilesWithSameVariablesRegex = new RegExp(findFilesWithSameVariablesRegexString);
return findFilesWithSameVariablesRegex.test(pathOfCurrentOutput);
@@ -87,7 +96,7 @@ const htmlPlugin = (configuration = { files: [], }) => {
}
else {
// If entryNames is not set, the related files are always next to the "main" output, and have the same filename, but the extension differs
return Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([key,]) => {
return Object.entries(metafile?.outputs || {}).filter(([key,]) => {
return path_1.default.parse(key).name === pathOfMatchedOutput.name && path_1.default.parse(key).dir === pathOfMatchedOutput.dir;
}).map(outputData => {
// Flatten the output, instead of returning an array, let's return an object that contains the path of the output file as path
@@ -118,7 +127,7 @@ const htmlPlugin = (configuration = { files: [], }) => {
}
async function injectFiles(dom, assets, outDir, publicPath, htmlFileConfiguration) {
const document = dom.window.document;
for (const script of (htmlFileConfiguration === null || htmlFileConfiguration === void 0 ? void 0 : htmlFileConfiguration.extraScripts) || []) {
for (const script of htmlFileConfiguration?.extraScripts || []) {
const scriptTag = document.createElement('script');
if (typeof script === 'string') {
scriptTag.setAttribute('src', script);
@@ -161,7 +170,9 @@ const htmlPlugin = (configuration = { files: [], }) => {
const scriptTag = document.createElement('script');
// Check if the JavaScript should be inlined.
if (isInline()) {
logInfo && console.log('Inlining script', filepath);
if (logInfo) {
console.log('Inlining script', filepath);
}
// Read the content of the JavaScript file, then append to the script tag
const scriptContent = await fs_1.default.promises.readFile(filepath, 'utf-8');
scriptTag.textContent = scriptContent;
@@ -196,27 +207,31 @@ const htmlPlugin = (configuration = { files: [], }) => {
document.head.appendChild(linkTag);
}
else {
logInfo && console.log(`Warning: found file ${targetPath}, but it was neither .js nor .css`);
if (logInfo) {
console.log(`Warning: found file ${targetPath}, but it was neither .js nor .css`);
}
}
}
}
return {
name: 'esbuild-html-plugin',
setup(build) {
build.onStart(() => {
if (!build.initialOptions.metafile) {
throw new Error('metafile is not enabled');
}
if (!build.initialOptions.outdir) {
throw new Error('outdir must be set');
}
});
if (build.initialOptions.metafile === false) {
throw new Error('metafile is explictly disabled. @craftamap/esbuild-html-plugin needs this to be enabled.');
}
// we need the metafile. If it's not set, we can set it to `true`
build.initialOptions.metafile = true;
if (!build.initialOptions.outdir) {
throw new Error('outdir must be set');
}
build.onEnd(async (result) => {
const startTime = Date.now();
if (build.initialOptions.logLevel == 'debug' || build.initialOptions.logLevel == 'info') {
logInfo = true;
}
logInfo && console.log();
if (logInfo) {
console.log();
}
for (const htmlFileConfiguration of configuration.files) {
// First, search for outputs with the configured entryPoints
const collectedEntrypoints = collectEntrypoints(htmlFileConfiguration, result.metafile);
@@ -229,8 +244,8 @@ const htmlPlugin = (configuration = { files: [], }) => {
const relatedOutputFiles = new Map();
relatedOutputFiles.set(entrypoint.path, entrypoint);
if (htmlFileConfiguration.findRelatedCssFiles) {
if (entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.cssBundle) {
relatedOutputFiles.set(entrypoint.cssBundle, { path: entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.cssBundle });
if (entrypoint?.cssBundle) {
relatedOutputFiles.set(entrypoint.cssBundle, { path: entrypoint?.cssBundle });
}
}
if (htmlFileConfiguration.findRelatedOutputFiles) {
@@ -241,7 +256,6 @@ const htmlPlugin = (configuration = { files: [], }) => {
collectedOutputFiles = [...collectedOutputFiles, ...relatedOutputFiles.values()];
}
// Note: we can safely disable this rule here, as we already asserted this in setup.onStart
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const outdir = build.initialOptions.outdir;
const publicPath = build.initialOptions.publicPath;
const templatingResult = await renderTemplate(htmlFileConfiguration);
@@ -254,12 +268,17 @@ const htmlPlugin = (configuration = { files: [], }) => {
}
if (htmlFileConfiguration.favicon) {
// Injects a favicon if present
await fs_1.default.promises.copyFile(htmlFileConfiguration.favicon, `${outdir}/favicon.ico`);
if (!fs_1.default.existsSync(htmlFileConfiguration.favicon)) {
throw new Error('favicon specified but does not exist');
}
const fileExt = path_1.default.extname(htmlFileConfiguration.favicon);
const faviconName = 'favicon' + fileExt;
await fs_1.default.promises.copyFile(htmlFileConfiguration.favicon, `${outdir}/${faviconName}`);
const linkTag = document.createElement('link');
linkTag.setAttribute('rel', 'icon');
let faviconPublicPath = '/favicon.ico';
let faviconPublicPath = `/${faviconName}`;
if (publicPath) {
faviconPublicPath = joinWithPublicPath(publicPath, 'favicon.ico');
faviconPublicPath = joinWithPublicPath(publicPath, faviconPublicPath);
}
linkTag.setAttribute('href', faviconPublicPath);
document.head.appendChild(linkTag);
@@ -271,9 +290,13 @@ const htmlPlugin = (configuration = { files: [], }) => {
});
await fs_1.default.promises.writeFile(out, dom.serialize());
const stat = await fs_1.default.promises.stat(out);
logInfo && console.log(` ${out} - ${stat.size}`);
if (logInfo) {
console.log(` ${out} - ${stat.size}`);
}
}
if (logInfo) {
console.log(` HTML Plugin Done in ${Date.now() - startTime}ms`);
}
logInfo && console.log(` HTML Plugin Done in ${Date.now() - startTime}ms`);
});
}
};
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@craftamap/esbuild-plugin-html",
"version": "0.8.0",
"version": "0.9.0",
"main": "./lib/cjs/index.js",
"repository": {
"type": "git",
@@ -20,21 +20,23 @@
"esbuild": ">=0.15.10"
},
"devDependencies": {
"@tsconfig/node12": "^1.0.9",
"@types/jsdom": "^16.2.13",
"@eslint/js": "^9.17.0",
"@tsconfig/node18": "^18.2.4",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.1",
"@types/node": "^16.9.1",
"@types/node": "^18.19.68",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"esbuild": "^0.21.1",
"eslint": "^8.8.0",
"typescript": "^4.5.5"
"eslint": "^9.17.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.2"
},
"engines": {
"node": ">13"
"node": ">=18"
},
"dependencies": {
"jsdom": "^17.0.0",
"jsdom": "^26.0.0",
"lodash": "^4.17.21"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
Loading