Skip to content

Commit 7c77d4f

Browse files
committed
fix: watching changes in template function imported in JS
1 parent 06286b5 commit 7c77d4f

12 files changed

Lines changed: 165 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Change log
22

3+
## 3.4.2 (2023-12-08)
4+
5+
- fix: watching changes in template function imported in JS
6+
37
## 3.4.1 (2023-12-08)
48

5-
- fix: runtime error using template function in js when external data is not defined
9+
- fix: runtime error using template function in JS when external data is not defined
610

711
## 3.4.0 (2023-12-03)
812

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "3.4.1",
3+
"version": "3.4.2",
44
"description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.",
55
"keywords": [
66
"html",

src/Common/CompilationHelpers.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Find the root issuer of a resource.
3+
*
4+
* @param {Compilation} compilation
5+
* @param {string} resource
6+
* @return {string|undefined}
7+
*/
8+
const findRootIssuer = (compilation, resource) => {
9+
const moduleMap = compilation.moduleGraph._moduleMap;
10+
const modules = moduleMap.keys();
11+
const [resourceFile] = resource.split('?', 1);
12+
let current;
13+
let parent;
14+
15+
for (let module of modules) {
16+
// skip non normal modules, e.g. runtime
17+
if (!module.resource) continue;
18+
19+
const [file] = module.resource.split('?', 1);
20+
if (file === resourceFile) {
21+
current = module;
22+
break;
23+
}
24+
}
25+
26+
if (current) {
27+
while ((parent = moduleMap.get(current).issuer)) {
28+
current = parent;
29+
}
30+
}
31+
32+
return current && current.resource !== resource ? current.resource : undefined;
33+
};
34+
35+
module.exports = { findRootIssuer };

src/Loader/Option.js

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const Preprocessor = require('./Preprocessor');
33
const PluginService = require('../Plugin/PluginService');
44
const { parseQuery } = require('../Common/Helpers');
55
const { rootSourceDir, filterParentPaths } = require('../Common/FileUtils');
6+
const { findRootIssuer } = require('../Common/CompilationHelpers');
67
const { watchPathsException, dataFileNotFoundException, dataFileException } = require('./Messages/Exeptions');
78

89
/**
@@ -21,6 +22,7 @@ class Option {
2122
static #options;
2223
static #rootContext;
2324
static #resourcePath;
25+
static #pluginOption;
2426

2527
// rule: the first value is default
2628
static preprocessorModes = new Set(['render', 'compile']);
@@ -33,8 +35,9 @@ class Option {
3335
const queryData = parseQuery(resourceQuery);
3436
let options = PluginService.getLoaderCache(loaderId);
3537

36-
this.fileSystem = loaderContext.fs.fileSystem;
38+
this.#pluginOption = PluginService.getOptions();
3739
this.#watch = PluginService.isWatchMode();
40+
this.fileSystem = loaderContext.fs.fileSystem;
3841
this.#webpackOptions = loaderContext._compiler.options || {};
3942
this.#rootContext = rootContext;
4043
this.#resourcePath = resourcePath;
@@ -43,6 +46,9 @@ class Option {
4346
const loaderOptions = PluginService.getLoaderOptions();
4447
options = { ...loaderOptions, ...(loaderContext.getOptions() || {}) };
4548

49+
// save the initial value defined in the webpack config
50+
options.originalPreprocessorMode = options.preprocessorMode;
51+
4652
// the assets root path is used for resolving files specified in attributes (`sources` option)
4753
// allow both 'root' and 'basedir' option name for compatibility
4854
const basedir = options.root || options.basedir || false;
@@ -53,16 +59,57 @@ class Option {
5359

5460
PluginService.setLoaderCache(loaderId, options);
5561
}
56-
5762
this.#options = options;
5863

59-
// preprocessor mode
64+
// if the data option is a string, it must be an absolute or relative filename of an existing file that exports the data
65+
const loaderData = this.#loadData(options.data);
66+
const entryData = this.#loadData(loaderContext.entryData);
67+
const contextData = loaderContext.data || {};
68+
69+
// merge plugin and loader data, the plugin data property overrides the same loader data property
70+
loaderObject.data = { ...contextData, ...loaderData, ...entryData, ...queryData };
71+
72+
// beforePreprocessor
73+
if (typeof options.beforePreprocessor !== 'function') {
74+
options.beforePreprocessor = null;
75+
}
76+
77+
// preprocessor
78+
this.#initPreprocessor(loaderContext, queryData);
79+
80+
// clean loaderContext of artifacts
81+
if (loaderContext.entryData != null) delete loaderContext.entryData;
82+
83+
// defaults, cacheable is true, the loader option is not documented in readme, use it only for debugging
84+
if (loaderContext.cacheable != null) loaderContext.cacheable(options?.cacheable !== false);
85+
86+
if (this.#watch) this.#initWatchFiles();
87+
}
88+
89+
/**
90+
* @param {BundlerPluginLoaderContext} loaderContext The loader context of Webpack.
91+
* @param {Object} queryData The parsed parameters from the url query.
92+
*/
93+
static #initPreprocessor(loaderContext, queryData) {
94+
const pluginOption = this.#pluginOption;
95+
const options = this.#options;
6096
const issuer = loaderContext._module.resourceResolveData?.context?.issuer || '';
6197
let [defaultPreprocessorMode] = this.preprocessorModes;
98+
let isIssuerScript = false;
6299
let preprocessorMode;
63100

101+
if (issuer) {
102+
isIssuerScript = pluginOption.isScript(issuer);
103+
if (!isIssuerScript) {
104+
const rootIssuer = findRootIssuer(PluginService.compilation, issuer);
105+
if (rootIssuer) {
106+
isIssuerScript = pluginOption.isScript(rootIssuer);
107+
}
108+
}
109+
}
110+
64111
// rule: defaults, if issuer is JS, then compile template to the template function
65-
if (issuer && PluginService.getOptions().isScript(issuer)) {
112+
if (isIssuerScript) {
66113
preprocessorMode = defaultPreprocessorMode = 'compile';
67114
}
68115

@@ -75,61 +122,35 @@ class Option {
75122
}
76123
}
77124

125+
// reset the original option value, also no cached state,
126+
// because the loader works in different modes depend on the context
127+
options.preprocessorMode = options.originalPreprocessorMode;
128+
78129
if (preprocessorMode && this.preprocessorModes.has(preprocessorMode)) {
79130
options.preprocessorMode = preprocessorMode;
80131
} else if (!this.preprocessorModes.has(options.preprocessorMode)) {
81132
options.preprocessorMode = defaultPreprocessorMode;
82133
}
83134

84-
// if the data option is a string, it must be an absolute or relative filename of an existing file that exports the data
85-
const loaderData = this.loadData(options.data);
86-
const entryData = this.loadData(loaderContext.entryData);
87-
const contextData = loaderContext.data || {};
88-
89-
//console.log('*** INIT DATA1: ', { data: loaderContext.data });
90-
91-
// merge plugin and loader data, the plugin data property overrides the same loader data property
92-
const data = { ...contextData, ...loaderData, ...entryData, ...queryData };
93-
if (Object.keys(data).length > 0) loaderObject.data = data;
94-
95-
loaderObject.data = data;
96-
97-
//if (!loaderObject.data) loaderObject.data = {};
98-
99-
//console.log('*** INIT DATA2: ', { data: loaderContext.data });
100-
101-
// beforePreprocessor
102-
if (typeof options.beforePreprocessor !== 'function') {
103-
options.beforePreprocessor = null;
104-
}
105-
106-
// preprocessor
107135
if (!Preprocessor.isUsed(options.preprocessor)) {
108136
options.preprocessor = Preprocessor.factory(loaderContext, {
109137
preprocessor: options.preprocessor,
110-
watch: this.#watch,
111138
options: options.preprocessorOptions,
139+
watch: this.#watch,
112140
});
113141
}
114-
115-
// clean loaderContext of artifacts
116-
if (loaderContext.entryData != null) delete loaderContext.entryData;
117-
118-
// defaults, cacheable is true, the loader option is not documented in readme, use it only for debugging
119-
if (loaderContext.cacheable != null) loaderContext.cacheable(options?.cacheable !== false);
120-
121-
if (this.#watch) this.#initWatchFiles();
122142
}
123143

124144
static #initWatchFiles() {
145+
const pluginOption = this.#pluginOption;
125146
const watchFiles = {
126147
// watch files only in the directories;
127148
// defaults is first-level subdirectory of a template, relative to root context
128149
paths: [],
129150

130151
// watch only files matched to RegExps,
131152
// if empty then watch all files, except ignored
132-
files: PluginService.getOptions().getEntryTest(),
153+
files: pluginOption.getEntryTest(),
133154

134155
// ignore paths and files matched to RegExps
135156
ignore: [
@@ -142,7 +163,7 @@ class Option {
142163
};
143164

144165
const fs = this.fileSystem;
145-
const { paths, files, ignore } = PluginService.getOptions().getWatchFiles();
166+
const { paths, files, ignore } = pluginOption.getWatchFiles();
146167
const watchDirs = new Set([rootSourceDir(this.#rootContext, this.#resourcePath)]);
147168
const rootContext = this.#rootContext;
148169

@@ -195,7 +216,7 @@ class Option {
195216
* @param {Object|string|null} dataValue If string, the relative or absolute filename.
196217
* @return {Object}
197218
*/
198-
static loadData(dataValue) {
219+
static #loadData(dataValue) {
199220
if (typeof dataValue !== 'string') return dataValue || {};
200221

201222
let dataFile = PluginService.dataFiles.get(dataValue);

src/Plugin/AssetCompiler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const AssetGenerator = require('webpack/lib/asset/AssetGenerator');
1212

1313
const { pluginName } = require('../config');
1414
const { baseUri, urlPathPrefix, cssLoaderName } = require('../Loader/Utils');
15+
const { findRootIssuer } = require('../Common/CompilationHelpers');
1516
const { isDir } = require('../Common/FileUtils');
1617
const createPersistentCache = require('./createPersistentCache');
1718
const PersistentCache = require('./PersistentCache');
@@ -34,7 +35,6 @@ const Integrity = require('./Extras/Integrity');
3435

3536
const { compilationName, verbose } = require('./Messages/Info');
3637
const { PluginError, afterEmitException } = require('./Messages/Exception');
37-
const Preprocessor = require('../Loader/Preprocessor');
3838

3939
const loaderPath = require.resolve('../Loader');
4040

@@ -585,7 +585,7 @@ class AssetCompiler {
585585
// to avoid splitting the loader runtime scripts;
586586
// allow runtime scripts for styles imported in JavaScript, regards deep imported styles via url()
587587
if (isIssuerStyle && file.endsWith('.js')) {
588-
const rootIssuer = Collection.findRootIssuer(issuer);
588+
const rootIssuer = findRootIssuer(this.compilation, issuer);
589589
meta.isScript = true;
590590

591591
// return true if the root issuer is a JS (not style and not template), otherwise return false
@@ -605,7 +605,7 @@ class AssetCompiler {
605605
// try to detect imported style as resolved resource file, because a request can be a node module w/o an extension
606606
// the issuer can be a style if a scss contains like `@import 'main.css'`
607607
if (!Option.isStyle(issuer) && !Option.isEntry(issuer) && meta.isStyle) {
608-
const rootIssuer = Collection.findRootIssuer(issuer);
608+
const rootIssuer = findRootIssuer(this.compilation, issuer);
609609

610610
Collection.importStyleRootIssuers.add(rootIssuer || issuer);
611611
meta.isImportedStyle = true;

src/Plugin/Collection.js

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -555,39 +555,6 @@ class Collection {
555555
return walk(module);
556556
}
557557

558-
/**
559-
* Find the root issuer of a resource.
560-
*
561-
* @param {string} resource
562-
* @return {string|undefined}
563-
*/
564-
static findRootIssuer(resource) {
565-
const moduleMap = this.compilation.moduleGraph._moduleMap;
566-
const modules = moduleMap.keys();
567-
const [resourceFile] = resource.split('?', 1);
568-
let current;
569-
let parent;
570-
571-
for (let module of modules) {
572-
// skip non normal modules, e.g. runtime
573-
if (!module.resource) continue;
574-
575-
const [file] = module.resource.split('?', 1);
576-
if (file === resourceFile) {
577-
current = module;
578-
break;
579-
}
580-
}
581-
582-
if (current) {
583-
while ((parent = moduleMap.get(current).issuer)) {
584-
current = parent;
585-
}
586-
}
587-
588-
return current && current.resource !== resource ? current.resource : undefined;
589-
}
590-
591558
/**
592559
* Find insert position for styles in the HTML head.
593560
*

src/Plugin/PluginService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class PluginService {
114114
/**
115115
* Returns plugin options instance.
116116
*
117-
* TODO: rename to getPluginOptionInstance()
117+
* TODO: rename to getOptionInstance()
118118
*
119119
* @return {OptionPluginInterface}
120120
*/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"description": "Watch changes in a template function imported in js",
3+
"scripts": {
4+
"start": "webpack serve --mode development",
5+
"watch": "webpack watch --mode development",
6+
"build": "webpack --mode=production --progress"
7+
},
8+
"devDependencies": {
9+
"html-bundler-webpack-plugin": "file:../../.."
10+
}
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import tmpl from './partials/content.html';
2+
3+
const html = tmpl({
4+
name: 'World',
5+
});
6+
7+
document.getElementById('main').innerHTML = html;
8+
9+
console.log('>> app');
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Home</title>
5+
<!-- load the rendered template in JS and add it into HTML in runtime -->
6+
<script src="./app.js" defer="defer"></script>
7+
</head>
8+
<body>
9+
<div id="main"></div>
10+
</body>
11+
</html>

0 commit comments

Comments
 (0)