Skip to content

Commit

Permalink
feat: add js.inline.keepAttributes option to allow keep some origin…
Browse files Browse the repository at this point in the history
…al script tag attributes when JS is inlined
  • Loading branch information
webdiscus committed Nov 7, 2023
1 parent 3ab957e commit 3559ea0
Show file tree
Hide file tree
Showing 29 changed files with 415 additions and 105 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change log

## 2.16.0-beta.1 (2023-10-23)

- feat: add `js.inline.keepAttributes` option to allow keep some original script tag attributes when JS is inlined
- feat(EXPERIMENTAL): add `afterEmit` callback, undocumented
- fix(EXPERIMENTAL, BREAKING CHANGE): rename `lazy` query to `url` to get output URL of CSS
- chore: add usage example of image-minimizer-webpack-plugin

## 2.16.0-beta.0 (2023-10-21)

- feat(EXPERIMENTAL): add possibility to get output CSS filename in JS via import a style file with the `lazy` query.\
Expand Down
100 changes: 80 additions & 20 deletions src/Common/HtmlParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

const textEncoder = new TextEncoder();
const spaceCodes = textEncoder.encode(' \n\r\t\f');
const tagEndCodes = textEncoder.encode(' \n\r\t\f/>');
const valueSeparatorCodes = textEncoder.encode('= \n\r\t\f');
const equalCode = '='.charCodeAt(0);
const quotCodes = textEncoder.encode(`"'`);

const comparePos = (a, b) => a.startPos - b.startPos;

Expand Down Expand Up @@ -36,6 +40,37 @@ const indexOfNonSpace = (content, startPos = 0) => {
return startPos;
};

/**
* Returns the first index of an attribute/value separator char.
* The char can be one of: =, space, tab, new line, carrier return, page break.
*
* @param {string} content
* @param {number} startPos
* @return {number}
*/
const indexOfValueSeparator = (content, startPos = 0) => {
const len = content.length;
for (; valueSeparatorCodes.indexOf(content.charCodeAt(startPos)) < 0 && startPos < len; startPos++) {}

return startPos < len ? startPos : -1;
};

/**
* Return end position of the last attribute in a tag.
*
* @param {string} content The tag content.
* @param {number} startAttrsPos The start position of first attribute.
* @return {number}
*/
const indexOfAttributesEnd = (content, startAttrsPos) => {
const len = content.length;
let pos = content.length - 1;

for (; tagEndCodes.indexOf(content.charCodeAt(pos)) >= 0 && pos > 0; pos--) {}

return pos < len && pos > startAttrsPos ? pos : -1;
};

