Skip to content

Commit

Permalink
refactor: merge HtmlTagsPlugin into the basic one (#1858)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Mar 18, 2024
1 parent 7d3db50 commit b6860a0
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 288 deletions.
1 change: 0 additions & 1 deletion e2e/cases/html-tags/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from 'node:path';
import { expect, test } from '@playwright/test';
import { build } from '@e2e/helper';

Expand Down
49 changes: 19 additions & 30 deletions packages/core/src/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import type {
NormalizedConfig,
HTMLPluginOptions,
} from '@rsbuild/shared';
import type { HtmlTagsPluginOptions } from '../rspack/HtmlTagsPlugin';
import type { HtmlInfo } from '../rspack/HtmlBasicPlugin';
import type { HtmlInfo, TagConfig } from '../rspack/HtmlBasicPlugin';
import type { RsbuildPlugin } from '../types';

export function getTitle(entryName: string, config: NormalizedConfig) {
Expand Down Expand Up @@ -172,34 +171,21 @@ function getChunks(
return [entryName];
}

export const applyInjectTags = (api: RsbuildPluginAPI) => {
api.modifyBundlerChain(async (chain, { CHAIN_ID }) => {
const config = api.getNormalizedConfig();
const tags = castArray(config.html.tags).filter(Boolean);
const getTagConfig = (api: RsbuildPluginAPI): TagConfig | undefined => {
const config = api.getNormalizedConfig();
const tags = castArray(config.html.tags).filter(Boolean);

// skip if options is empty.
if (!tags.length) {
return;
}

const { HtmlTagsPlugin } = await import('../rspack/HtmlTagsPlugin');

// create shared options used for entry without specified options.
const sharedOptions: HtmlTagsPluginOptions = {
append: true,
hash: false,
publicPath: true,
tags,
};
// skip if options is empty.
if (!tags.length) {
return undefined;
}

// apply webpack plugin for each entries.
for (const [entry, filename] of Object.entries(api.getHTMLPaths())) {
const opts = { ...sharedOptions, includes: [filename] };
chain
.plugin(`${CHAIN_ID.PLUGIN.HTML_TAGS}#${entry}`)
.use(HtmlTagsPlugin, [opts]);
}
});
return {
append: true,
hash: false,
publicPath: true,
tags,
};
};

export const pluginHtml = (): RsbuildPlugin => ({
Expand Down Expand Up @@ -264,6 +250,11 @@ export const pluginHtml = (): RsbuildPlugin => ({
htmlInfo.templateContent = templateContent;
}

const tagConfig = getTagConfig(api);
if (tagConfig) {
htmlInfo.tagConfig = tagConfig;
}

pluginOptions.title = getTitle(entryName, config);

const favicon = getFavicon(entryName, config);
Expand Down Expand Up @@ -351,7 +342,5 @@ export const pluginHtml = (): RsbuildPlugin => ({
}
},
);

applyInjectTags(api);
},
});
203 changes: 202 additions & 1 deletion packages/core/src/rspack/HtmlBasicPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,214 @@
import type HtmlWebpackPlugin from 'html-webpack-plugin';
import type { Compiler, Compilation } from '@rspack/core';
import {
partition,
isFunction,
withPublicPath,
type HtmlInjectTag,
type HtmlInjectTagHandler,
type HtmlInjectTagDescriptor,
type HtmlInjectTagUtils,
} from '@rsbuild/shared';
import { getHTMLPlugin } from '../provider/htmlPluginUtil';

export interface TagConfig {
hash?: HtmlInjectTag['hash'];
publicPath?: HtmlInjectTag['publicPath'];
append?: HtmlInjectTag['append'];
includes?: string[];
tags?: HtmlInjectTagDescriptor[];
}

/** @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Void_element} */
export const VOID_TAGS = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];

/** @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also} */
export const HEAD_TAGS = [
'title',
'base',
'link',
'style',
'meta',
'script',
'noscript',
'template',
];

export const FILE_ATTRS = {
link: 'href',
script: 'src',
};

const withHash = (url: string, hash: string) => `${url}?${hash}`;

export type HtmlInfo = {
favicon?: string;
tagConfig?: TagConfig;
templateContent?: string;
};

export type HtmlBasicPluginOptions = {
info: Record<string, HtmlInfo>;
};

export type AlterAssetTagGroupsData = {
headTags: HtmlWebpackPlugin.HtmlTagObject[];
bodyTags: HtmlWebpackPlugin.HtmlTagObject[];
outputName: string;
publicPath: string;
plugin: HtmlWebpackPlugin;
};

export const hasTitle = (html?: string): boolean =>
html ? /<title/i.test(html) && /<\/title/i.test(html) : false;

const modifyTags = (
data: AlterAssetTagGroupsData,
tagConfig: TagConfig,
compilationHash: string,
) => {
// skip unmatched file and empty tag list.
const includesCurrentFile =
!tagConfig.includes || tagConfig.includes.includes(data.outputName);

if (!includesCurrentFile || !tagConfig.tags?.length) {
return data;
}

// convert tags between `HtmlInjectTag` and `HtmlWebpackPlugin.HtmlTagObject`.
const fromWebpackTags = (
tags: HtmlWebpackPlugin.HtmlTagObject[],
override?: Partial<HtmlInjectTag>,
) => {
const ret: HtmlInjectTag[] = [];
for (const tag of tags) {
ret.push({
tag: tag.tagName,
attrs: tag.attributes,
children: tag.innerHTML,
publicPath: false,
...override,
});
}
return ret;
};

const fromInjectTags = (tags: HtmlInjectTag[]) => {
const ret: HtmlWebpackPlugin.HtmlTagObject[] = [];

for (const tag of tags) {
// apply publicPath and hash to filename attr.
const attrs = { ...tag.attrs };
const filenameTag = FILE_ATTRS[tag.tag as keyof typeof FILE_ATTRS];
let filename = attrs[filenameTag];

if (typeof filename === 'string') {
const optPublicPath = tag.publicPath ?? tagConfig.publicPath;

if (typeof optPublicPath === 'function') {
filename = optPublicPath(filename, data.publicPath);
} else if (typeof optPublicPath === 'string') {
filename = withPublicPath(filename, optPublicPath);
} else if (optPublicPath !== false) {
filename = withPublicPath(filename, data.publicPath);
}

const optHash = tag.hash ?? tagConfig.hash;

if (typeof optHash === 'function') {
if (compilationHash.length) {
filename = optHash(filename, compilationHash);
}
} else if (typeof optHash === 'string') {
if (optHash.length) {
filename = withHash(filename, optHash);
}
} else if (optHash === true) {
if (compilationHash.length) {
filename = withHash(filename, compilationHash);
}
}

attrs[filenameTag] = filename;
}

ret.push({
meta: {},
tagName: tag.tag,
attributes: attrs,
voidTag: VOID_TAGS.includes(tag.tag),
innerHTML: tag.children,
});
}
return ret;
};

// create tag list from html-webpack-plugin and options.
const handlers: HtmlInjectTagHandler[] = [];
let tags = [
...fromWebpackTags(data.headTags, { head: true }),
...fromWebpackTags(data.bodyTags, { head: false }),
];

for (const tag of tagConfig.tags) {
if (isFunction(tag)) {
handlers.push(tag);
} else {
tags.push(tag);
}
}

const getPriority = (tag: HtmlInjectTag) => {
const head = tag.head ?? HEAD_TAGS.includes(tag.tag);
let priority = head ? -2 : 2;

const append = tag.append ?? tagConfig.append;
if (typeof append === 'boolean') {
priority += append ? 1 : -1;
}

return priority;
};

// apply tag handler callbacks.
tags = tags.sort((tag1, tag2) => getPriority(tag1) - getPriority(tag2));

const utils: HtmlInjectTagUtils = {
outputName: data.outputName,
publicPath: data.publicPath,
hash: compilationHash,
};
for (const handler of handlers) {
tags = handler(tags, utils) || tags;
}

// apply to html-webpack-plugin.
const [headTags, bodyTags] = partition(
tags,
(tag) => tag.head ?? HEAD_TAGS.includes(tag.tag),
);
data.headTags = fromInjectTags(headTags);
data.bodyTags = fromInjectTags(bodyTags);

return data;
};

export class HtmlBasicPlugin {
readonly name: string;

Expand Down Expand Up @@ -57,6 +252,8 @@ export class HtmlBasicPlugin {
};

compiler.hooks.compilation.tap(this.name, (compilation: Compilation) => {
const compilationHash = compilation.hash || '';

getHTMLPlugin()
.getHooks(compilation)
.alterAssetTagGroups.tap(this.name, (data) => {
Expand All @@ -67,12 +264,16 @@ export class HtmlBasicPlugin {
}

const { headTags } = data;
const { templateContent } = this.options.info[entryName];
const { tagConfig, templateContent } = this.options.info[entryName];

if (!hasTitle(templateContent)) {
addTitleTag(headTags, data.plugin.options?.title);
}

if (tagConfig) {
modifyTags(data, tagConfig, compilationHash);
}

addFavicon(headTags, entryName);
return data;
});
Expand Down
Loading

0 comments on commit b6860a0

Please sign in to comment.