diff --git a/.gitignore b/.gitignore index 138033e..33864e3 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,5 @@ src/*.js __tests__/CHANGELOG-heavy.md lib test/dist/ -package-lock.json \ No newline at end of file +package-lock.json +test/**/dist diff --git a/.vscode/settings.json b/.vscode/settings.json index 8384e5c..d7d4966 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,4 +9,5 @@ "peacock.color": "#000000", "peacock.affectActivityBar": false, "peacock.affectTitleBar": false, + "liveServer.settings.multiRootWorkspaceName": "html-webpack-inject-preload", } \ No newline at end of file diff --git a/README.md b/README.md index 4db01ac..dfe07ce 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ module.exports = { * files: An array of files object * match: A regular expression to target files you want to preload * attributes: Any attributes you want to use. The plugin will add the attribute `rel="preload"` by default. +* entryPointWebpackPreload: boolean. support `/* webpackPreload: true */ ` magic comment, when preloading files in initial chunk, see this [issue](https://github.com/jantimon/html-webpack-plugin/issues/1317) in detail. `crossorigin` for font will be add automatically. **Usage** diff --git a/src/entry-point-webpack-preload.ts b/src/entry-point-webpack-preload.ts new file mode 100644 index 0000000..34f7d05 --- /dev/null +++ b/src/entry-point-webpack-preload.ts @@ -0,0 +1,161 @@ +import type { Compilation } from 'webpack'; +import type { + default as HtmlWebpackPluginInstance, + HtmlTagObject, +} from 'html-webpack-plugin'; + + +type AlterAssetTagGroupsHookParam = Parameters[1]>[0]; + +type EntryName = string; +type File = string; + +export function addLinkForEntryPointWebpackPreload( + compilation: Compilation, + htmlPluginData: AlterAssetTagGroupsHookParam, + ) { + + // Html can contain multiple entrypoints, entries contains preloaded ChunkGroups, ChunkGroups contains chunks, chunks contains files. + // Files are what we need. + + const entryFileMap = prepareEntryFileMap(compilation, htmlPluginData); + + // Prepare link tags for HtmlWebpackPlugin + const publicPath = getPublicPath(compilation, htmlPluginData); + const entryHtmlTagObjectMap = generateHtmlTagObject(entryFileMap, publicPath, compilation); + + // Related files's link tags should follow parent script tag (the entries scripts' tag) + // according to this [blog](https://web.dev/priority-hints/#using-preload-after-chrome-95). + alterAssetTagGroups(entryHtmlTagObjectMap, compilation, htmlPluginData); +} + +function alterAssetTagGroups(entryHtmlTagObjectMap: Map>, compilation: Compilation, htmlPluginData: AlterAssetTagGroupsHookParam) { + for (const [entryName, linkTags] of entryHtmlTagObjectMap) { + //Find first link index to inject before, which is the script elemet for the entrypoint. + let files = compilation.entrypoints.get(entryName)?.getEntrypointChunk().files; + if (!files || files.size === 0) { + continue; + } + const lastFile = [...files][files.size - 1] + const findLastFileScriptTagIndex = tag => tag.tagName === 'script' && (tag.attributes.src as string).indexOf(lastFile) !== -1; + let linkIndex = htmlPluginData.headTags.findIndex( + findLastFileScriptTagIndex + ); + if (linkIndex === -1) { + htmlPluginData.bodyTags.findIndex(findLastFileScriptTagIndex); + } + if (linkIndex === -1) { + console.warn(`cannot find entrypoints\'s script tags for entry: ${entryName}`); + continue; + }; + htmlPluginData.headTags.splice(linkIndex, 0, ...linkTags); + } +} + +/** + * Get entrypoints related preload files' names + * + * Html can contain multiple entrypoints, entries contains preloaded ChunkGroups, ChunkGroups contains chunks, chunks contains files. + * Files are what we need. + * @param compilation + * @param htmlPluginData + */ +function prepareEntryFileMap( + compilation: Compilation, + htmlPluginData: AlterAssetTagGroupsHookParam) { + const entryFileMap = new Map>; + + const entries = htmlPluginData.plugin.options?.chunks ?? 'all'; + let entriesKeys = Array.isArray(entries) ? entries : Array.from(compilation.entrypoints.keys()); + + for (const key of entriesKeys) { + const files = new Set(); + const preloaded = compilation.entrypoints.get(key)?.getChildrenByOrders(compilation.moduleGraph, compilation.chunkGraph).preload; + if (!preloaded) continue; + entryFileMap.set(key, files); + // cannot get font files in `preload` + for (const group of preloaded) { // the order of preloaded is relevant + for (const chunk of group.chunks) + for (const file of chunk.files) files.add(file); + } + } + + return entryFileMap; +} + +/** + * Generate HtmlTagObjects for HtmlWebpackPlugin + * @param entryFileMap + * @param publicPath + * @returns + */ +function generateHtmlTagObject(entryFileMap: Map>, publicPath: string, compilation: Compilation): Map> { + const map = new Map(); + for (const [key, filesNames] of entryFileMap) { + map.set(key, [...filesNames].map(fileName => { + const href = `${publicPath}${fileName}`; + const as = getTypeOfResource(fileName); + const crossOrigin = as === 'font' ? 'anonymous' : compilation.outputOptions.crossOriginLoading; + let attributes: HtmlTagObject['attributes'] = { + rel: 'preload', + href, + as + } + if (crossOrigin) { + attributes = { ...attributes, crossorigin: crossOrigin } + } + return { + tagName: 'link', + attributes, + voidTag: true, + meta: { + plugin: 'html-webpack-inject-preload', + }, + } + })); + + } + return map; +} + +function getTypeOfResource(fileName: String) { + if (fileName.match(/.js$/)) { + return 'script' + } + if (fileName.match(/.css$/)) { + return 'style' + } + if (fileName.match(/.(woff2|woff|ttf|otf)$/)) { + return 'font' + } + if (fileName.match(/.(gif|jpeg|png|svg)$/)) { + return 'image' + } +} + +function getPublicPath(compilation: Compilation, htmlPluginData: AlterAssetTagGroupsHookParam) { + //Get public path + //html-webpack-plugin v5 + let publicPath = htmlPluginData.publicPath; + + //html-webpack-plugin v4 + if (typeof publicPath === 'undefined') { + if ( + htmlPluginData.plugin.options?.publicPath && + htmlPluginData.plugin.options?.publicPath !== 'auto' + ) { + publicPath = htmlPluginData.plugin.options?.publicPath; + } else { + publicPath = + typeof compilation.options.output.publicPath === 'string' + ? compilation.options.output.publicPath + : '/'; + } + + //prevent wrong url + if (publicPath[publicPath.length - 1] !== '/') { + publicPath = publicPath + '/'; + } + } + return publicPath; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index d91613b..d001209 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,11 +2,17 @@ import type { default as HtmlWebpackPluginInstance, HtmlTagObject, } from 'html-webpack-plugin'; -import type {Compilation, Compiler, WebpackPluginInstance} from 'webpack'; +import type { Compilation, Compiler, WebpackPluginInstance } from 'webpack'; +import { addLinkForEntryPointWebpackPreload } from './entry-point-webpack-preload'; declare namespace HtmlWebpackInjectPreload { interface Options { - files: HtmlWebpackInjectPreload.File[]; + files?: HtmlWebpackInjectPreload.File[]; + /** + * generate link tag for `import()`s in a entry chunk + * @defaultValue false + */ + entryPointWebpackPreload?: boolean; } interface File { @@ -46,6 +52,7 @@ interface HtmlWebpackPluginData { class HtmlWebpackInjectPreload implements WebpackPluginInstance { private options: HtmlWebpackInjectPreload.Options = { files: [], + entryPointWebpackPreload: false }; /** @@ -106,6 +113,11 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance { } } + // generate link tag for `import()`s in a entry chunk + if (this.options.entryPointWebpackPreload) { + addLinkForEntryPointWebpackPreload(compilation, htmlPluginData); + } + //Get assets name const assets = new Set(Object.keys(compilation.assets)); compilation.chunks.forEach(chunk => { @@ -116,10 +128,13 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance { const linkIndex = htmlPluginData.headTags.findIndex( tag => tag.tagName === 'link', ); - + const files = this.options.files; + if (!files) { + return; + } assets.forEach(asset => { - for (let index = 0; index < this.options.files.length; index++) { - const file = this.options.files[index]; + for (let index = 0; index < files.length; index++) { + const file = files[index]; if (file.match.test(asset)) { let href = @@ -185,3 +200,5 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance { } export = HtmlWebpackInjectPreload; +// exports.default = HtmlWebpackInjectPreload; +// module.exports = HtmlWebpackInjectPreload; \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.css b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.css new file mode 100644 index 0000000..95aa070 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.css @@ -0,0 +1,11 @@ +body { + background-color: blue; + font-family: Roboto, sans-serif; +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + src: url('../../../../Roboto-Regular.woff2') format('woff2'), + url('../../../..//Roboto-Regular.woff') format('woff'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.js b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.js new file mode 100644 index 0000000..017cf62 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-async-chunk.js @@ -0,0 +1,4 @@ +import './await-imported-by-async-chunk.css'; +export async function awaitImportByAsyncChunk() { + console.log('awaitImportByAsyncChunk'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.css b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.css new file mode 100644 index 0000000..52bee54 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.css @@ -0,0 +1,11 @@ +body { + background-color: green; + font-family: Roboto, sans-serif; +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + src: url('../../../../Roboto-Regular.woff2') format('woff2'), + url('../../../..//Roboto-Regular.woff') format('woff'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.js b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.js new file mode 100644 index 0000000..65792e6 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/await-imported-by-intial-chunk.js @@ -0,0 +1,5 @@ +import './await-imported-by-intial-chunk.css'; +export async function awaitImportByInitialChunk() { + const { awaitImportByAsyncChunk } = await import(/* webpackPreload: true */'./await-imported-by-async-chunk'); + awaitImportByAsyncChunk(); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/entry.js b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/entry.js new file mode 100644 index 0000000..979cf20 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/entry.js @@ -0,0 +1,6 @@ +async function main() { + const { awaitImportByInitialChunk } = await import(/* webpackPreload: true */ './await-imported-by-intial-chunk'); + awaitImportByInitialChunk(); +} + +main(); \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/index.html b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/index.html new file mode 100644 index 0000000..6f4ed73 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/fixtures/index.html @@ -0,0 +1,10 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + foo + + \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-crossorigin/test-crossorigin.test.ts b/test/entrypoint-webpack-preload/spec/test-crossorigin/test-crossorigin.test.ts new file mode 100644 index 0000000..2e98c56 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-crossorigin/test-crossorigin.test.ts @@ -0,0 +1,64 @@ +import { webpack } from "webpack"; +import { getWebpackConfig } from "../../webpack-base-config"; +import path from 'path'; +import fs from 'fs'; +import { expectSuccessfulBuild } from "../utils"; +describe('HTMLWebpackInjectPreload test entry point webpack preload with cross origin loading', () => { + it('should add crossorigin attributions when wepack output crossOriginLoading is \'anonymous\'', done => { + let config = getWebpackConfig('./fixtures/entry.js'); + if (config.output) { + config.output.crossOriginLoading = 'anonymous'; + } + const compiler = webpack(config); + + compiler.run((err, stats) => { + expectSuccessfulBuild(err, stats); + + const html = fs.readFileSync( + path.join(__dirname, 'dist/index.html'), + 'utf8', + ); + const preloadRegex = /]+rel=["']preload["'][^>]+as=["']([^"']+)["'][^>]*>/; + const globalPreloadRegex = new RegExp(preloadRegex, "gm"); + const result = html.match(globalPreloadRegex); + expect(result).not.toBeNull(); + expect(result?.length).toBe(2); + + const corssOriginRegex = /]+rel=["']preload["'][^>]+crossorigin=["']([^"']+)["'][^>]*>/; + const cssPreload = result![0].match(corssOriginRegex); + expect(cssPreload![1]).toBe('anonymous'); + const jsPreload = result![1].match(corssOriginRegex); + expect(jsPreload![1]).toBe('anonymous'); + done(); + }); + }); + + it('should add crossorigin attributions when wepack output crossOriginLoading is \'use-credentials\'', done => { + let config = getWebpackConfig('./fixtures/entry.js'); + if (config.output) { + config.output.crossOriginLoading = 'use-credentials'; + } + const compiler = webpack(config); + + compiler.run((err, stats) => { + expectSuccessfulBuild(err, stats); + + const html = fs.readFileSync( + path.join(__dirname, 'dist/index.html'), + 'utf8', + ); + const preloadRegex = /]+rel=["']preload["'][^>]+as=["']([^"']+)["'][^>]*>/; + const globalPreloadRegex = new RegExp(preloadRegex, "gm"); + const result = html.match(globalPreloadRegex); + expect(result).not.toBeNull(); + expect(result?.length).toBe(2); + + const corssOriginRegex = /]+rel=["']preload["'][^>]+crossorigin=["']([^"']+)["'][^>]*>/; + const cssPreload = result![0].match(corssOriginRegex); + expect(cssPreload![1]).toBe('use-credentials'); + const jsPreload = result![1].match(corssOriginRegex); + expect(jsPreload![1]).toBe('use-credentials'); + done(); + }); + }); + }) \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.css b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.css new file mode 100644 index 0000000..95aa070 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.css @@ -0,0 +1,11 @@ +body { + background-color: blue; + font-family: Roboto, sans-serif; +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + src: url('../../../../Roboto-Regular.woff2') format('woff2'), + url('../../../..//Roboto-Regular.woff') format('woff'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.js b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.js new file mode 100644 index 0000000..017cf62 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-async-chunk.js @@ -0,0 +1,4 @@ +import './await-imported-by-async-chunk.css'; +export async function awaitImportByAsyncChunk() { + console.log('awaitImportByAsyncChunk'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.css b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.css new file mode 100644 index 0000000..52bee54 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.css @@ -0,0 +1,11 @@ +body { + background-color: green; + font-family: Roboto, sans-serif; +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + src: url('../../../../Roboto-Regular.woff2') format('woff2'), + url('../../../..//Roboto-Regular.woff') format('woff'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.js b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.js new file mode 100644 index 0000000..65792e6 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/await-imported-by-intial-chunk.js @@ -0,0 +1,5 @@ +import './await-imported-by-intial-chunk.css'; +export async function awaitImportByInitialChunk() { + const { awaitImportByAsyncChunk } = await import(/* webpackPreload: true */'./await-imported-by-async-chunk'); + awaitImportByAsyncChunk(); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/entry.js b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/entry.js new file mode 100644 index 0000000..979cf20 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/entry.js @@ -0,0 +1,6 @@ +async function main() { + const { awaitImportByInitialChunk } = await import(/* webpackPreload: true */ './await-imported-by-intial-chunk'); + awaitImportByInitialChunk(); +} + +main(); \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/index.html b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/index.html new file mode 100644 index 0000000..6f4ed73 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/fixtures/index.html @@ -0,0 +1,10 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + foo + + \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-css-preload/test-css-preload.test.ts b/test/entrypoint-webpack-preload/spec/test-css-preload/test-css-preload.test.ts new file mode 100644 index 0000000..168b805 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-css-preload/test-css-preload.test.ts @@ -0,0 +1,34 @@ +import webpack from 'webpack'; +import { getWebpackConfig } from '../../webpack-base-config'; +import path from 'path'; +import fs from 'fs'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import { expectSuccessfulBuild } from '../utils'; + +describe('HTMLWebpackInjectPreload test entry point webpack preload for css', () => { + it('should add preload link tag for css await imported by initial chunk, with \'style\ as attribute', done => { + let config = getWebpackConfig('./fixtures/entry.js'); + const htmlWebpackPlugin = config.plugins?.find(ele => ele.constructor && ele.constructor.name === 'HtmlWebpackPlugin') as HtmlWebpackPlugin; + htmlWebpackPlugin.userOptions.template = path.join(__dirname, './fixtures/index.html') + const compiler = webpack(config); + + compiler.run((err, stats) => { + expectSuccessfulBuild(err, stats); + const html = fs.readFileSync( + path.join(__dirname, 'dist/index.html'), + 'utf8', + ); + // capture 'as' attribute + const preloadRegex = /]+rel=["']preload["'][^>]+as=["']([^"']+)["'][^>]*>/; + const globalPreloadRegex = new RegExp(preloadRegex, "gm"); + const result = html.match(globalPreloadRegex); + expect(result).not.toBeNull(); + expect(result?.length).toBe(2); + const cssPreload = result![0].match(preloadRegex); + expect(cssPreload![1]).toBe('style'); + const jsPreload = result![1].match(preloadRegex); + expect(jsPreload![1]).toBe('script'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-async-chunk.js b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-async-chunk.js new file mode 100644 index 0000000..a7f1c2f --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-async-chunk.js @@ -0,0 +1,3 @@ +export async function awaitImportByAsyncChunk() { + console.log('awaitImportByAsyncChunk'); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-intial-chunk.js b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-intial-chunk.js new file mode 100644 index 0000000..a5bb317 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/await-imported-by-intial-chunk.js @@ -0,0 +1,4 @@ +export async function awaitImportByInitialChunk() { + const { awaitImportByAsyncChunk } = await import('./await-imported-by-async-chunk'); + awaitImportByAsyncChunk(); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/entry.js b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/entry.js new file mode 100644 index 0000000..979cf20 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-js-preload/fixtures/entry.js @@ -0,0 +1,6 @@ +async function main() { + const { awaitImportByInitialChunk } = await import(/* webpackPreload: true */ './await-imported-by-intial-chunk'); + awaitImportByInitialChunk(); +} + +main(); \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/test-js-preload/test-js-preload.test.ts b/test/entrypoint-webpack-preload/spec/test-js-preload/test-js-preload.test.ts new file mode 100644 index 0000000..009e7ff --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/test-js-preload/test-js-preload.test.ts @@ -0,0 +1,27 @@ +import webpack from 'webpack'; +import { getWebpackConfig } from '../../webpack-base-config'; +import path from 'path'; +import fs from 'fs'; +import { expectSuccessfulBuild } from '../utils'; + +describe('HTMLWebpackInjectPreload test entry point webpack preload', () => { + it('should add preload link tag for js await imported by initial chunk, with \'script\ as attribute', done => { + const config = getWebpackConfig('./fixtures/entry.js'); + const compiler = webpack(config); + + compiler.run((err, stats) => { + expectSuccessfulBuild(err, stats); + + const html = fs.readFileSync( + path.join(__dirname, 'dist/index.html'), + 'utf8', + ); + // capture 'as' attribute + const preloadRegex = /]+rel=["']preload["'][^>]+as=["']([^"']+)["'][^>]*>/m + const result = html.match(preloadRegex); + expect(result).not.toBeNull(); + expect(result![1]).toBe('script') + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/spec/utils.ts b/test/entrypoint-webpack-preload/spec/utils.ts new file mode 100644 index 0000000..b0b77d1 --- /dev/null +++ b/test/entrypoint-webpack-preload/spec/utils.ts @@ -0,0 +1,9 @@ +import { Stats } from "webpack"; +export function expectSuccessfulBuild(err: Error | null | undefined, stats?: Stats) { + if (err) expect(err).toBeNull(); + const statsErrors = stats ? stats.compilation.errors : []; + if (statsErrors.length > 0) { + console.error(statsErrors); + } + expect(statsErrors.length).toBe(0); +} \ No newline at end of file diff --git a/test/entrypoint-webpack-preload/webpack-base-config.ts b/test/entrypoint-webpack-preload/webpack-base-config.ts new file mode 100644 index 0000000..416e149 --- /dev/null +++ b/test/entrypoint-webpack-preload/webpack-base-config.ts @@ -0,0 +1,44 @@ +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import webpack, { WebpackPluginInstance } from 'webpack'; +import path from 'path'; +import HtmlWebpackInjectPreload from '../../src/main'; +export function getWebpackConfig(fixtureEntryPath: string) { + const fixtureOutputPath = 'dist/'; + const entry = path.join(require.main?.path ?? __dirname, fixtureEntryPath); + const config: webpack.Configuration = { + mode: 'production', + entry, + module: { + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + { + test: /\.(woff|woff2)$/, + use: [ + { + loader: 'url-loader', + options: { + name: '[name].[ext]', + limit: 8192, + }, + }, + ], + }, + ], + }, + output: { + path: path.join(require.main?.path ?? __dirname, fixtureOutputPath), + publicPath: '', + }, + plugins: [ + new MiniCssExtractPlugin() as WebpackPluginInstance, + new HtmlWebpackPlugin(), + new HtmlWebpackInjectPreload({ entryPointWebpackPreload: true }), + ], + }; + + return config +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index eb9a8ff..e5015e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,5 @@ ], "declaration": true, }, - "exclude": ["node_modules", "**/*.test.ts", "lib"], + "exclude": ["node_modules", "test", "lib"], } \ No newline at end of file