/**
* Returns the first index of the searched char if none of the excluded chars was found before it.
*
Expand Down Expand Up @@ -191,26 +226,51 @@ class HtmlParser {
*/
static parseAttrAll(content) {
let attrs = {};
let pos = 1;
let spacePos, attrStartPos, attrEndPos, valueStartPos, valueEndPos, attr;

while (true) {
spacePos = indexOfSpace(content, pos);
// when no space between attributes
if (spacePos < 0) spacePos = pos;
attrStartPos = indexOfNonSpace(content, spacePos);
attrEndPos = content.indexOf('="', attrStartPos);
// no more attributes with value
if (attrEndPos < 0) break;

valueStartPos = attrEndPos + 2;
valueEndPos = content.indexOf('"', valueStartPos);
// not closed quote
if (valueEndPos < 0) break;

attr = content.slice(attrStartPos, attrEndPos);
attrs[attr] = content.slice(valueStartPos, valueEndPos);
pos = valueEndPos + 1;
let pos = indexOfSpace(content, 1);

// no attributes
if (pos < 0) return attrs;

let attrsEndPos = indexOfAttributesEnd(content, pos);

// no attributes
if (attrsEndPos < 0) return attrs;

while (pos < attrsEndPos) {
let attrStartPos = indexOfNonSpace(content, pos);
pos = indexOfValueSeparator(content, attrStartPos + 1);
let hasValue = pos >= 0 && content.charCodeAt(pos) === equalCode;

if (pos < 0) pos = attrsEndPos + 1;

let attr = content.slice(attrStartPos, pos);
let value = undefined;

if (hasValue) {
// TODO: allow a space around equal, e.g., `attr = value`
let valueStartPos = pos + 1;
let valueEndPos = -1;
let nextCharCode = content.charCodeAt(valueStartPos);

if (quotCodes.indexOf(nextCharCode) >= 0) {
// value with quotes
valueEndPos = content.indexOf(String.fromCharCode([nextCharCode]), ++valueStartPos);
if (valueEndPos > -1) pos = valueEndPos + 1;
} else {
// value w/o quotes
valueEndPos = indexOfSpace(content, valueStartPos);
if (valueEndPos > -1) pos = valueEndPos;
}

if (valueEndPos < 0) {
valueEndPos = attrsEndPos + 1;
pos = valueEndPos;
}

value = content.slice(valueStartPos, valueEndPos);
}

attrs[attr] = value;
}

return attrs;
Expand Down
6 changes: 3 additions & 3 deletions src/Loader/cssLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const pitchLoader = async function (remaining) {
// TODO: find the module from this._compilation, because this._module is deprecated
const { resource, resourcePath, _module: module } = this;
const options = this.getOptions() || {};
const isLazy = module.resourceResolveData?.query.includes('lazy');
const isUrl = module.resourceResolveData?.query.includes('url');

remaining += resource.includes('?') ? '&' : '?';

Expand All @@ -44,8 +44,8 @@ const pitchLoader = async function (remaining) {
module._cssSource = esModule ? result.default : result;
Collection.setImportStyleEsModule(esModule);

// support for lazy load CSS in JavaScript, see js-import-css-lazy
return '/* extracted by HTMLBundler CSSLoader */' + (isLazy ? module._cssSource : '');
// support for lazy load CSS in JavaScript, see js-import-css-lazy-url
return '/* extracted by HTMLBundler CSSLoader */' + (isUrl ? module._cssSource : '');
};

module.exports = loader;
Expand Down
84 changes: 46 additions & 38 deletions src/Plugin/AssetCompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,17 +309,17 @@ class AssetCompiler {
contextModuleFactory.hooks.alternativeRequests.tap(pluginName, this.filterAlternativeRequests);

// TODO: reserved for next major version supported Webpack 5.81+
// - add support for lazy load CSS in JavaScript, see js-import-css-lazy
// - add support for lazy load CSS in JavaScript, see js-import-css-lazy-url
// - move the code from afterCreateModule to here
// - replace the 'javascript/auto' with constant from webpack
// normalModuleFactory.hooks.createModuleClass.for('javascript/auto').tap(pluginName, (createData, resolveData) => {
// const {
// _bundlerPluginMeta: { isImportedStyle },
// } = resolveData;
// const query = createData.resourceResolveData?.query || '';
// const isLazy = query.includes('lazy');
// const isUrl = query.includes('url');
//
// if (isImportedStyle && isLazy && !query.includes(cssLoaderName)) {
// if (isImportedStyle && isUrl && !query.includes(cssLoaderName)) {
// const dataUrlOptions = undefined;
// const filename = Options.getCss().filename;
// createData.settings.type = 'asset/resource';
Expand Down Expand Up @@ -397,29 +397,35 @@ class AssetCompiler {
result = hooks.integrityHashes.promise(hashes);
}

result.then(() => {
const entries = [];
const entries = [];

/** @type {CompileOptions} */
const options = {
verbose: Options.isVerbose(),
outputPath: Options.getWebpackOutputPath(),
};
/** @type {CompileOptions} */
const options = {
verbose: Options.isVerbose(),
outputPath: Options.getWebpackOutputPath(),
};

// prepare the CompileEntries for the hook
for (const [, { entry, assets }] of Collection.data) {
/** @type {CompileEntry} */
const compileEntry = {
name: entry.originalName,
resource: entry.resource,
assetFile: entry.filename,
assets,
};
entries.push(compileEntry);
}
result
.then(() => {
// prepare the CompileEntries for the hook
for (const [, { entry, assets }] of Collection.data) {
/** @type {CompileEntry} */
const compileEntry = {
name: entry.originalName,
resource: entry.resource,
assetFile: entry.filename,
assets,
};
entries.push(compileEntry);
}

hooks.afterEmit.promise(entries, options);
});
return hooks.afterEmit.promise(entries, options);
})
.then(() => {
if (Options.hasAfterEmit()) {
Options.afterEmit(entries, options, this.compilation);
}
});

this.promises.push(result);

Expand Down Expand Up @@ -680,10 +686,10 @@ class AssetCompiler {
const { _bundlerPluginMeta: meta } = resolveData;
const { rawRequest, resource } = createData;
const query = module.resourceResolveData?.query || '';
const isLazy = query.includes('lazy');
const isUrl = query.includes('url');

// TODO: undocumented experimental support for lazy load a source style file (with `?lazy` query) in JavaScript
if (meta.isImportedStyle && isLazy && !query.includes(cssLoaderName)) {
// TODO: undocumented experimental support for url load a source style file (with `?url` query) in JavaScript
if (meta.isImportedStyle && isUrl && !query.includes(cssLoaderName)) {
const dataUrlOptions = undefined;
const filename = Options.getCss().filename;

Expand Down Expand Up @@ -882,9 +888,9 @@ class AssetCompiler {
}

/**
* @param {Object} entry The entry point of the chunk.
* @param {Object} chunk The chunk of an asset.
* @param {Object} module The module of the chunk.
* @param {AssetEntryOptions} entry The entry point of the chunk.
* @param {Chunk} chunk The chunk of an asset.
* @param {Module} module The module of the chunk.
* @return {Object|null|boolean} assetModule Returns the asset module object.
* If returns undefined, then skip processing of the module.
* If returns null, then break the hook processing to show the original error, occurs by an inner error.
Expand Down Expand Up @@ -938,6 +944,7 @@ class AssetCompiler {
return;
}

assetModule.name = entry.originalName;
assetModule.outputPath = entry.outputPath;
assetModule.filename = entry.filenameTemplate;
assetModule.assetFile = assetFile;
Expand Down Expand Up @@ -1020,7 +1027,7 @@ class AssetCompiler {

// 2. squash styles from all nested files into one file
for (const module of modules) {
const isLazy = module.resourceResolveData?.query.includes('lazy');
const isUrl = module.resourceResolveData?.query.includes('url');
const importData = {
resource: module.resource,
assets: [],
Expand Down Expand Up @@ -1048,16 +1055,16 @@ class AssetCompiler {
}
}

if (isLazy) {
// TODO: add support for lazy load CSS in JavaScript, see js-import-css-lazy
if (isUrl) {
// TODO: add support for lazy load CSS in JavaScript, see js-import-css-lazy-url
Collection.setData(
entry,
{ resource: issuer },
{
type: Collection.type.style,
// lazy file can't be inlined, it makes no sense
inline: false,
lazy: true,
lazyUrl: true,
resource: module.resource,
assetFile: module.buildInfo.filename,
}
Expand Down Expand Up @@ -1228,7 +1235,7 @@ class AssetCompiler {
* Render the module source code generated by a loader.
*
* @param {string} type The type of module, one of the values: template, style.
* @param {boolean} inline Whether the resource should be inlined.
* @param {string?} name The entry name. Used for template entry only.
* @param {Object} source The Webpack source.
* @param {string} resource The full path of source file, including a query.
* @param {string} sourceFile The full path of source file w/o a query.
Expand All @@ -1237,7 +1244,7 @@ class AssetCompiler {
* @param {string|function} filename The filename template.
* @return {string|null} Return rendered HTML or null to not save the rendered content.
*/
renderModule({ type, inline, source, sourceFile, resource, assetFile, outputPath, filename }) {
renderModule({ type, name, source, sourceFile, resource, assetFile, outputPath, filename }) {
/** @type FileInfo */
const issuer = {
resource,
Expand Down Expand Up @@ -1266,13 +1273,14 @@ class AssetCompiler {
if (Options.hasPostprocess()) {
/** @type {TemplateInfo} */
const templateInfo = {
verbose: Options.isVerbose(),
// TODO: deprecate the filename and sourceFile properties
name,
// TODO: deprecate the filename property
filename,
sourceFile,
outputPath,
sourceFile,
resource,
assetFile,
verbose: Options.isVerbose(),
};

// TODO:
Expand Down
26 changes: 24 additions & 2 deletions src/Plugin/Collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,11 @@ class Collection {
let pos = content.indexOf(resource);
if (pos < 0) return false;

const { keepAttributes } = Options.getJs().inline;

const sources = this.compilation.assets;
const { chunks } = asset;
const openTag = '<script>';
let openTag = '<script>';
const closeTag = '</script>';

let srcStartPos = pos;
Expand All @@ -188,6 +190,7 @@ class Collection {
}

while (tagStartPos >= 0 && content.charAt(--tagStartPos) !== '<') {}
let tagEndPos2 = content.indexOf(closeTag, tagEndPos);
tagEndPos = content.indexOf(closeTag, tagEndPos) + closeTag.length;

let beforeTagSrc = content.slice(tagStartPos, srcStartPos);
Expand All @@ -198,6 +201,25 @@ class Collection {

if (inline) {
const code = sources[chunkFile].source();

if (keepAttributes) {
let tag = content.slice(tagStartPos, tagEndPos2);
const attributes = HtmlParser.parseAttrAll(tag);
let attrsStr = '';

for (const [attribute, value] of Object.entries(attributes)) {
if (keepAttributes({ attributes, attribute, value }) === true) {
if (attrsStr) attrsStr += ' ';
attrsStr += attribute;
if (value != null) attrsStr += `="${value}"`;
}
}

if (attrsStr) {
openTag = `<script ${attrsStr}>`;
}
}

replacement += openTag + code + closeTag;
AssetTrash.add(chunkFile);
} else {
Expand Down Expand Up @@ -307,7 +329,7 @@ class Collection {

/**
* @param {Compilation} compilation
* @param {CompilationHooks} hooks
* @param {HtmlBundlerPlugin.Hooks} hooks
*/
static init(compilation, hooks) {
this.compilation = compilation;
Expand Down
Loading

0 comments on commit 3559ea0

Please sign in to comment.