@esbuild-toolbox/html-auto-include 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2026 Andrea Mannarà
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is furnished
10
+ to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
16
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
17
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # HTML Auto include
2
+ ## @esbuild-toolbox/html-auto-include
3
+
4
+ This package provides a utility that include automatically entrypoints reference to HTML passed. By default, it will try to include index.html in the current folder.
5
+ THe plugin will scan the final build and add `<script>` tags to the HTML with the reference to the generated files.
6
+ It also support `<link>` tags for stylesheets.
7
+
8
+ ### Installation
9
+
10
+ #### npm
11
+ ```bash
12
+ npm install @esbuild-toolbox/html-auto-include
13
+ ```
14
+
15
+ #### yarn
16
+ ```bash
17
+ yarn add @esbuild-toolbox/html-auto-include
18
+ ```
19
+
20
+ #### pnpm
21
+ ```bash
22
+ pnpm add @esbuild-toolbox/html-auto-include
23
+ ```
24
+
25
+ ### Usage
26
+
27
+ Basic usage it will search for index.html in the current folder and add the reference to the generated files.
28
+
29
+ ```js
30
+ import { htmlAutoIncludePlugin } from '@esbuild-toolbox/html-auto-include';
31
+
32
+ esbuild.build({
33
+ plugins: [htmlAutoIncludePlugin()],
34
+ });
35
+ ```
36
+
37
+ You can also pass a custom HTML file to include references to.
38
+
39
+ ```js
40
+ esbuild.build({
41
+ plugins: [htmlAutoIncludePlugin({ html: 'path/to/index.html' })],
42
+ });
43
+ ```
44
+
45
+ ### Options
46
+
47
+ - `html`: The path to the HTML file to include references to. Defaults to `index.html` in the current folder.
48
+ - `outfile`: The name of the output HTML file. Defaults to the value of `html`.
49
+ - `pathPrefix`: The path prefix to use for the generated files. Defaults to `./`.
50
+ - `scriptTagAttribute`: Attributes to add to the generated `<script>` tags. Defaults to `{}`.
51
+ - `base`: The base URL to use for the generated files. Defaults undefined
52
+
53
+ ## Acknowledgements
54
+
55
+ - [happy-dom](https://github.com/capricorn86/happy-dom)
@@ -0,0 +1,3 @@
1
+ html {
2
+ background-color: red;
3
+ }
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test</title>
5
+ </head>
6
+ <body>
7
+ <main>test</main>
8
+ </body>
9
+ </html>
@@ -0,0 +1,3 @@
1
+ import "./index.css";
2
+
3
+ console.log("test");
@@ -0,0 +1,11 @@
1
+ export const dynamicImportsFromMeta = (metafile = {}) => {
2
+ const filesData = Object.values(metafile.outputs ?? {});
3
+
4
+ return filesData.flatMap((fileData) => {
5
+ if (!fileData.imports) return [];
6
+
7
+ return fileData.imports
8
+ .filter((importObj) => importObj.kind === "dynamic-import")
9
+ .map(({ path }) => path);
10
+ });
11
+ };
@@ -0,0 +1,34 @@
1
+ import { test, after, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { dynamicImportsFromMeta } from "./dynamic-imports-from-meta.js";
4
+
5
+ const metafile = {
6
+ outputs: {
7
+ "dist/index.js": {
8
+ imports: [
9
+ { kind: "dynamic-import", path: "path/to/dynamic-import.js" },
10
+ {
11
+ kind: "other",
12
+ path: "path/to/other.js",
13
+ },
14
+ ],
15
+ },
16
+ },
17
+ };
18
+
19
+ describe("dynamicImportsFromMeta", () => {
20
+ test("should return dynamic imports from meta file", () => {
21
+ const result = dynamicImportsFromMeta(metafile);
22
+ assert.deepStrictEqual(result, ["path/to/dynamic-import.js"]);
23
+ });
24
+
25
+ test("should return empty array if no dynamic imports", () => {
26
+ const result = dynamicImportsFromMeta({});
27
+ assert.deepStrictEqual(result, []);
28
+ });
29
+
30
+ test("should return empty array if no output", () => {
31
+ const result = dynamicImportsFromMeta({ outputs: {} });
32
+ assert.deepStrictEqual(result, []);
33
+ });
34
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@esbuild-toolbox/html-auto-include",
3
+ "version": "0.0.1",
4
+ "description": "A utility that include automatically entrypoints reference to HTML passed.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/huvber/esbuild-toolbox.git",
9
+ "directory": "packages/html-auto-include"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "devDependencies": {
15
+ "esbuild": "^0.28.0"
16
+ },
17
+ "dependencies": {
18
+ "happy-dom": "^20.10.2"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test \"**/*.spec.js\""
22
+ }
23
+ }
package/plugin.js ADDED
@@ -0,0 +1,142 @@
1
+ import { Window } from 'happy-dom';
2
+ import fs from 'node:fs/promises'
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ import pkg from "./package.json" with {type: 'json'};
7
+ import { dynamicImportsFromMeta } from './dynamic-imports-from-meta.js';
8
+
9
+
10
+ /**
11
+ * Setup __dirname
12
+ */
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ /**
17
+ * Initialize happy-dom
18
+ */
19
+ const window = new Window();
20
+ const document = window.document;
21
+
22
+ /**
23
+ * Setup constants
24
+ */
25
+ const CSS_EXTENSION = '.css';
26
+ const JS_EXTENSION = '.js';
27
+ const CHUNK_FILE_PREFIX = 'chunk-';
28
+
29
+ const defaultOptions = {
30
+ html: 'index.html',
31
+ outfile: undefined,
32
+ pathPrefix: './',
33
+ scriptTagAttribute: {},
34
+ base: undefined
35
+ }
36
+
37
+ /**
38
+ * This plugin include automatically entrypoints reference to HTML passed.
39
+ * By default, it will try to include index.html in the current folder.
40
+ * THe plugin will scan the final build and add `<script>` tags to the HTML
41
+ * with the reference to the generated files.
42
+ * It also support `<link>` tags for stylesheets.
43
+ *
44
+ * @param {*} options
45
+ * @param {string} [options.html='index.html'] - The HTML file to include references to. Defaults to `'index.html'`.
46
+ * @param {string} [options.outfile] - The output file to write the modified HTML to. Defaults is equal to html filename`.
47
+ * @param {string} [options.pathPrefix='./'] - The path prefix to use for the generated files. Defaults to `'./'`.
48
+ * @param {object} [options.scriptTagAttribute={}] - The attributes to add to the `<script>` tags. Defaults to `{}`.
49
+ * @param {string} [options.base] - The base URL to use for the generated files.
50
+ *
51
+ * @returns
52
+ */
53
+ export function htmlAutoIncludePlugin(options = {}) {
54
+ const currentOptions = { ...defaultOptions, ...options }
55
+
56
+ return {
57
+ name: pkg.name,
58
+ async setup(build) {
59
+ // activate metafile
60
+ build.initialOptions.metafile = true;
61
+
62
+ // extract the template
63
+ const template = await fs.readFile(path.join(__dirname, currentOptions.html), 'utf8');
64
+
65
+ if (!template) throw new Error(`Could not read HTML template: ${currentOptions.html}`);
66
+
67
+ document.documentElement.innerHTML = template;
68
+
69
+ const documentBody = document.body;
70
+ const documentHead = document.head;
71
+
72
+ build.onEnd(async (result) => {
73
+ const dynamicImports = dynamicImportsFromMeta(result?.metafile)
74
+ const outputFiles = Object.keys(result?.metafile?.outputs ?? {});
75
+
76
+ for (const file of outputFiles) {
77
+ const isDynamicImport = dynamicImports.includes(file);
78
+ const isChunkFile = file.includes(CHUNK_FILE_PREFIX);
79
+
80
+ /**
81
+ * Dynamic import and chunk files are imported by javascript
82
+ */
83
+ if (isDynamicImport || isChunkFile) continue;
84
+
85
+ /**
86
+ * Setup the correct filename
87
+ */
88
+ const fileName = file.split('/').pop();
89
+ const prefixedFilename = `${currentOptions.pathPrefix}/${fileName}`.replaceAll('//', '/');
90
+
91
+ /**
92
+ * Add a script element for each JS file
93
+ */
94
+ if (fileName.endsWith(JS_EXTENSION)) {
95
+ const element = document.createElement('script')
96
+ element.src = prefixedFilename;
97
+
98
+ for (const [attribute, value] of Object.entries(currentOptions.scriptAttributes ?? {})) {
99
+ element.setAttribute(attribute, value);
100
+ }
101
+
102
+ documentBody.appendChild(element);
103
+ continue;
104
+ }
105
+
106
+ /**
107
+ * Add a link element for each CSS file
108
+ */
109
+ if (fileName.endsWith(CSS_EXTENSION)) {
110
+ const element = document.createElement('link')
111
+ element.rel = 'stylesheet';
112
+ element.href = prefixedFilename;
113
+ documentHead.appendChild(element);
114
+ continue;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Add a base element if a base URL is provided
120
+ */
121
+ if(currentOptions.base) {
122
+ const baseElement = document.createElement('base')
123
+ baseElement.setAttribute('href', currentOptions.base)
124
+
125
+ document.documentElement.setAttribute('base', currentOptions.base);
126
+ documentHead.prepend(baseElement)
127
+
128
+ }
129
+
130
+ /**
131
+ * Add the resulting HTML to the output file
132
+ */
133
+ const resultedHtml = document.documentElement.outerHTML;
134
+ const outputFile = currentOptions.outfile || currentOptions.html.split('/').pop();
135
+
136
+ await fs.writeFile(path.join(build.initialOptions.outdir, outputFile), resultedHtml)
137
+ });
138
+ },
139
+ }
140
+ }
141
+
142
+ export default htmlAutoIncludePlugin;
package/plugin.spec.js ADDED
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs/promises";
2
+ import { test, describe, afterEach } from "node:test";
3
+ import assert from "node:assert";
4
+
5
+ import { build } from "esbuild";
6
+ import { htmlAutoIncludePlugin } from "./plugin.js";
7
+
8
+ describe("htmlAutoIncludePlugin", async () => {
9
+ afterEach(async () => {
10
+ await fs.rm("./dist", { recursive: true, force: true });
11
+ });
12
+
13
+ test("should include the index.js and index.css", async () => {
14
+ await build({
15
+ entryPoints: ["./_test-folder_/index.js"],
16
+ bundle: true,
17
+ plugins: [
18
+ htmlAutoIncludePlugin({
19
+ html: "_test-folder_/index.html",
20
+ }),
21
+ ],
22
+ outdir: "./dist",
23
+ });
24
+
25
+ const generatedHtml = await fs.readFile("./dist/index.html", "utf-8");
26
+ console.log(generatedHtml);
27
+ assert.match(generatedHtml, /<script src=".\/index.js"><\/script>/);
28
+ assert.match(generatedHtml, /<link rel="stylesheet" href=".\/index.css">/);
29
+ });
30
+
31
+ test("should generate in another file", async () => {
32
+ await build({
33
+ entryPoints: ["./_test-folder_/index.js"],
34
+ bundle: true,
35
+ plugins: [
36
+ htmlAutoIncludePlugin({
37
+ html: "_test-folder_/index.html",
38
+ outfile: "foo.html",
39
+ }),
40
+ ],
41
+ outdir: "./dist",
42
+ });
43
+
44
+ const generatedHtml = await fs.readFile("./dist/foo.html", "utf-8");
45
+
46
+ assert.match(generatedHtml, /<script src=".\/index.js"><\/script>/);
47
+ assert.match(generatedHtml, /<link rel="stylesheet" href=".\/index.css">/);
48
+ });
49
+
50
+ test("should add the defined attributes to the script tag", async () => {
51
+ await build({
52
+ entryPoints: ["./_test-folder_/index.js"],
53
+ bundle: true,
54
+ plugins: [
55
+ htmlAutoIncludePlugin({
56
+ html: "_test-folder_/index.html",
57
+ scriptAttributes: {
58
+ module: true,
59
+ },
60
+ }),
61
+ ],
62
+ outdir: "./dist",
63
+ });
64
+
65
+ const generatedHtml = await fs.readFile("./dist/index.html", "utf-8");
66
+
67
+ assert.match(
68
+ generatedHtml,
69
+ /<script src=".\/index.js" module="true"><\/script>/
70
+ );
71
+ });
72
+
73
+ test("should add the correct prefix", async () => {
74
+ await build({
75
+ entryPoints: ["./_test-folder_/index.js"],
76
+ bundle: true,
77
+ plugins: [
78
+ htmlAutoIncludePlugin({
79
+ html: "_test-folder_/index.html",
80
+ pathPrefix: "./foo",
81
+ }),
82
+ ],
83
+ outdir: "./dist",
84
+ });
85
+
86
+ const generatedHtml = await fs.readFile("./dist/index.html", "utf-8");
87
+
88
+ assert.match(generatedHtml, /<script src=".\/foo\/index.js"><\/script>/);
89
+ assert.match(
90
+ generatedHtml,
91
+ /<link rel="stylesheet" href=".\/foo\/index.css">/
92
+ );
93
+ });
94
+
95
+ test("should add the correct base", async () => {
96
+ await build({
97
+ entryPoints: ["./_test-folder_/index.js"],
98
+ bundle: true,
99
+ plugins: [
100
+ htmlAutoIncludePlugin({
101
+ html: "_test-folder_/index.html",
102
+ base: "/foo",
103
+ }),
104
+ ],
105
+ outdir: "./dist",
106
+ });
107
+
108
+ const generatedHtml = await fs.readFile("./dist/index.html", "utf-8");
109
+
110
+ assert.match(generatedHtml, /<base href="\/foo">/);
111
+ });
112
+ });