@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 +20 -0
- package/README.md +55 -0
- package/_test-folder_/index.css +3 -0
- package/_test-folder_/index.html +9 -0
- package/_test-folder_/index.js +3 -0
- package/dynamic-imports-from-meta.js +11 -0
- package/dynamic-imports-from-meta.spec.js +34 -0
- package/package.json +23 -0
- package/plugin.js +142 -0
- package/plugin.spec.js +112 -0
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,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
|
+
});
|