Skip to content

Commit

Permalink
chore: add workflow scheme
Browse files Browse the repository at this point in the history
  • Loading branch information
webdiscus committed Jun 28, 2023
1 parent 09fa73f commit 307d686
Show file tree
Hide file tree
Showing 24 changed files with 305 additions and 117 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
## HTML as entrypoint

The plugin supports HTML templates as entrypoints.\
In HTML templates can be referenced any resources such as JS, SCSS, images and other assets.\
In HTML templates can be referenced any resources such as JS, SCSS, images and other assets, similar to how it works in Vite.\
For example:
- `<link href="@images/favicon.png" type="image/png" rel=icon />`
- `<link href="./style.scss" rel="stylesheet">`
Expand All @@ -27,9 +27,11 @@ For example:

Note: `@images` is the Webpack alias to a source images directory.

The source files of assets referenced in HTML are processed and extracted to the output directory.
In the generated HTML and CSS, the plugin substitutes the output filenames of the processed resources.

The plugin detects all source files referenced in HTML and extracts processed assets to the output directory.
In the generated HTML and CSS, the plugin substitutes the source filenames with the output filenames.

<img width="830" style="max-width: 100%;" src="https://raw.githubusercontent.com/webdiscus/html-bundler-webpack-plugin/devel/images/workflow.png">

### 💡 Highlights

