├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── package.json
├── src
└── main.ts
├── test
├── Roboto-Bold.woff2
├── Roboto-Regular.woff
├── Roboto-Regular.woff2
├── entry.js
├── expected-1.html
├── expected-2.html
├── index.test.ts
├── jest.config.js
├── setupTest.js
└── style.css
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directory
2 | node_modules
3 |
4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | jspm_packages/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | # OS metadata
91 | .DS_Store
92 | Thumbs.db
93 |
94 | # Ignore built ts files
95 | __tests__/runner/*
96 | __tests__/changeinfo.xml
97 | src/test.ts
98 | src/*.js
99 | __tests__/CHANGELOG-heavy.md
100 | lib
101 | test/dist/
102 | package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | test/
3 | .vscode/
4 | tsconfig.json
5 | .prettierignore
6 | .prettierrc.json
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /test/dist/
2 | lib/
3 | node_modules/
4 | README.md
5 | *.html
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": false,
9 | "arrowParens": "avoid",
10 | "parser": "typescript"
11 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "workbench.colorCustomizations": {
5 | "statusBar.background": "#000000",
6 | "statusBar.foreground": "#e7e7e7",
7 | "statusBarItem.hoverBackground": "#1a1a1a"
8 | },
9 | "peacock.color": "#000000",
10 | "peacock.affectActivityBar": false,
11 | "peacock.affectTitleBar": false,
12 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Principal Studio
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
10 | furnished 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
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/@principalstudio/html-webpack-inject-preload) [](https://nodejs.org/)
2 |
3 |
4 |
5 | # HTML Webpack Inject Preload
6 | A [HTML Webpack Plugin](https://github.com/jantimon/html-webpack-plugin) for injecting [<link rel='preload'>](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content)
7 |
8 | This plugin allows to add preload links anywhere you want.
9 |
10 | # Installation
11 |
12 | You need to have HTMLWebpackPlugin v4 or v5 to make this plugin work.
13 |
14 | ```
15 | npm i -D @principalstudio/html-webpack-inject-preload
16 | ```
17 |
18 | **webpack.config.js**
19 |
20 | ```js
21 | const HtmlWebpackPlugin = require('html-webpack-plugin');
22 | const HtmlWebpackInjectPreload = require('@principalstudio/html-webpack-inject-preload');
23 |
24 | module.exports = {
25 | entry: 'index.js',
26 | output: {
27 | path: __dirname + '/dist',
28 | filename: 'index_bundle.js'
29 | },
30 | plugins: [
31 | new HtmlWebpackPlugin(),
32 | new HtmlWebpackInjectPreload({
33 | files: [
34 | {
35 | match: /.*\.woff2$/,
36 | attributes: {as: 'font', type: 'font/woff2', crossorigin: true },
37 | },
38 | {
39 | match: /vendors\.[a-z-0-9]*.css$/,
40 | attributes: {as: 'style' },
41 | },
42 | ]
43 | })
44 | ]
45 | }
46 | ```
47 |
48 | **Options**
49 |
50 | * files: An array of files object
51 | * match: A regular expression to target files you want to preload
52 | * attributes: Any attributes you want to use. The plugin will add the attribute `rel="preload"` by default.
53 |
54 | **Usage**
55 |
56 | The plugin is really simple to use. The plugin injects in `headTags`, before any link, the preload elements.
57 |
58 | For example
59 |
60 | ```html
61 |
62 |
63 |
64 |
65 | Webpack App
66 | <%= htmlWebpackPlugin.tags.headTags %>
67 |
68 |
69 |
70 |
71 |
72 | ```
73 |
74 | will generate
75 |
76 | ```html
77 |
78 |
79 |
80 |
81 | Webpack App
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | ```
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@principalstudio/html-webpack-inject-preload",
3 | "version": "1.2.7",
4 | "description": "A HTML Webpack plugin for injecting ",
5 | "main": "lib/main.js",
6 | "types": "lib/main.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "test": "jest --config=./test/jest.config.js",
10 | "all": "npm run build && npm run test && npm link",
11 | "all-quick": "npm run build && npm link",
12 | "prepublishOnly": "npm run build && npm run test"
13 | },
14 | "engines": {
15 | "node": ">=10.23"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/principalstudio/html-webpack-inject-preload.git"
20 | },
21 | "keywords": [
22 | "webpack",
23 | "plugin",
24 | "html-webpack-plugin",
25 | "preload",
26 | "inject"
27 | ],
28 | "author": "Principal Studio",
29 | "bugs": {
30 | "url": "https://github.com/principalstudio/html-webpack-inject-preload/issues"
31 | },
32 | "homepage": "https://github.com/principalstudio/html-webpack-inject-preload#readme",
33 | "devDependencies": {
34 | "@types/jest": "^26.0.20",
35 | "@types/node": "^14.14.32",
36 | "@typescript-eslint/parser": "^4.16.1",
37 | "css-loader": "^5.1.1",
38 | "eslint": "^7.21.0",
39 | "file-loader": "^6.2.0",
40 | "html-webpack-plugin": "5.3.0",
41 | "jest": "^26.6.3",
42 | "mini-css-extract-plugin": "^1.3.9",
43 | "prettier": "^2.2.1",
44 | "ts-jest": "^26.5.3",
45 | "typescript": "^4.2.3",
46 | "url-loader": "^4.1.1",
47 | "webpack": "^5.24.4"
48 | },
49 | "peerDependencies": {
50 | "html-webpack-plugin": "^4.0.0 || ^5.0.0",
51 | "webpack": "^4.0.0 || ^5.0.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | default as HtmlWebpackPluginInstance,
3 | HtmlTagObject,
4 | } from 'html-webpack-plugin';
5 | import type {Compilation, Compiler, WebpackPluginInstance} from 'webpack';
6 |
7 | declare namespace HtmlWebpackInjectPreload {
8 | interface Options {
9 | files: HtmlWebpackInjectPreload.File[];
10 | }
11 |
12 | interface File {
13 | match: RegExp;
14 | attributes: Record;
15 | }
16 | }
17 |
18 | interface HtmlWebpackPluginData {
19 | headTags: HtmlWebpackPluginInstance.HtmlTagObject[];
20 | bodyTags: HtmlWebpackPluginInstance.HtmlTagObject[];
21 | outputName: string;
22 | publicPath: string;
23 | plugin: HtmlWebpackPluginInstance;
24 | }
25 |
26 | /**
27 | * Inject preload files before the content of the targeted files
28 | *
29 | * @example
30 | * new HtmlWebpackInjectPreload({
31 | * files: [
32 | * {
33 | * match: /.*\.woff2/$,
34 | * attributes: { rel: 'preload', as: 'font', type: 'font/woff2',
35 | * crossorigin: true },
36 | * },
37 | * {
38 | * match: /vendors\.[a-z-0-9]*.css/$,
39 | * attributes: { rel: 'preload', as: 'style' },
40 | * },
41 | * ],
42 | * })
43 | *
44 | * @class InjectPreloadFiles
45 | */
46 | class HtmlWebpackInjectPreload implements WebpackPluginInstance {
47 | private options: HtmlWebpackInjectPreload.Options = {
48 | files: [],
49 | };
50 |
51 | /**
52 | * Creates an instance of HtmlWebpackInjectPreload.
53 | *
54 | * @memberof InjectPreloadFiles
55 | */
56 | constructor(options: HtmlWebpackInjectPreload.Options) {
57 | this.options = Object.assign(this.options, options);
58 | }
59 |
60 | /**
61 | * Extract HTMLWebpack Plugin by jahed
62 | *
63 | * @param compiler
64 | */
65 | private extractHtmlWebpackPluginModule = (
66 | compiler: Compiler,
67 | ): typeof HtmlWebpackPluginInstance | null => {
68 | const htmlWebpackPlugin = (compiler.options.plugins || []).find(plugin => {
69 | return plugin.constructor.name === 'HtmlWebpackPlugin';
70 | }) as typeof HtmlWebpackPluginInstance | undefined;
71 | if (!htmlWebpackPlugin) {
72 | return null;
73 | }
74 | const HtmlWebpackPlugin = htmlWebpackPlugin.constructor;
75 | if (!HtmlWebpackPlugin || !('getHooks' in HtmlWebpackPlugin)) {
76 | return null;
77 | }
78 | return HtmlWebpackPlugin as typeof HtmlWebpackPluginInstance;
79 | };
80 |
81 | private addLinks(
82 | compilation: Compilation,
83 | htmlPluginData: HtmlWebpackPluginData,
84 | ) {
85 | //Get public path
86 | //html-webpack-plugin v5
87 | let publicPath = htmlPluginData.publicPath;
88 |
89 | //html-webpack-plugin v4
90 | if (typeof publicPath === 'undefined') {
91 | if (
92 | htmlPluginData.plugin.options?.publicPath &&
93 | htmlPluginData.plugin.options?.publicPath !== 'auto'
94 | ) {
95 | publicPath = htmlPluginData.plugin.options?.publicPath;
96 | } else {
97 | publicPath =
98 | typeof compilation.options.output.publicPath === 'string'
99 | ? compilation.options.output.publicPath
100 | : '/';
101 | }
102 |
103 | //prevent wrong url
104 | if (publicPath[publicPath.length - 1] !== '/') {
105 | publicPath = publicPath + '/';
106 | }
107 | }
108 |
109 | //Get assets name
110 | const assets = new Set(Object.keys(compilation.assets));
111 | compilation.chunks.forEach(chunk => {
112 | chunk.files.forEach((file: string) => assets.add(file));
113 | });
114 |
115 | //Find first link index to inject before
116 | const linkIndex = htmlPluginData.headTags.findIndex(
117 | tag => tag.tagName === 'link',
118 | );
119 |
120 | assets.forEach(asset => {
121 | for (let index = 0; index < this.options.files.length; index++) {
122 | const file = this.options.files[index];
123 |
124 | if (file.match.test(asset)) {
125 | let href =
126 | file.attributes && file.attributes.href
127 | ? file.attributes.href
128 | : false;
129 | if (href === false || typeof href === 'undefined') {
130 | href = asset;
131 | }
132 | href = href[0] === '/' ? href : publicPath + href;
133 |
134 | const preload: HtmlTagObject = {
135 | tagName: 'link',
136 | attributes: Object.assign(
137 | {
138 | rel: 'preload',
139 | href,
140 | },
141 | file.attributes,
142 | ),
143 | voidTag: true,
144 | meta: {
145 | plugin: 'html-webpack-inject-preload',
146 | },
147 | };
148 |
149 | if (linkIndex > -1) {
150 | //before link
151 | htmlPluginData.headTags.splice(linkIndex, 0, preload);
152 | } else {
153 | // before everything
154 | htmlPluginData.headTags.unshift(preload);
155 | }
156 | }
157 | }
158 | });
159 |
160 | return htmlPluginData;
161 | }
162 |
163 | apply(compiler: Compiler) {
164 | compiler.hooks.compilation.tap('HtmlWebpackInjectPreload', compilation => {
165 | const HtmlWebpackPlugin = this.extractHtmlWebpackPluginModule(compiler);
166 | if (!HtmlWebpackPlugin) {
167 | throw new Error(
168 | 'HtmlWebpackInjectPreload needs to be used with html-webpack-plugin 4 or 5',
169 | );
170 | }
171 |
172 | const hooks = HtmlWebpackPlugin.getHooks(compilation);
173 | hooks.alterAssetTagGroups.tapAsync(
174 | 'HtmlWebpackInjectPreload',
175 | (htmlPluginData, callback: any) => {
176 | try {
177 | callback(null, this.addLinks(compilation, htmlPluginData));
178 | } catch (error) {
179 | callback(error);
180 | }
181 | },
182 | );
183 | });
184 | }
185 | }
186 |
187 | export = HtmlWebpackInjectPreload;
188 |
--------------------------------------------------------------------------------
/test/Roboto-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/principalstudio/html-webpack-inject-preload/647ebcbaaf7ab7f319218a261d347056b2d451f8/test/Roboto-Bold.woff2
--------------------------------------------------------------------------------
/test/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/principalstudio/html-webpack-inject-preload/647ebcbaaf7ab7f319218a261d347056b2d451f8/test/Roboto-Regular.woff
--------------------------------------------------------------------------------
/test/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/principalstudio/html-webpack-inject-preload/647ebcbaaf7ab7f319218a261d347056b2d451f8/test/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/test/entry.js:
--------------------------------------------------------------------------------
1 | require('./style.css');
--------------------------------------------------------------------------------
/test/expected-1.html:
--------------------------------------------------------------------------------
1 | Webpack App
--------------------------------------------------------------------------------
/test/expected-2.html:
--------------------------------------------------------------------------------
1 | Webpack App
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import HtmlWebpackInjectPreload from '../src/main';
2 | import HtmlWebpackPlugin from 'html-webpack-plugin';
3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
4 | import webpack, {WebpackPluginInstance} from 'webpack';
5 | import path from 'path';
6 | import fs from 'fs';
7 |
8 | const options: HtmlWebpackInjectPreload.Options = {
9 | files: [
10 | {
11 | match: /.*\.woff2$/,
12 | attributes: {
13 | as: 'font',
14 | type: 'font/woff2',
15 | crossorigin: true,
16 | },
17 | },
18 | {
19 | match: /.*\.woff$/,
20 | attributes: {
21 | as: 'font',
22 | type: 'font/woff',
23 | crossorigin: true,
24 | },
25 | },
26 | {
27 | match: /.*\.css$/,
28 | attributes: {as: 'style', href: 'test-alt.css'},
29 | },
30 | {
31 | match: /.*\.null/,
32 | attributes: {href: false},
33 | },
34 | ],
35 | };
36 |
37 | const config: webpack.Configuration = {
38 | mode: 'production',
39 | context: path.resolve(__dirname),
40 | entry: path.join(__dirname, 'entry.js'),
41 | module: {
42 | rules: [
43 | {
44 | test: /\.css$/i,
45 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
46 | },
47 | {
48 | test: /\.(woff|woff2)$/,
49 | use: [
50 | {
51 | loader: 'url-loader',
52 | options: {
53 | name: '[name].[ext]',
54 | limit: 8192,
55 | },
56 | },
57 | ],
58 | },
59 | ],
60 | },
61 | output: {
62 | path: path.join(__dirname, 'dist/test1'),
63 | publicPath: '/',
64 | },
65 | plugins: [
66 | new MiniCssExtractPlugin() as WebpackPluginInstance,
67 | new HtmlWebpackPlugin(),
68 | new HtmlWebpackInjectPreload(options),
69 | ],
70 | };
71 |
72 | describe('HTMLWebpackInjectPreload', () => {
73 | it('test plugin', done => {
74 | const compiler = webpack(config);
75 |
76 | compiler.run((err, stats) => {
77 | if (err) expect(err).toBeNull();
78 |
79 | const statsErrors = stats ? stats.compilation.errors : [];
80 | if (statsErrors.length > 0) {
81 | console.error(statsErrors);
82 | }
83 | expect(statsErrors.length).toBe(0);
84 |
85 | const result = fs.readFileSync(
86 | path.join(__dirname, 'dist/test1/index.html'),
87 | 'utf8',
88 | );
89 | const expected = fs.readFileSync(
90 | path.join(__dirname, 'expected-1.html'),
91 | 'utf8',
92 | );
93 | expect(result).toBe(expected);
94 | done();
95 | });
96 | });
97 |
98 | it('test plugin public path', done => {
99 | const config2 = Object.assign(config, {
100 | output: {
101 | path: path.join(__dirname, 'dist/test2'),
102 | publicPath: '/test',
103 | },
104 | });
105 | const compiler = webpack(config2);
106 |
107 | compiler.run((err, stats) => {
108 | if (err) expect(err).toBeNull();
109 |
110 | const statsErrors = stats ? stats.compilation.errors : [];
111 | if (statsErrors.length > 0) {
112 | console.error(statsErrors);
113 | }
114 | expect(statsErrors.length).toBe(0);
115 |
116 | const result = fs.readFileSync(
117 | path.join(__dirname, 'dist/test2/index.html'),
118 | 'utf8',
119 | );
120 | const expected = fs.readFileSync(
121 | path.join(__dirname, 'expected-2.html'),
122 | 'utf8',
123 | );
124 | expect(result).toBe(expected);
125 | done();
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/test/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ["**/?(*.)+(spec|test).ts?(x)"],
5 | setupFilesAfterEnv: ['/setupTest.js'],
6 | };
--------------------------------------------------------------------------------
/test/setupTest.js:
--------------------------------------------------------------------------------
1 | jest.setTimeout(25000);
2 | const fs = require('fs');
3 | if (!fs.existsSync('./test/dist'))
4 | fs.mkdirSync('./test/dist');
--------------------------------------------------------------------------------
/test/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto';
3 | font-weight: 400;
4 | src: url('./Roboto-Regular.woff2') format('woff2'),
5 | url('./Roboto-Regular.woff') format('woff');
6 | }
7 |
8 | @font-face {
9 | font-family: 'Roboto';
10 | font-weight: 700;
11 | src: url('./Roboto-Bold.woff2') format('woff2');
12 | }
13 |
14 | body {
15 | font-family: Roboto, sans-serif;
16 | background-color: red;
17 | color: white;
18 | }
19 |
20 | body::before {
21 | content: 'Hello world'
22 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
5 | "module": "commonjs",
6 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | "outDir": "./lib",
8 | "moduleResolution": "node",
9 | /* Redirect output structure to the directory. */
10 | "rootDir": "./src",
11 | /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
12 | "strict": true,
13 | /* Enable all strict type-checking options. */
14 | "noImplicitAny": false,
15 | /* Raise error on expressions and declarations with an implied 'any' type. */
16 | "esModuleInterop": true,
17 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
18 | "lib": [
19 | "es2019"
20 | ],
21 | "declaration": true,
22 | },
23 | "exclude": ["node_modules", "**/*.test.ts", "lib"],
24 | }
--------------------------------------------------------------------------------