pug-plugin
Advanced tools
Comparing version 2.0.1 to 2.1.0-alpha.0
# Change log | ||
## 2.1.0-beta.0 (2022-04-14) | ||
DON'T use this version!\ | ||
This is only for private temporary test. This version will be soon deleted. | ||
- feat: added supports the `responsive-loader` (EXPERIMENTAL) | ||
## 2.0.2 (2022-04-08) | ||
- feat: caching of an already processed asset when the same asset is required in different issuer files | ||
- fix: conflict of multiple styles with the same filename | ||
- fix: resolving url() in styles required in pug | ||
- fix: potential collision when resolving resources for `compile` method | ||
- test: caching for styles required with same name | ||
## 2.0.1 (2022-04-03) | ||
@@ -4,0 +17,0 @@ - fixed incorrect output directory for a module if the option `outputPath` was relative |
{ | ||
"name": "pug-plugin", | ||
"version": "2.0.1", | ||
"description": "The pug plugin to handle the pug, html, css, scss files in webpack entry and extract css from pug.", | ||
"version": "2.1.0-alpha.0", | ||
"description": "Pug plugin to extract html, css and js from pug templates defined in webpack entry.", | ||
"keywords": [ | ||
"pug", | ||
"plugin", | ||
"webpack", | ||
"entry", | ||
"pug", | ||
"html", | ||
@@ -68,3 +69,3 @@ "css", | ||
"devDependencies": { | ||
"@babel/core": "^7.17.8", | ||
"@babel/core": "^7.17.9", | ||
"@babel/preset-env": "^7.16.11", | ||
@@ -74,11 +75,11 @@ "@types/jest": "^27.4.1", | ||
"css-loader": "^6.7.1", | ||
"cssnano": "^5.1.5", | ||
"cssnano": "^5.1.7", | ||
"html-loader": "^3.1.0", | ||
"jest": "^27.5.1", | ||
"material-icons": "^1.10.7", | ||
"material-icons": "^1.10.8", | ||
"mini-css-extract-plugin": "^2.6.0", | ||
"postcss-loader": "^6.2.1", | ||
"prettier": "^2.6.1", | ||
"prettier": "^2.6.2", | ||
"rimraf": "^3.0.2", | ||
"sass": "^1.49.9", | ||
"sass": "^1.50.0", | ||
"sass-loader": "^12.6.0", | ||
@@ -88,4 +89,4 @@ "style-loader": "^3.3.1", | ||
"resolve-url-loader": "^5.0.0", | ||
"webpack": "^5.71.0" | ||
"webpack": "^5.72.0" | ||
} | ||
} |
@@ -24,24 +24,45 @@ <div align="center"> | ||
This plugin extract HTML and CSS from `pug` `html` `scss` `css` files defined in `webpack entry` and save into separate files. | ||
The plugin can extract CSS and JavaScript from source files required in pug, without necessary to define them in the webpack entry. | ||
The pug plugin extract HTML, JavaScript and CSS from pug template defined in `webpack entry`. | ||
Using the `pug-plugin` no longer requires additional plugins and loaders such as: | ||
- [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) | ||
- [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) | ||
- [webpack-remove-empty-scripts](https://github.com/webdiscus/webpack-remove-empty-scripts) | ||
or [webpack-fix-style-only-entries](https://github.com/fqborges/webpack-fix-style-only-entries) \ | ||
(bug fix plugins for `mini-css-extract-plugin`) | ||
- [resolve-url-loader](https://github.com/bholloway/resolve-url-loader) | ||
- [pug-loader](https://github.com/webdiscus/pug-loader) (this loader is already included in the `pug-plugin`) | ||
Now is possible to use the source files of styles and scripts directly in pug. | ||
```pug | ||
link(href=require('./styles.scss') rel='stylesheet') | ||
script(src=require('./main.js')) | ||
``` | ||
The generated HTML contains hashed css and js filenames, depending on how webpack is configured. | ||
```html | ||
<link rel="stylesheet" href="/assets/css/styles.05e4dd86.css"> | ||
<script src="/assets/js/main.f4b855d8.js"></script> | ||
``` | ||
> The plugin can be used not only for `pug` but also for simply extracting `HTML` or `CSS` from `webpack entry`, independent of pug usage. | ||
Now is possible to define only pug templates in webpack entry. All styles and scripts will be automatically extracted from pug. | ||
```js | ||
const PugPlugin = require('pug-plugin'); | ||
module.exports = { | ||
entry: { | ||
'index': './src/index.pug', // extract html, css and js from pug | ||
}, | ||
plugins: [ | ||
new PugPlugin(), | ||
], | ||
// ... | ||
}; | ||
``` | ||
## Requirements | ||
- **Webpack 5** \ | ||
⚠️ Working with Webpack 4 is not guaranteed. | ||
- **Asset Modules** for Webpack 5: `asset/resource` `asset/inline` `asset/source` `asset` \ | ||
⚠️ Does not support deprecated modules such as `file-loader` `url-loader` `raw-loader`. | ||
- **Pug 3** \ | ||
⚠️ By usage Pug v2.x is required extra install the `pug-walk` package. Working with Pug < v3.0.2 is not guaranteed. | ||
> 💡 The required styles and scripts in pug do not need to define in the webpack entry. | ||
> All required resources will be automatically handled by webpack. | ||
The single pug plugin perform the most commonly used functions of the following packages: | ||
| Packages | Features | | ||
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------| | ||
| [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) | extract HTML from pug | | ||
| [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) | extract CSS from styles | | ||
| [webpack-remove-empty-scripts](https://github.com/webdiscus/webpack-remove-empty-scripts) | prevent generating empty files by the `mini-css-extract-plugin` | | ||
| [resolve-url-loader](https://github.com/bholloway/resolve-url-loader) | resolve the url in CSS | | ||
| [pug-loader](https://github.com/webdiscus/pug-loader) | the pug loader is already included in the pug plugin | | ||
You can replace all of the above packages with just one pug plugin. | ||
<a id="install" name="install" href="#install"></a> | ||
@@ -56,3 +77,3 @@ ## Install | ||
The minimal configuration in `webpack.config.js`: | ||
The minimal webpack config to extract HTML from pug: | ||
```js | ||
@@ -59,0 +80,0 @@ const path = require('path'); |
@@ -70,4 +70,6 @@ const fs = require('fs'); | ||
*/ | ||
const resolveException = (file) => { | ||
let message = `\n${ansis.black.bgRedBright(`[${plugin}]`)} Can't resolve the file ${ansis.cyan(file)}`; | ||
const resolveException = (file, issuer) => { | ||
let message = `\n${ansis.black.bgRedBright(`[${plugin}]`)} Can't resolve the file ${ansis.cyan( | ||
file | ||
)} in ${ansis.blueBright(issuer)}`; | ||
@@ -74,0 +76,0 @@ if (path.isAbsolute(file) && !fs.existsSync(file)) { |
929
src/index.js
const vm = require('vm'); | ||
const path = require('path'); | ||
const url = require('url'); | ||
const ansis = require('ansis'); | ||
@@ -11,3 +12,3 @@ const { merge } = require('webpack-merge'); | ||
const { extractHtml, extractCss } = require('./modules'); | ||
const { isFunction, pathToPosix, outToConsole } = require('./utils'); | ||
const { isFunction, pathToPosix, parseRequest, outToConsole } = require('./utils'); | ||
const { urlDependencyResolver, resourceResolver } = require('./resolver'); | ||
@@ -49,3 +50,3 @@ | ||
/** | ||
* @typedef {Object} AssetEntry | ||
* @typedef {Object} AssetEntryOptions | ||
* @property {string} name The key of webpack entry. | ||
@@ -99,55 +100,402 @@ * @property {string} file The output asset file with absolute path. | ||
class PugPlugin { | ||
/** @type {AssetEntry[]} */ | ||
entries = []; | ||
entryIndex = 1; | ||
/** | ||
* AssetTrash singleton. | ||
* Accumulate and remove junk assets from compilation. | ||
*/ | ||
const AssetTrash = { | ||
trash: [], | ||
/** | ||
* Add a junk js file to trash. | ||
* | ||
* @param {string} file | ||
*/ | ||
toTrash(file) { | ||
this.trash.push(file); | ||
}, | ||
/** | ||
* @type {{ | ||
* files: {}, | ||
* trashAssets: [], | ||
* add(name: string, request: string, issuer: string): void, | ||
* setIssuerFilename(issuer: string, filename: string): void, | ||
* has(request: string): boolean, | ||
* reset(): void | ||
* }} | ||
* Remove all js trash files from compilation. | ||
* | ||
* @param {Compilation} compilation The instance of the webpack compilation. | ||
*/ | ||
scripts = { | ||
files: {}, | ||
trashAssets: [], | ||
clearCompilation(compilation) { | ||
this.trash.forEach((file) => { | ||
compilation.deleteAsset(file); | ||
}); | ||
this.reset(); | ||
}, | ||
add(name, request, issuer) { | ||
this.files[request] = { | ||
name, | ||
issuer: { | ||
filename: undefined, | ||
request: issuer, | ||
}, | ||
}; | ||
}, | ||
/** | ||
* Clear caches before start of this plugin. | ||
*/ | ||
reset() { | ||
this.trash = []; | ||
}, | ||
}; | ||
setIssuerFilename(issuer, filename) { | ||
for (let request in this.files) { | ||
if (this.files[request].issuer.request === issuer) { | ||
this.files[request].issuer.filename = filename; | ||
/** | ||
* AssetEntry singleton. | ||
*/ | ||
const AssetEntry = { | ||
/** @type {AssetEntryOptions[]} */ | ||
entries: [], | ||
/** @type {string[]} */ | ||
addedToCompilationEntryNames: [], | ||
compilation: null, | ||
EntryPlugin: null, | ||
AssetScript: null, | ||
/** | ||
* @param {Compilation} compilation The instance of the webpack compilation. | ||
* @param {AssetScript} AssetScript The instance of the AssetScript. | ||
*/ | ||
init(compilation, AssetScript) { | ||
this.compilation = compilation; | ||
this.EntryPlugin = compilation.compiler.webpack.EntryPlugin; | ||
this.AssetScript = AssetScript; | ||
this.resetAdditionalEntries(); | ||
}, | ||
/** | ||
* @param {{}} entry The webpack entry object. | ||
* @param {AssetEntryOptions} assetEntryOptions | ||
* @param {string} webpackOutputPath | ||
*/ | ||
add(entry, assetEntryOptions, webpackOutputPath) { | ||
const { outputPath, filenameTemplate } = assetEntryOptions; | ||
const relativeOutputPath = path.isAbsolute(outputPath) ? path.relative(webpackOutputPath, outputPath) : outputPath; | ||
entry.filename = (pathData, assetInfo) => { | ||
if (!assetEntryOptions.filename) { | ||
Object.defineProperty(assetEntryOptions, 'filename', { | ||
set(filename) { | ||
// replace the setter with value of resolved filename | ||
delete this.filename; | ||
this.filename = filename; | ||
this.file = path.join(outputPath, filename); | ||
}, | ||
}); | ||
} | ||
let filename = isFunction(filenameTemplate) ? filenameTemplate(pathData, assetInfo) : filenameTemplate; | ||
if (relativeOutputPath) { | ||
filename = path.posix.join(relativeOutputPath, filename); | ||
} | ||
return filename; | ||
}; | ||
this.entries.push(assetEntryOptions); | ||
}, | ||
/** | ||
* Add a resource from pug, e.g. script, to webpack compilation. | ||
* | ||
* @param {string} importFile | ||
* @param {string} filenameTemplate | ||
* @param {string} outputPath | ||
* @param {string} context | ||
* @param {string} issuer | ||
* @return {boolean} | ||
*/ | ||
addToCompilation({ importFile, filenameTemplate, outputPath, context, issuer }) { | ||
let name = this.AssetScript.getUniqueName(importFile, issuer); | ||
// ignore duplicate entries with same name | ||
if (name === false) return false; | ||
const entry = { | ||
name, | ||
runtime: undefined, | ||
layer: undefined, | ||
dependOn: undefined, | ||
baseUri: undefined, | ||
publicPath: undefined, | ||
chunkLoading: undefined, | ||
asyncChunks: undefined, | ||
wasmLoading: undefined, | ||
library: undefined, | ||
}; | ||
/** @type {AssetEntryOptions} */ | ||
const assetEntryOptions = { | ||
name, | ||
filenameTemplate, | ||
filename: undefined, | ||
file: undefined, | ||
importFile, | ||
sourcePath: context, | ||
outputPath, | ||
postprocess: undefined, | ||
extract: undefined, | ||
verbose: false, | ||
}; | ||
this.add(entry, assetEntryOptions, outputPath); | ||
// adds the entry of the script from pug to the compilation | ||
// see reference: node_modules/webpack/lib/EntryPlugin.js | ||
const entryDependency = this.EntryPlugin.createDependency(importFile, { name }); | ||
this.compilation.addEntry(context, entryDependency, entry, (err) => { | ||
if (err) addEntryException(err, name); | ||
}); | ||
this.addedToCompilationEntryNames.push(name); | ||
return true; | ||
}, | ||
/** | ||
* @param {string} name The entry name. | ||
* @returns {AssetEntryOptions} | ||
*/ | ||
findByName(name) { | ||
return this.entries.find((entry) => entry.name === name); | ||
}, | ||
/** | ||
* @param {Module} module The chunk module. | ||
* @returns {boolean} | ||
*/ | ||
isEntryModule(module) { | ||
if (!module.resource) return false; | ||
const { resource } = parseRequest(module.resource); | ||
return this.entries.find((entry) => entry.importFile === resource) !== undefined; | ||
}, | ||
/** | ||
* Reset entries added not via webpack entry. | ||
* This is important for webpack watch and serve. | ||
*/ | ||
resetAdditionalEntries() { | ||
for (const entryName of this.addedToCompilationEntryNames) { | ||
const index = this.entries.findIndex((entry) => entry.name === entryName); | ||
this.entries.splice(index, 1); | ||
} | ||
this.addedToCompilationEntryNames = []; | ||
}, | ||
clear() { | ||
this.entries = []; | ||
this.addedToCompilationEntryNames = []; | ||
}, | ||
}; | ||
/** | ||
* AssetScript singleton. | ||
*/ | ||
const AssetScript = { | ||
files: [], | ||
index: 1, | ||
/** | ||
* Replace all required source filenames with generating asset filenames. | ||
* Note: this method must be called in the afterProcessAssets compilation hook. | ||
* | ||
* @param {Compilation} compilation The instance of the webpack compilation. | ||
* @param {string} outputPublicPath The output public path. | ||
*/ | ||
replaceSourceFilesInCompilation(compilation, outputPublicPath) { | ||
const RawSource = compilation.compiler.webpack.sources.RawSource; | ||
let existsScripts = []; | ||
// in the content, replace the source script file with the output filename | ||
for (let asset of this.files) { | ||
let { request: sourceFile, filename: assetFile, chunkFiles } = asset; | ||
const issuerFile = asset.issuer.filename; | ||
if (!compilation.assets.hasOwnProperty(issuerFile)) { | ||
// let's show an original error | ||
continue; | ||
} | ||
const content = compilation.assets[issuerFile].source(); | ||
let chunkScripts = '', | ||
chunkFile, | ||
newContent; | ||
// save asset filenames to cache for webpack serve, watch | ||
if (assetFile == null) { | ||
const chunkGroup = compilation.namedChunkGroups.get(asset.name); | ||
const entrypointChunk = chunkGroup.getEntrypointChunk(); | ||
chunkFile = entrypointChunk.files.values().next().value; | ||
assetFile = path.posix.join(outputPublicPath, chunkFile); | ||
chunkFiles = chunkGroup.getFiles(); | ||
asset.filename = assetFile; | ||
asset.chunkFiles = chunkFiles; | ||
} | ||
// replace source filename with asset filename | ||
if (chunkFiles.length > 1) { | ||
existsScripts.push(chunkFile); | ||
// generate additional scripts of chunks | ||
for (let file of chunkFiles) { | ||
if (existsScripts.indexOf(file) < 0) { | ||
const scriptFile = path.posix.join(outputPublicPath, file); | ||
chunkScripts += `<script src="${scriptFile}"></script>`; | ||
existsScripts.push(file); | ||
} | ||
} | ||
// inject generated chunks before original <script> and replace source file with output filename | ||
const srcStartPos = content.indexOf(sourceFile); | ||
const srcEndPos = srcStartPos + sourceFile.length; | ||
let tagStartPos = srcStartPos; | ||
while (tagStartPos >= 0 && content.charAt(--tagStartPos) !== '<') {} | ||
newContent = | ||
content.slice(0, tagStartPos) + | ||
chunkScripts + | ||
content.slice(tagStartPos, srcStartPos) + | ||
assetFile + | ||
content.slice(srcEndPos); | ||
} else { | ||
newContent = content.replace(sourceFile, assetFile); | ||
} | ||
}, | ||
has(request) { | ||
return this.files.hasOwnProperty(request); | ||
}, | ||
compilation.assets[issuerFile] = new RawSource(newContent); | ||
} | ||
getResource(request) { | ||
const [resource, query] = request.split('?'); | ||
// --> console.log('\n -+-+-+ replaceSourceFilesInCompilation:\n', this.files, existsScripts); | ||
}, | ||
return query === 'isScript' ? resource : null; | ||
}, | ||
/** | ||
* @param {string} request The source file of asset. | ||
* @param {string} issuer The issuer of the asset. | ||
* @return {string | false} return false if the file was already processed else return unique assetFile | ||
*/ | ||
getUniqueName(request, issuer) { | ||
let { name } = path.parse(request); | ||
reset() { | ||
this.files = {}; | ||
this.trashAssets = []; | ||
}, | ||
}; | ||
const entry = AssetEntry.findByName(name); | ||
let uniqueName = name; | ||
let result = name; | ||
// the entrypoint name must be unique, if already exists then add index: `main` => `main.1`, etc | ||
if (entry) { | ||
if (entry.importFile === request) { | ||
result = false; | ||
} else { | ||
uniqueName = name + '.' + this.index++; | ||
result = uniqueName; | ||
} | ||
} | ||
this.add(uniqueName, request, issuer); | ||
return result; | ||
}, | ||
/** | ||
* @param {string} name The unique name of entry point. | ||
* @param {string} request The required resource file. | ||
* @param {string} issuer The source file of issuer of the required file. | ||
*/ | ||
add(name, request, issuer) { | ||
// prevent to add duplicates by webpack serve, watch | ||
if (this.files.find((item) => item.request === request && item.issuer.request === issuer)) return; | ||
this.files.push({ | ||
name, | ||
request, | ||
filename: undefined, | ||
chunkFiles: [], | ||
issuer: { | ||
filename: undefined, | ||
request: issuer, | ||
}, | ||
}); | ||
}, | ||
/** | ||
* | ||
* @param {string} issuer The source file of issuer of the required file. | ||
* @param {string} filename The asset filename of issuer. | ||
*/ | ||
setIssuerFilename(issuer, filename) { | ||
for (let item of this.files) { | ||
if (item.issuer.request === issuer) { | ||
item.issuer.filename = filename; | ||
} | ||
} | ||
}, | ||
has(request) { | ||
return this.files.find((item) => item.request === request); | ||
}, | ||
getResource(request) { | ||
const { resource, query } = parseRequest(request); | ||
return query === 'isScript' ? resource : null; | ||
}, | ||
/** | ||
* Reset cache before new compilation by webpack watch or serve. | ||
*/ | ||
reset() { | ||
// don't reset files because this cache is used by webpack watch or serve | ||
//this.files = []; | ||
this.index = 1; | ||
}, | ||
/** | ||
* Clear caches before start of this plugin. | ||
*/ | ||
clear() { | ||
this.files = []; | ||
this.index = 1; | ||
}, | ||
}; | ||
/** | ||
* AssetModule singleton. | ||
*/ | ||
const AssetModule = { | ||
files: [], | ||
index: {}, | ||
/** | ||
* @param {string} sourceFile | ||
* @param {string} assetFile | ||
* @return {string|boolean} return false if the file was already processed else return unique assetFile | ||
*/ | ||
getUniqueFilename(sourceFile, assetFile) { | ||
const sameAssets = this.files.filter((item) => item.filename === assetFile) || []; | ||
let uniqueFilename = assetFile; | ||
if (sameAssets.length > 0) { | ||
if (sameAssets.find((item) => item.request === sourceFile)) return false; | ||
let index = '.' + this.index[assetFile]++; | ||
let pos = assetFile.lastIndexOf('.'); | ||
// paranoid filename extension check, should normally never occur | ||
if (pos < 0) pos = assetFile.length; | ||
uniqueFilename = assetFile.slice(0, pos) + index + assetFile.slice(pos); | ||
} else { | ||
// start index of same asset filename, eg: styles.1.css | ||
this.index[assetFile] = 1; | ||
} | ||
this.files.push({ | ||
request: sourceFile, | ||
filename: assetFile, | ||
}); | ||
return uniqueFilename; | ||
}, | ||
reset() { | ||
this.files = []; | ||
}, | ||
}; | ||
/** | ||
* Class PugPlugin. | ||
*/ | ||
class PugPlugin { | ||
entryLibrary = { | ||
@@ -176,18 +524,23 @@ name: 'return', | ||
const { RawSource } = compiler.webpack.sources; | ||
const { HotUpdateChunk } = compiler.webpack; | ||
const webpackOptions = compiler.options; | ||
const { | ||
path: webpackOutputPath, | ||
publicPath: webpackOutputPublicPath, | ||
filename: webpackOutputFilename, | ||
publicPath: webpackPublicPath, | ||
filename: webpackScriptFilename, | ||
} = webpackOptions.output; | ||
const { RawSource } = compiler.webpack.sources; | ||
const { EntryPlugin, HotUpdateChunk } = compiler.webpack; | ||
// TODO: resolveInPaths 'auto' publicPath | ||
if (webpackOutputPublicPath == null || webpackOutputPublicPath === 'auto') publicPathException(); | ||
if (webpackPublicPath == null || webpackPublicPath === 'auto') publicPathException(); | ||
resourceResolver.init({ | ||
publicPath: webpackOutputPublicPath, | ||
publicPath: webpackPublicPath, | ||
}); | ||
// clear caches by tests, webpack watch or serve | ||
AssetEntry.clear(); | ||
AssetScript.clear(); | ||
AssetTrash.reset(); | ||
// enable library type `jsonp` for compilation JS from source into HTML string via Function() | ||
@@ -215,3 +568,3 @@ if (webpackOptions.output.enabledLibraryTypes.indexOf('jsonp') < 0) { | ||
const importFile = entry.import[0]; | ||
let sourceFile = this.getFileFromImport(importFile); | ||
let { resource: sourceFile } = parseRequest(importFile); | ||
const module = this.getModule(sourceFile); | ||
@@ -238,11 +591,11 @@ | ||
/** @type {AssetEntry} */ | ||
const assetEntry = { | ||
name: name, | ||
filenameTemplate: filenameTemplate, | ||
/** @type {AssetEntryOptions} */ | ||
const assetEntryOptions = { | ||
name, | ||
filenameTemplate, | ||
filename: undefined, | ||
file: undefined, | ||
importFile: sourceFile, | ||
sourcePath: sourcePath, | ||
outputPath: outputPath, | ||
sourcePath, | ||
outputPath, | ||
library: entry.library, | ||
@@ -254,118 +607,59 @@ postprocess: isFunction(postprocess) ? postprocess : null, | ||
const relativeOutputPath = path.isAbsolute(outputPath) | ||
? path.relative(webpackOutputPath, outputPath) | ||
: outputPath; | ||
entry.filename = (pathData, assetInfo) => { | ||
if (!assetEntry.filename) { | ||
Object.defineProperty(assetEntry, 'filename', { | ||
set(filename) { | ||
// replace the setter with value of resolved filename | ||
delete this.filename; | ||
this.filename = filename; | ||
this.file = path.join(webpackOutputPath, filename); | ||
}, | ||
}); | ||
} | ||
let filename = isFunction(filenameTemplate) ? filenameTemplate(pathData, assetInfo) : filenameTemplate; | ||
filename = path.posix.join(relativeOutputPath, filename); | ||
return filename; | ||
}; | ||
this.entries.push(assetEntry); | ||
AssetEntry.add(entry, assetEntryOptions, webpackOutputPath); | ||
} | ||
}); | ||
// Normal Module Factory | ||
compiler.hooks.normalModuleFactory.tap(plugin, (normalModuleFactory) => { | ||
urlDependencyResolver.init(normalModuleFactory.fs.fileSystem); | ||
this.scripts.reset(); | ||
// This compilation | ||
compiler.hooks.thisCompilation.tap(plugin, (compilation, { normalModuleFactory, contextModuleFactory }) => { | ||
const verbose = this.verbose; | ||
this.compilation = compilation; | ||
normalModuleFactory.hooks.createModule.tap(plugin, (createData, resolveData) => { | ||
const { context, rawRequest, resource } = createData; | ||
if (rawRequest !== resource) { | ||
resourceResolver.addResolvedFile(context, rawRequest, resource); | ||
} | ||
}); | ||
urlDependencyResolver.init(normalModuleFactory.fs.fileSystem, compiler.options); | ||
AssetEntry.init(compilation, AssetScript); | ||
AssetTrash.reset(); | ||
AssetScript.reset(); | ||
AssetModule.reset(); | ||
// before resolve | ||
normalModuleFactory.hooks.beforeResolve.tap(plugin, (resolveData) => { | ||
const compilation = this.compilation; | ||
const { context, request } = resolveData; | ||
const scriptResource = this.scripts.getResource(request); | ||
const importFile = AssetScript.getResource(request); | ||
if (scriptResource != null) { | ||
if (importFile) { | ||
const { issuer } = resolveData.contextInfo; | ||
let { name } = path.parse(scriptResource); | ||
const existsName = this.entries.find((item) => item.name === name); | ||
const res = AssetEntry.addToCompilation({ | ||
importFile, | ||
filenameTemplate: webpackScriptFilename, | ||
outputPath: webpackOutputPath, | ||
context, | ||
issuer, | ||
}); | ||
if (!res) return false; | ||
// the entrypoint name must be unique, if already exists then add index: `main` => `main1`, `main2`, etc | ||
if (existsName) name += this.entryIndex++; | ||
this.scripts.add(name, scriptResource, issuer); | ||
resolveData.request = scriptResource; | ||
resolveData.request = importFile; | ||
} else if (resolveData.dependencyType === 'url') { | ||
urlDependencyResolver.resolve(resolveData); | ||
} | ||
}); | ||
// TODO: refactoring - add to entry as function | ||
const outputPath = webpackOutputPath; | ||
const relativeOutputPath = path.relative(webpackOutputPath, outputPath); | ||
const filenameTemplate = webpackOutputFilename; | ||
const assetEntry = { | ||
name, | ||
filenameTemplate: filenameTemplate, | ||
filename: undefined, | ||
file: undefined, | ||
importFile: scriptResource, | ||
sourcePath: context, | ||
outputPath: outputPath, | ||
postprocess: undefined, | ||
extract: undefined, | ||
verbose: true, | ||
}; | ||
// after create module | ||
normalModuleFactory.hooks.module.tap(plugin, (module, createData, resolveData) => { | ||
const { context, rawRequest, resource } = createData; | ||
const entryOptions = { | ||
name, | ||
filename(pathData, assetInfo) { | ||
if (!assetEntry.filename) { | ||
Object.defineProperty(assetEntry, 'filename', { | ||
set(filename) { | ||
// replace the setter with value of resolved filename | ||
delete this.filename; | ||
this.filename = filename; | ||
this.file = path.join(webpackOutputPath, filename); | ||
}, | ||
}); | ||
} | ||
if (rawRequest !== resource) { | ||
resourceResolver.addResolvedFile(context, rawRequest, resource); | ||
} | ||
let filename = isFunction(filenameTemplate) ? filenameTemplate(pathData, assetInfo) : filenameTemplate; | ||
filename = path.posix.join(relativeOutputPath, filename); | ||
return filename; | ||
}, | ||
runtime: undefined, | ||
layer: undefined, | ||
dependOn: undefined, | ||
baseUri: undefined, | ||
publicPath: undefined, | ||
chunkLoading: undefined, | ||
asyncChunks: undefined, | ||
wasmLoading: undefined, | ||
library: undefined, | ||
}; | ||
if (resolveData.dependencyType === 'url') { | ||
const issuer = resolveData.contextInfo.issuer; | ||
// see reference: node_modules/webpack/lib/EntryPlugin.js | ||
const dep = EntryPlugin.createDependency(scriptResource, { name }); | ||
compilation.addEntry(context, dep, entryOptions, (err) => { | ||
if (err) addEntryException(err, name); | ||
}); | ||
this.entries.push(assetEntry); | ||
} else if (resolveData.dependencyType === 'url') { | ||
urlDependencyResolver.resolve(resolveData); | ||
// TODO: detect correct type by usage other loaders like 'responsive-loader' | ||
module.isDependencyTypeUrl = true; | ||
if (resolveData?.contextInfo.issuer) { | ||
resourceResolver.addToModuleCache(resource, rawRequest, issuer); | ||
} | ||
} | ||
}); | ||
}); | ||
// This compilation | ||
compiler.hooks.thisCompilation.tap(plugin, (compilation) => { | ||
const verbose = this.verbose; | ||
this.compilation = compilation; | ||
// build module | ||
@@ -389,3 +683,3 @@ compilation.hooks.buildModule.tap(plugin, (module) => { | ||
const entry = this.getEntryByName(chunk.name); | ||
const entry = AssetEntry.findByName(chunk.name); | ||
@@ -395,2 +689,3 @@ // process only entries supported by this plugin | ||
const chunkModules = chunkGraph.getChunkModulesIterable(chunk); | ||
const sources = new Set(); | ||
@@ -400,17 +695,22 @@ const contentHashType = 'javascript'; | ||
// the current resource issuer | ||
let resourceIssuer = ''; | ||
entry.filename = assetFile; | ||
this.scripts.setIssuerFilename(entry.importFile, assetFile); | ||
resourceResolver.scripts = this.scripts; | ||
resourceResolver.clearChunkCache(); | ||
AssetScript.setIssuerFilename(entry.importFile, assetFile); | ||
resourceResolver.scripts = AssetScript; | ||
if (verbose) this.verboseEntry(entry); | ||
for (const module of chunkGraph.getChunkModules(chunk)) { | ||
for (const module of chunkModules) { | ||
if (!module.resource) continue; | ||
const sourceFile = module.resource; | ||
const context = path.dirname(sourceFile); | ||
// add needless chunks to trash | ||
if (this.scripts.has(module.resource)) { | ||
if (AssetScript.has(sourceFile)) { | ||
const file = module.buildInfo.filename; | ||
if (file != null) { | ||
this.scripts.trashAssets.push(file); | ||
AssetTrash.toTrash(file); | ||
} | ||
@@ -422,5 +722,5 @@ continue; | ||
const resourceResolveDataContext = module.resourceResolveData.context || {}; | ||
const issuerFile = resourceResolveDataContext.issuer || module.resource; | ||
const issuerFile = resourceResolveDataContext.issuer || sourceFile; | ||
if (this.isEntryModule(module) && chunkGraph.isEntryModuleInChunk(module, chunk)) { | ||
if (AssetEntry.isEntryModule(module) && chunkGraph.isEntryModuleInChunk(module, chunk)) { | ||
// entry-point | ||
@@ -431,3 +731,3 @@ const source = module.originalSource(); | ||
const pluginModule = this.getModule(module.resource) || entry; | ||
const pluginModule = this.getModule(sourceFile) || entry; | ||
@@ -457,2 +757,4 @@ const postprocessInfo = { | ||
}); | ||
resourceIssuer = entry.importFile; | ||
} else if (module.type === 'javascript/auto') { | ||
@@ -465,11 +767,10 @@ // require a resource supported via the plugin module, e.g. style | ||
const pluginModule = this.getModule(module.resource); | ||
const pluginModule = this.getModule(sourceFile); | ||
if (!pluginModule) continue; | ||
let { name } = path.parse(sourceFile); | ||
const filenameTemplate = pluginModule.filename ? pluginModule.filename : entry.filenameTemplate; | ||
// TODO: generate content hash for assets required in pug for true [contenthash] in filename | ||
// origin: buildInfo.assetInfo.contenthash or buildInfo.fullContentHash | ||
const hash = buildInfo.assetInfo ? buildInfo.assetInfo.contenthash : buildInfo.hash; | ||
const { name } = path.parse(module.resource); | ||
// TODO: research why sometimes the id is relative path instead of a number, it's ok? | ||
@@ -488,3 +789,9 @@ const id = chunkGraph.getModuleId(module); | ||
}; | ||
const assetFile = compilation.getAssetPath(filenameTemplate, contextData); | ||
let assetFile = compilation.getAssetPath(filenameTemplate, contextData); | ||
assetFile = AssetModule.getUniqueFilename(sourceFile, assetFile); | ||
// skip already processed assets | ||
if (assetFile === false) continue; | ||
const postprocessInfo = { | ||
@@ -494,3 +801,3 @@ isEntry: false, | ||
filename: filenameTemplate, | ||
sourceFile: module.resource, | ||
sourceFile, | ||
outputFile: entry.file, | ||
@@ -500,8 +807,6 @@ assetFile, | ||
resourceResolver.addToChunkCache(module, assetFile); | ||
if (verbose) { | ||
this.verboseExtractModule({ | ||
issuerFile, | ||
sourceFile: module.resource, | ||
sourceFile, | ||
assetFile: path.join(webpackOutputPath, assetFile), | ||
@@ -515,3 +820,3 @@ }); | ||
source: source.source().toString(), | ||
sourceFile: module.resource, | ||
sourceFile, | ||
assetFile, | ||
@@ -526,13 +831,126 @@ pluginModule, | ||
}); | ||
//resourceResolver.addToChunkCache(module, assetFile); | ||
resourceResolver.addToChunkCache(module, path.posix.join(webpackPublicPath, assetFile)); | ||
//resourceResolver.addToChunkCache2(context, module.rawRequest, assetFile); | ||
resourceIssuer = sourceFile; | ||
} else if (module.type === 'asset/resource') { | ||
// require a resource | ||
const assetFile = buildInfo.filename; | ||
resourceResolver.addToChunkCache(module, assetFile); | ||
// require a resource in pug or in css via url() | ||
let assetFile = buildInfo.filename; | ||
if (verbose) | ||
// TODO: refactoring code | ||
// -- BEGIN resource loader -- | ||
const resourceQuery = url.parse(module.rawRequest, true).query; | ||
// console.log( | ||
// '\n +++ addToChunkCache2: ', | ||
// assetFile, | ||
// module.type, | ||
// module.isDependencyTypeUrl, | ||
// '\n', | ||
// context, | ||
// '\n- rawRequest: ', | ||
// module.rawRequest, | ||
// '\n- assetFile: ', | ||
// assetFile, | ||
// //'\n- assets: ', | ||
// //buildInfo.assets, | ||
// //'\n- assetsInfo: ', | ||
// // buildInfo.assetsInfo, | ||
// '\n- resourceQuery: ', | ||
// resourceQuery | ||
// ); | ||
if (resourceQuery && resourceQuery.prop) { | ||
const prop = resourceQuery.prop; | ||
const source = module?.originalSource()?.source()?.toString(); | ||
//console.log(' ~~~~ COMPILED source: ', source); | ||
if (source) { | ||
const contextObject = vm.createContext({ | ||
__webpack_public_path__: webpackPublicPath, | ||
module: { exports: {} }, | ||
}); | ||
const script = new vm.Script(source, { filename: sourceFile }); | ||
const result = script.runInContext(contextObject); | ||
if (result && result.hasOwnProperty(prop)) { | ||
AssetTrash.toTrash(assetFile); | ||
assetFile = result[prop]; | ||
//console.log('\n~~~~ COMPILED ASSET: ', resourceQuery); | ||
resourceResolver.addToChunkCache(module, assetFile); | ||
if (verbose) { | ||
this.verboseExtractResource({ | ||
issuerFile, | ||
sourceFile, | ||
outputPath: webpackOutputPath, | ||
assetFile: assetFile, | ||
}); | ||
} | ||
continue; | ||
} | ||
} | ||
} | ||
// get the real filename of the asset by usage a loader for the resource, e.g. `responsive-loader` | ||
// and add the original asset file to trash to remove it from compilation | ||
if (buildInfo.assetsInfo != null) { | ||
const assets = Array.from(buildInfo.assetsInfo.keys()); | ||
if (assets.length > 0) { | ||
// dummy size for srcSec attribute with more than 2 files | ||
let defaultSize = assets.length > 1 ? ' 1' : ''; | ||
const realAssetFile = assets | ||
.map((value) => path.posix.join(webpackPublicPath, value) + defaultSize) | ||
.join(','); | ||
if (realAssetFile) { | ||
AssetTrash.toTrash(assetFile); | ||
assetFile = realAssetFile; | ||
// TODO: add original assetFile to trash | ||
//console.log('\n xxx DELETE original assetFile: ', buildInfo.filename, ' => OK: ', assetFile); | ||
resourceResolver.addToChunkCache(module, assetFile); | ||
if (verbose) { | ||
this.verboseExtractResource({ | ||
issuerFile, | ||
sourceFile, | ||
outputPath: webpackOutputPath, | ||
assetFile: assetFile, | ||
}); | ||
} | ||
continue; | ||
// assetFile = { | ||
// src: 'img/favicon.2ff47510-300w.webp', | ||
// srcSet: 'img/favicon.2ff47510-300w.webp 300w', | ||
// }; | ||
} | ||
} | ||
} | ||
// if (assetFile !== buildInfo.filename) { | ||
// console.log('\n === REAL assetFile: ', buildInfo.filename, ' => ', assetFile); | ||
// } | ||
// -- END resource loader -- | ||
assetFile = path.posix.join(webpackPublicPath, assetFile); | ||
if (verbose) { | ||
this.verboseExtractResource({ | ||
issuerFile, | ||
sourceFile: module.resource, | ||
assetFile: path.join(webpackOutputPath, assetFile), | ||
sourceFile, | ||
outputPath: webpackOutputPath, | ||
assetFile: assetFile, | ||
}); | ||
} | ||
if (module.isDependencyTypeUrl) { | ||
resourceResolver.setAssetFileInModuleCache(module.resource, assetFile); | ||
} else { | ||
//console.log('\n +++ addToChunkCache:\n', context, '\n', '\n- ', module.rawRequest, '\n- ', assetFile); | ||
resourceResolver.addToChunkCache(module, assetFile); | ||
//resourceResolver.addToChunkCache2(context, module.rawRequest, assetFile); | ||
} | ||
} | ||
@@ -564,7 +982,6 @@ } | ||
// called direct after renderManifest | ||
// direct after renderManifest | ||
compilation.hooks.chunkAsset.tap(plugin, (chunk, file) => { | ||
// avoid saving runtime js files from node_modules as assets by usage of the splitChunks optimization, | ||
// because it is never used in assets, it's wrong extracted files by webpack | ||
if (chunk.chunkReason && chunk.chunkReason.indexOf('defaultVendors') > 0) { | ||
@@ -580,3 +997,3 @@ const modules = compilation.chunkGraph.getChunkModules(chunk); | ||
) { | ||
this.scripts.trashAssets.push(file); | ||
AssetTrash.toTrash(file); | ||
} | ||
@@ -591,6 +1008,3 @@ } | ||
(assets) => { | ||
// clean trash | ||
this.scripts.trashAssets.forEach((file) => { | ||
compilation.deleteAsset(file); | ||
}); | ||
AssetTrash.clearCompilation(compilation); | ||
} | ||
@@ -602,54 +1016,3 @@ ); | ||
compilation.hooks.afterProcessAssets.tap(plugin, () => { | ||
const RawSource = compiler.webpack.sources.RawSource; | ||
let existsScripts = []; | ||
for (let sourceFile in this.scripts.files) { | ||
const item = this.scripts.files[sourceFile]; | ||
const issuerFile = item.issuer.filename; | ||
const chunkGroup = compilation.namedChunkGroups.get(item.name); | ||
const entrypointChunk = chunkGroup.getEntrypointChunk(); | ||
const chunkFile = entrypointChunk.files.values().next().value; | ||
const outputFile = path.posix.join(webpackOutputPublicPath, chunkFile); | ||
// replace source filename with asset filename | ||
const chunkFiles = chunkGroup.getFiles(); | ||
const asset = compilation.assets[issuerFile]; | ||
// continue by a potential error, normally never occur | ||
if (asset == null) continue; | ||
const content = asset.source(); | ||
let chunkScripts = ''; | ||
let newContent; | ||
if (chunkFiles.length > 1) { | ||
existsScripts.push(chunkFile); | ||
// generate additional scripts of chunks | ||
for (let file of chunkFiles) { | ||
if (existsScripts.indexOf(file) < 0) { | ||
const scriptFile = path.posix.join(webpackOutputPublicPath, file); | ||
chunkScripts += `<script src="${scriptFile}"></script>`; | ||
existsScripts.push(file); | ||
} | ||
} | ||
// inject generated chunks before original <script> and replace source file with output filename | ||
const srcStartPos = content.indexOf(sourceFile); | ||
const srcEndPos = srcStartPos + sourceFile.length; | ||
let tagStartPos = srcStartPos; | ||
while (tagStartPos >= 0 && content.charAt(--tagStartPos) !== '<') {} | ||
newContent = | ||
content.slice(0, tagStartPos) + | ||
chunkScripts + | ||
content.slice(tagStartPos, srcStartPos) + | ||
outputFile + | ||
content.slice(srcEndPos); | ||
} else { | ||
newContent = content.replace(sourceFile, outputFile); | ||
} | ||
compilation.assets[issuerFile] = new RawSource(newContent); | ||
} | ||
AssetScript.replaceSourceFilesInCompilation(compilation, webpackPublicPath); | ||
}); | ||
@@ -671,18 +1034,17 @@ }); | ||
let result, compiledCode; | ||
resourceResolver.setCurrentContext(path.dirname(sourceFile)); | ||
resourceResolver.setIssuer(sourceFile); | ||
if (pluginModule && pluginModule.compile) { | ||
compiledCode = pluginModule.compile(source, assetFile); | ||
} else { | ||
const contextOptions = { | ||
require: resourceResolver.require, | ||
// the `module.id` is required for `css-loader`, in module extractCss expected as source path | ||
module: { id: sourceFile }, | ||
}; | ||
const contextObject = vm.createContext(contextOptions); | ||
const sourceCjs = this.toCommonJS(source); | ||
const script = new vm.Script(sourceCjs, { filename: sourceFile }); | ||
compiledCode = script.runInContext(contextObject) || ''; | ||
} | ||
const sourceCjs = this.toCommonJS(source); | ||
const contextOptions = { | ||
require: resourceResolver.require, | ||
// the `module.id` is required for `css-loader`, in module extractCss expected as source path | ||
module: { id: sourceFile }, | ||
}; | ||
const contextObject = vm.createContext(contextOptions); | ||
const script = new vm.Script(sourceCjs, { filename: sourceFile }); | ||
compiledCode = script.runInContext(contextObject) || ''; | ||
//if (assetFile === 'index.html') console.log('### compiledCode: ', assetFile, sourceCjs, '\n\n:', compiledCode); | ||
try { | ||
@@ -741,45 +1103,13 @@ result = isFunction(compiledCode) ? compiledCode() : compiledCode; | ||
/** | ||
* Return the file part from import file. | ||
* For example: from `index.pug?data={"key":"value"}` return `index.pug` | ||
* | ||
* @param {string} str | ||
* @returns {string} | ||
*/ | ||
getFileFromImport(str) { | ||
return str.split('?', 1)[0]; | ||
} | ||
/** | ||
* @param {string} name The entry name. | ||
* @returns {AssetEntry} | ||
*/ | ||
getEntryByName(name) { | ||
return this.entries.find((entry) => entry.name === name); | ||
} | ||
/** | ||
* @param {string} filename The filename of a source file, can contain a query string. | ||
* @param {string} request The request of a source file, can contain a query string. | ||
* @returns {ModuleOptions} | ||
*/ | ||
getModule(filename) { | ||
return this.options.modules.find( | ||
(module) => module.enabled !== false && module.test.test(this.getFileFromImport(filename)) | ||
); | ||
getModule(request) { | ||
const { resource } = parseRequest(request); | ||
return this.options.modules.find((module) => module.enabled !== false && module.test.test(resource)); | ||
} | ||
/** | ||
* @param {Module} module The chunk module. | ||
* @returns {boolean} | ||
* @param {AssetEntryOptions} entry | ||
*/ | ||
isEntryModule(module) { | ||
const importFile = module.resource; | ||
return importFile | ||
? this.entries.find((entry) => entry.importFile === this.getFileFromImport(importFile)) !== undefined | ||
: false; | ||
} | ||
/** | ||
* @param {AssetEntry} entry | ||
*/ | ||
verboseEntry(entry) { | ||
@@ -789,3 +1119,3 @@ if (!entry) return; | ||
`${ansis.black.bgYellow(`[${plugin}]`)} Compile the entry ${ansis.green(entry.name)}\n` + | ||
` - filename: ${ | ||
` filename: ${ | ||
isFunction(entry.filenameTemplate) | ||
@@ -795,4 +1125,4 @@ ? ansis.greenBright('[Function: filename]') | ||
}\n` + | ||
` - source: ${ansis.cyan(entry.importFile)}\n` + | ||
` - output: ${ansis.cyanBright(entry.file)}\n` | ||
` source: ${ansis.cyan(entry.importFile)}\n` + | ||
` output: ${ansis.cyanBright(entry.file)}\n` | ||
); | ||
@@ -810,4 +1140,4 @@ } | ||
`${ansis.green(issuerFile)}\n` + | ||
` - source: ${ansis.cyan(sourceFile)}\n` + | ||
` - output: ${ansis.cyanBright(assetFile)}\n` | ||
` source: ${ansis.cyan(sourceFile)}\n` + | ||
` output: ${ansis.cyanBright(assetFile)}\n` | ||
); | ||
@@ -819,10 +1149,13 @@ } | ||
* @param {string} sourceFile | ||
* @param {string} outputPath | ||
* @param {string} assetFile | ||
*/ | ||
verboseExtractResource({ issuerFile, sourceFile, assetFile }) { | ||
verboseExtractResource({ issuerFile, sourceFile, outputPath, assetFile }) { | ||
outToConsole( | ||
`${ansis.black.bgYellow(`[${plugin}]`) + ansis.black.bgGreen(` Extract Resource `)} in ` + | ||
`${ansis.green(issuerFile)}\n` + | ||
` - source: ${ansis.cyan(sourceFile)}\n` + | ||
` - output: ${ansis.cyanBright(assetFile)}\n` | ||
` source: ${ansis.cyan(sourceFile)}\n` + | ||
//` output: ${ansis.cyanBright(assetFile)}\n` | ||
` output path: ${ansis.cyanBright(outputPath)}\n` + | ||
` asset: ${ansis.cyanBright(assetFile)}\n` | ||
); | ||
@@ -829,0 +1162,0 @@ } |
@@ -13,4 +13,5 @@ const path = require('path'); | ||
* @param {FileSystem} fs | ||
* @param {Object} options The webpack options. | ||
*/ | ||
init(fs) { | ||
init(fs, options) { | ||
this.fs = fs; | ||
@@ -85,2 +86,3 @@ }, | ||
const dependency = resolveData.dependencies[0]; | ||
// TODO: use ModuleGraph.getParentModule(dependency); | ||
@@ -90,3 +92,3 @@ const parentModule = dependency._parentModule || {}; | ||
const snapshot = buildInfo.snapshot || {}; | ||
const issuers = snapshot.fileTimestamps; | ||
const issuers = snapshot.fileTimestamps || snapshot.fileTshs; | ||
/** @type {string} closest issuer that can import the resource */ | ||
@@ -111,11 +113,2 @@ const closestIssuer = issuers != null && issuers.size > 0 ? Array.from(issuers.keys()).pop() : null; | ||
if (resolvedFile != null) { | ||
// TODO: delete commented code in next version | ||
// let relativeFile = path.relative(resolveData.context, resolvedFile); | ||
// if (relativeFile[0] !== '.') { | ||
// relativeFile = './' + relativeFile; | ||
// } | ||
// resolveData.request = relativeFile; | ||
// dependency.request = relativeFile; | ||
// dependency.userRequest = relativeFile; | ||
resolveData.request = resolvedFile; | ||
@@ -132,4 +125,2 @@ dependency.request = resolvedFile; | ||
* Resolve required resources. | ||
* | ||
* @type {{addResolvedPath(string): void, init({publicPath: string}): void, resolveAsset(string): (string|null), webpackOptionsResolve: {}, addToChunkCache({}, string): void, chunkCache: {paths: Set<any>, files: Map<any, any>}, getId(string, string): symbol, require(string): string, clearCache(): void, publicPath: string, addResolvedFile(string, string, string): void, clearChunkCache(): void, context: string, setCurrentContext(string): void, globalCache: {paths: Set<any>, files: Map<any, any>}}} | ||
*/ | ||
@@ -174,2 +165,9 @@ const resourceResolver = { | ||
/** | ||
* @type {Map} | ||
* key is rawRequest | ||
* value is list of contexts | ||
*/ | ||
moduleCache: new Map(), | ||
/** | ||
* @param {string} publicPath | ||
@@ -179,2 +177,4 @@ */ | ||
this.publicPath = publicPath; | ||
// clean cache for multiple calling of webpack.run(), e.g. by tests, webpack watch or webpack serve | ||
this.clearChunkCache(); | ||
}, | ||
@@ -212,9 +212,13 @@ | ||
this.chunkCache.paths.clear(); | ||
this.moduleCache.clear(); | ||
}, | ||
/** | ||
* @param {string} context | ||
* Set the current source file, which is the issuer for assets when compiling the source code. | ||
* | ||
* @param {string} issuer | ||
*/ | ||
setCurrentContext(context) { | ||
this.context = context; | ||
setIssuer(issuer) { | ||
this.issuer = issuer; | ||
this.context = path.dirname(issuer); | ||
}, | ||
@@ -225,9 +229,9 @@ | ||
* | ||
* @param {{}} module The chunk module. | ||
* @param {Module} module The webpack chunk module. | ||
* @param {string} assetFile The web path of the asset. | ||
*/ | ||
addToChunkCache(module, assetFile) { | ||
const request = module.rawRequest; | ||
const resourceContext = module.resourceResolveData.context; | ||
const context = resourceContext.issuer ? path.dirname(resourceContext.issuer) : module.context; | ||
const request = module.rawRequest; | ||
@@ -240,2 +244,62 @@ const assetId = this.getId(context, request); | ||
/** | ||
* Add the context and resolved path of the resource to resolve it in require() at render time. | ||
* | ||
* @param {string} context The context directory of required resource. | ||
* @param {string} request The raw request. | ||
* @param {string} assetFile The web path of the asset. | ||
*/ | ||
addToChunkCache2(context, request, assetFile) { | ||
// Note: when the same resource module is used multiple times, | ||
// only the first module is stored in the chunk, | ||
// so using the issuer of the resource module is not reliable. | ||
// The true issuer is the last used parent module of this resource module. | ||
const assetId = this.getId(context, request); | ||
this.chunkCache.files.set(assetId, assetFile); | ||
this.chunkCache.paths.add(context); | ||
}, | ||
/** | ||
* @param {string} resource The full path of source resource. | ||
* @param {string} rawRequest The raw request of resource is argument of URL() in css. | ||
* @param {string} issuer The parent file of the resource. | ||
*/ | ||
addToModuleCache(resource, rawRequest, issuer) { | ||
if (!this.moduleCache.has(resource)) { | ||
this.moduleCache.set(resource, { | ||
issuers: [], | ||
rawRequest, | ||
assetFile: undefined, | ||
}); | ||
} | ||
this.moduleCache.get(resource).issuers.push(issuer); | ||
}, | ||
/** | ||
* @param {string} resource The full path of source resource. | ||
* @param {string} assetFile The output asset filename. | ||
*/ | ||
setAssetFileInModuleCache(resource, assetFile) { | ||
if (!this.moduleCache.has(resource)) return; | ||
this.moduleCache.get(resource).assetFile = assetFile; | ||
}, | ||
/** | ||
* @param {string} rawRequest The raw request of resource is argument of URL() in css. | ||
* @param {string} issuer The parent file of the resource. | ||
* @return {null|string|*} | ||
*/ | ||
findAssetFileInModuleCache(rawRequest, issuer) { | ||
for (const item of this.moduleCache.values()) { | ||
if (item.rawRequest === rawRequest && item.issuers.indexOf(issuer) >= 0) { | ||
return item.assetFile; | ||
} | ||
} | ||
return null; | ||
}, | ||
/** | ||
* Add resolved path to global cache. | ||
@@ -271,19 +335,42 @@ * | ||
resolveAsset(file) { | ||
const { issuer, context } = this; | ||
let dir, assetId, assetFile; | ||
// firstly try to find an asset in resolved directories of the chunk | ||
// this is mostly used case by resolve a required resource in pug | ||
// console.log( | ||
// '\n ====> RESOLVE:', | ||
// file, | ||
// '\n - issuer: ', | ||
// issuer, | ||
// '\n - context: ', | ||
// context, | ||
// '\n', | ||
// this.chunkCache.files, | ||
// this.chunkCache.paths, | ||
// this.moduleCache, | ||
// this.globalCache.paths | ||
// ); | ||
// try to resolve a resource required in pug by absolute path of source file | ||
if (path.isAbsolute(file)) { | ||
assetId = this.getId('', file); | ||
assetFile = this.chunkCache.files.get(assetId); | ||
if (assetFile != null) return assetFile; | ||
} | ||
// try to resolve a resource required in pug relative by context directory | ||
for (dir of this.chunkCache.paths) { | ||
if (dir.indexOf(context) < 0) continue; | ||
assetId = this.getId(dir, file); | ||
assetFile = this.chunkCache.files.get(assetId); | ||
if (assetFile != null) { | ||
return assetFile; | ||
} | ||
if (assetFile != null) return assetFile; | ||
} | ||
// secondly try to find an asset in resolved directories of global cache | ||
// this case can be by resolve a resource from url used in css | ||
// try to resolve ta resource required via url in css | ||
assetFile = this.findAssetFileInModuleCache(file, issuer); | ||
if (assetFile != null) return assetFile; | ||
// try to resolve a resource imported from `node_modules` via url in css | ||
for (dir of this.globalCache.paths) { | ||
assetId = this.getId(dir, file); | ||
assetFile = this.chunkCache.files.get(assetId); | ||
const fullPath = path.resolve(dir, file); | ||
assetFile = this.findAssetFileInModuleCache(fullPath, issuer); | ||
if (assetFile != null) { | ||
@@ -294,3 +381,3 @@ return assetFile; | ||
// at the end try to resolve the full path of file and then try to resolve an asset by this full path | ||
// try to resolve the full path of the file and then try to resolve an asset by resolved file | ||
// this case can be by usage an alias retrieved using webpack resolve.plugins | ||
@@ -307,39 +394,54 @@ const resolvedFile = this.globalCache.files.get(file); | ||
/** | ||
* Require the resource file in the compiled pug or css. | ||
* Require the resource request in the compiled pug or css. | ||
* | ||
* @param {string} file The required file from source directory. | ||
* @param {string} request The request of source resource. | ||
* @returns {string} The output asset filename generated by filename template. | ||
* @throws {Error} | ||
*/ | ||
require(file) { | ||
require(request) { | ||
const self = resourceResolver; | ||
// bypass the inline data-url, e.g.: data:image/png;base64 | ||
if (file.startsWith('data:')) return file; | ||
if (request.startsWith('data:')) return request; | ||
// @import CSS rule is not supported. | ||
if (file.indexOf('??ruleSet') > 0) resolveException(file); | ||
if (request.indexOf('??ruleSet') > 0) resolveException(request); | ||
const assetFile = self.resolveAsset(file); | ||
// require resources | ||
const assetFile = self.resolveAsset(request); | ||
if (assetFile) { | ||
if (assetFile === 'img/1033740.9f92ec83-320w.webp') { | ||
console.log('\n --- RESOLVED assetFile: ', assetFile); | ||
// return { | ||
// src: 'img/favicon.2ff47510-300w.webp', | ||
// srcSet: 'img/favicon.2ff47510-300w.webp 300w', | ||
// }; | ||
} | ||
if (assetFile) { | ||
//console.log('\n *** REQUIRE assetFile: ', file, assetFile); | ||
return path.posix.join(self.publicPath, assetFile); | ||
// if (typeof assetFile !== 'string') { | ||
// console.log('\n --- RESOLVED assetFile: ', this.issuer, assetFile); | ||
// //return "module.exports = { src: '### SRC ###', srcSet: '### srcSet ###' };"; | ||
// return { | ||
// src: 'img/favicon.2ff47510-300w.webp', | ||
// srcSet: 'img/favicon.2ff47510-300w.webp 300w', | ||
// }; | ||
// } | ||
//return typeof assetFile === 'string' ? path.posix.join(self.publicPath, assetFile) : assetFile; | ||
return assetFile; | ||
} | ||
// require script in tag <script src=require('./main.js')> | ||
const scriptResource = self.scripts.getResource(file); | ||
if (scriptResource != null) { | ||
//console.log('\n *** REQUIRE script: ', file, '\n', scriptResource, self.scripts); | ||
return scriptResource; | ||
} | ||
const file = self.scripts.getResource(request); | ||
if (file != null) return file; | ||
// require only js code or json data | ||
if (/\.js[a-z0-9]*$/i.test(file)) { | ||
const fullPath = path.resolve(self.context, file); | ||
//console.log('\n *** REQUIRE JS: ', file, fullPath); | ||
if (/\.js[a-z0-9]*$/i.test(request)) { | ||
const fullPath = path.resolve(self.context, request); | ||
return require(fullPath); | ||
} | ||
resolveException(file); | ||
//return request; | ||
resolveException(request, self.issuer); | ||
}, | ||
@@ -346,0 +448,0 @@ }; |
@@ -14,2 +14,11 @@ /** | ||
/** | ||
* @param {string} request | ||
* @return {{resource: string, query: string|null}} | ||
*/ | ||
const parseRequest = (request) => { | ||
const [resource, query] = request.split('?'); | ||
return { resource, query }; | ||
}; | ||
const isFunction = (value) => typeof value === 'function'; | ||
@@ -41,2 +50,3 @@ | ||
pathToPosix, | ||
parseRequest, | ||
isFunction, | ||
@@ -43,0 +53,0 @@ shallowEqual, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
100007
1670
936
1
1