Expand All @@ -41,6 +43,7 @@ In the generated HTML and CSS, the plugin substitutes the output filenames of th
- Dynamically loading template variables after changes using the [data](#loader-option-data) option.
- Auto generation of `<link rel="preload">` to [preload](#option-preload) fonts, images, video, scripts, styles, etc.


### ✅ Profit

You can specify the script and style source files directly in an HTML template,
Expand Down
Binary file added images/workflow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 42 additions & 13 deletions src/Plugin/AssetCompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,12 @@ class AssetCompiler {
});

// entry options
compiler.hooks.entryOption.tap(pluginName, this.afterProcessEntry);
if (Options.isDynamicEntry()) {
AssetEntry.initDynamicEntry();
} else {
AssetEntry.initEntry();
compiler.hooks.entryOption.tap(pluginName, this.afterProcessEntry);
}

// this compilation
compiler.hooks.thisCompilation.tap(pluginName, (compilation, { normalModuleFactory, contextModuleFactory }) => {
Expand All @@ -186,6 +191,8 @@ class AssetCompiler {

this.compilation = compilation;

AssetEntry.init({ compilation, entryLibrary: this.entryLibrary });

Resolver.init({
fs,
rootContext: Options.rootContext,
Expand All @@ -196,7 +203,6 @@ class AssetCompiler {
moduleGraph: compilation.moduleGraph,
});

AssetEntry.setCompilation(compilation);
Collection.setCompilation(compilation);

// resolve modules
Expand All @@ -216,25 +222,30 @@ class AssetCompiler {
// normalModuleHooks.beforeParse.tap(pluginName, (module) => {});
// normalModuleHooks.beforeSnapshot.tap(pluginName, (module) => {});

// TODO: implement real content hash for css
// compilation.hooks.contentHash.tap(pluginName, (chunk) => {});

// render source code of modules
compilation.hooks.renderManifest.tap(pluginName, this.renderManifest);

// after render module's sources
// note: only here is possible to modify an asset content via async function;
// `Infinity` ensures that the processAssets will only run after all other taps
// note:
// - only here is possible to modify an asset content via async function
// - `Infinity` ensures that the process will be run after all optimizations
compilation.hooks.processAssets.tapPromise({ name: pluginName, stage: Infinity }, (assets) => {
// minify html before injecting inlined js and css to avoid:
// - needles minification already minified assets in production mode
// - issues by parsing the inlined js/css code with the html minification module
const result = this.afterRenderModules(compilation);

// TODO: calc real content hash for js and css
// if (Options.isRealContentHash()) {
// console.log('*** afterProcessAssets: ', { assets: Object.keys(assets) });
// }

return Promise.resolve(result);
});

// postprocess for the content of assets
compilation.hooks.afterProcessAssets.tap(pluginName, (assets) => {
// note: this hook not provides testable exceptions,
// therefore, we save an exception to throw it in the done hook
// this hook doesn't provide testable exceptions, therefore, save an exception to throw it in the done hook
try {
this.afterProcessAssets(assets);
} catch (error) {
Expand All @@ -255,7 +266,7 @@ class AssetCompiler {
* @param {Object<name:string, entry: Object>} entries The webpack entries.
*/
afterProcessEntry(context, entries) {
AssetEntry.addEntries(entries, { entryLibrary: this.entryLibrary });
AssetEntry.addEntries(entries);
}

/**
Expand Down Expand Up @@ -498,6 +509,8 @@ class AssetCompiler {
if (assetModule === false) return;
if (assetModule == null) continue;

//console.log('\n++ assetModule: ', { filename: assetModule.filename }, assetModule);

assetModules.add(assetModule);
} else if (module.type === 'asset/resource') {
// resource required in the template or in the CSS via url()
Expand All @@ -523,6 +536,8 @@ class AssetCompiler {
fileManifest.filename = module.assetFile;
fileManifest.render = () => new RawSource(content);
result.push(fileManifest);

//console.log('\n++ fileManifest: ', { filename: fileManifest.filename }, fileManifest);
}

// 2. render styles imported in JavaScript
Expand Down Expand Up @@ -570,25 +585,31 @@ class AssetCompiler {
};

if (sourceFile === entry.sourceFile) {
let assetFile = AssetEntry.getFilename(entry);
// note: the entry can be not a template file, e.g., a style or script defined directly in entry
if (entry.isTemplate) {
this.currentEntryPoint = entry;
module._isTemplate = true;
assetModule.type = Collection.type.template;

console.log('\n++ createAssetModule: ', { filename: entry.filename, assetFile }, entry);

// save the template request with the query, because it can be resolved with different output paths:
// - 'index': './index.ext' => dist/index.html
// - 'index/de': './index.ext?lang=de' => dist/de/index.html
Asset.add(resource, entry.filename);
//Asset.add(resource, entry.filename);
Asset.add(resource, assetFile);
} else if (Options.isStyle(sourceFile)) {
assetModule.type = Collection.type.style;
} else {
// skip unsupported entry type
return;
}

assetModule.outputPath = entry.outputPath;
assetModule.filename = entry.filenameTemplate;
assetModule.assetFile = entry.filename;
//assetModule.assetFile = entry.filename;
assetModule.assetFile = assetFile;
assetModule.fileManifest.identifier = `${pluginName}.${chunk.id}`;
assetModule.fileManifest.hash = chunk.contentHash['javascript'];

Expand Down Expand Up @@ -804,7 +825,10 @@ class AssetCompiler {

/**
* Called after the processAssets hook had finished without error.
* @note: Only at this stage the js file has the final hashed name.
*
* @note:
* This stage has the final hashed output js filename.
* This is the last stage where is able to modify compiled assets.
*
* @param {Object} assets
*/
Expand Down Expand Up @@ -838,6 +862,11 @@ class AssetCompiler {

Collection.render(compilation, callback);

// if (Options.isRealContentHash()) {
// // TODO: calc real content hash for js and css
// console.log('*** afterProcessAssets: ', { assets: Object.keys(assets) });
// }

// remove all unused assets from compilation
AssetTrash.clearCompilation(compilation);
}
Expand Down
114 changes: 111 additions & 3 deletions src/Plugin/AssetEntry.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const fs = require('fs');
const path = require('path');
const Options = require('./Options');
const PluginService = require('./PluginService');
const { isFunction } = require('../Common/Helpers');
const { readDirRecursiveSync } = require('../Common/FileUtils');
const { optionEntryPathException } = require('./Messages/Exception');

/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack").Module} Module */
Expand Down Expand Up @@ -54,10 +57,84 @@ class AssetEntry {
/** @type {Compilation} */
static compilation = null;

/** @type {Object} */
static entryLibrary = null;

// the id to bind loader with data passed into template via entry.data
static idIndex = 1;
static data = new Map();

static init({ compilation, entryLibrary }) {
this.compilation = compilation;
this.entryLibrary = entryLibrary;
}

static initEntry() {
const { entry } = Options.webpackOptions;
const pluginEntry = Options.options.entry;

for (const key in pluginEntry) {
const entry = pluginEntry[key];

if (entry.import == null) {
pluginEntry[key] = { import: [entry] };
continue;
}

if (!Array.isArray(entry.import)) {
entry.import = [entry.import];
}
}

Options.webpackOptions.entry = { ...entry, ...pluginEntry };
}

static initDynamicEntry() {
const { entry } = Options.webpackOptions;
const dir = Options.options.entry;

Options.webpackOptions.entry = () => {
const dynEntry = this.getDynamicEntry(dir);
const entries = { ...entry, ...dynEntry };

this.addEntries(entries);

console.log('\n++ DYN ENTRY: ', entries);

return entries;
};
}

/**
* Returns dynamic entries read recursively from the entry path.
*
* @param {string} dir The entry path.
* @return {Object}
* @throws
*/
static getDynamicEntry(dir) {
if (!path.isAbsolute(dir)) {
dir = path.join(Options.rootContext, dir);
}

try {
if (!fs.lstatSync(dir).isDirectory()) optionEntryPathException(dir);
} catch (error) {
optionEntryPathException(dir);
}

const entry = {};
const files = readDirRecursiveSync(dir, { fs, includes: [Options.options.test] });

files.forEach((file) => {
let outputFile = path.relative(dir, file);
let key = outputFile.slice(0, outputFile.lastIndexOf('.'));
entry[key] = { import: [file] };
});

return entry;
}

/**
* Whether the entry is unique.
*
Expand Down Expand Up @@ -145,6 +222,19 @@ class AssetEntry {
return false;
}

/**
* @param {AssetEntryOptions} entry
*/
static getFilename(entry) {
// TODO: test
// let filename = entry.filename;
// if (filename != null) return filename;
// this.applyTemplateFilename(entry);
// console.log('\n=== getFilename: ', { filename }, entry);

return entry.filename;
}

/**
* Set generated output filename for the asset defined as entrypoint.
*
Expand All @@ -159,6 +249,9 @@ class AssetEntry {
}
}

/**
* @param {AssetEntryOptions} entry
*/
static applyTemplateFilename(entry) {
if (entry.isTemplate) {
entry.filename = this.compilation.getAssetPath(entry.filenameTemplate, {
Expand All @@ -182,9 +275,19 @@ class AssetEntry {

/**
* @param {Array<Object>} entries
* @param {Object} entryLibrary
*/
static addEntries(entries, { entryLibrary }) {
static addEntries(entries) {
// let isDynEntry = false;
// if (typeof entries === 'function') {
// entries = entries();
// console.log('\n++++++++++++++++++++ add DYNAMIC Entries: ', entries);
// isDynEntry = true;
//
// //if (!dynamicEntry) return;
// }

console.log('\n++ addEntries: ', entries);

for (let name in entries) {
const entry = entries[name];
const importFile = entry.import[0];
Expand All @@ -199,7 +302,7 @@ class AssetEntry {
const id = this.idIndex++;
let { verbose, filename: filenameTemplate, sourcePath, outputPath } = options;

if (!entry.library) entry.library = entryLibrary;
if (!entry.library) entry.library = this.entryLibrary;
if (entry.filename) filenameTemplate = entry.filename;
if (entry.data) this.data.set(id, entry.data);

Expand Down Expand Up @@ -258,6 +361,8 @@ class AssetEntry {
}

entry.filename = (pathData, assetInfo) => {
console.log('** ENTRY filename: ', { filenameTemplate }, assetEntryOptions);

if (assetEntryOptions.filename != null) return assetEntryOptions.filename;

// the `filename` property of the `PathData` type should be a source file, but in entry this property not exists
Expand All @@ -274,6 +379,7 @@ class AssetEntry {
};

assetEntryOptions.originalEntry = entry;
//console.log('++ ADD ENTRY: ', assetEntryOptions);

this.entries.set(name, assetEntryOptions);
this.entriesByResource.set(assetEntryOptions.id, assetEntryOptions);
Expand Down Expand Up @@ -302,6 +408,8 @@ class AssetEntry {

const issuerEntry = [...this.entries.values()].find(({ resource }) => resource === issuer);

//console.log('++ addToCompilation: ', this.entries);

const entry = {
name,
runtime: undefined,
Expand Down
4 changes: 4 additions & 0 deletions src/Plugin/Collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,10 +785,14 @@ class Collection {
* Called before each new compilation after changes, in the serve/watch mode.
*/
static reset() {
if (Options.isDynamicEntry()) return;

this.index = {};
this.files.forEach((item, key) => {
if (item.assets != null) item.assets.length = 0;
});

// TODO: non't remove if entry is dynamic
this.data.clear();
this.importStyleRootIssuers.clear();
this.importStyleSources.clear();
Expand Down
Loading

0 comments on commit 307d686

Please sign in to comment.