pug-plugin
Advanced tools
# Change log | ||
## 1.3.0 (2022-02-07) | ||
- added extraction of source map for CSS in separate file | ||
- fix not extracted source map for CSS in node <=14 | ||
- replace console.log with process.stdout by output in terminal | ||
- update the pug-loader to new version | ||
- update readme: remove unsupported substitutions `[base]` `[path]` `[ext]` by the option filename | ||
## 1.2.5 (2022-01-31) | ||
@@ -4,0 +11,0 @@ - update the pug-loader to the latest version supported the `htmlWebpackPlugin.options` in pug template |
{ | ||
"name": "pug-plugin", | ||
"version": "1.2.5", | ||
"version": "1.3.0-beta.0", | ||
"description": "The pug plugin to handle the pug, html, css, scss files in webpack entry and extract css from pug.", | ||
@@ -60,3 +60,3 @@ "keywords": [ | ||
"dependencies": { | ||
"@webdiscus/pug-loader": "^1.6.4", | ||
"@webdiscus/pug-loader": "^1.7.0-beta.0", | ||
"ansis": "1.3.4", | ||
@@ -66,16 +66,20 @@ "webpack-merge": "^5.8.0" | ||
"devDependencies": { | ||
"@babel/core": "^7.16.12", | ||
"@babel/core": "^7.17.0", | ||
"@babel/preset-env": "^7.16.11", | ||
"@types/jest": "^27.4.0", | ||
"css-loader": "^6.5.1", | ||
"autoprefixer": "^10.4.2", | ||
"css-loader": "^6.6.0", | ||
"cssnano": "^5.0.17", | ||
"html-loader": "^3.1.0", | ||
"jest": "^27.4.7", | ||
"jest": "^27.5.0", | ||
"mini-css-extract-plugin": "^2.5.3", | ||
"postcss-loader": "^6.2.1", | ||
"prettier": "^2.5.1", | ||
"mini-css-extract-plugin": "^2.5.3", | ||
"rimraf": "^3.0.2", | ||
"sass": "^1.49.0", | ||
"sass": "^1.49.7", | ||
"sass-loader": "^12.4.0", | ||
"style-loader": "^3.3.1", | ||
"webpack": "^5.67.0" | ||
"tsconfig-paths-webpack-plugin": "^3.5.2", | ||
"webpack": "^5.68.0" | ||
} | ||
} |
@@ -288,10 +288,6 @@ <div align="center"> | ||
Type: `string | Function` Default: `webpack.output.filename || '[name].html'`<br> | ||
The file name of output file. | ||
- If type is `string` then following substitutions are available in template strings | ||
The name of output file. | ||
- If type is `string` then following substitutions (see [output.filename](https://webpack.js.org/configuration/output/#outputfilename) for chunk-level) are available in template string: | ||
- `[id]` The ID of the chunk. | ||
- `[name]` Only filename without extension or path. | ||
- `[base]` Filename with extension. | ||
- `[path]` Only path, without filename. | ||
- `[path][name]` The path and filename without extension. | ||
- `[ext]` Extension with leading `.`. | ||
- `[id]` The ID of the chunk. | ||
- `[contenthash]` The hash of the content. | ||
@@ -426,3 +422,3 @@ - `[contenthash:nn]` The `nn` is the length of hashes (defaults to 20). | ||
<a id="require-style" name="require-style" href="#require-style"></a> | ||
### Extract CSS via `require` in pug | ||
### Extract CSS from SASS via `require` in pug | ||
@@ -505,4 +501,4 @@ Dependencies: | ||
## ! ACHTUNG ! ATTENTION ! | ||
### STOP import styles in JavaScript! This is very BAD praxis. | ||
> ⚠️ Don't do that in JavaScript: `import ./src/styles.scss`. This is popular but **_dirty way_**. | ||
### Don't import styles in JavaScript! | ||
> ❌ BAD practice: `import ./src/styles.scss` is a popular but **_dirty way_**. | ||
@@ -516,4 +512,4 @@ ### Clarification | ||
> ### Correct ways | ||
> - add a source style file directly in pug via `require`, like `link(href=require('~Styles/styles.scss'))` | ||
> ### ✅ Correct ways | ||
> - add a source style file directly in pug via `require`, e.g. `link(href=require('~Styles/styles.scss'))` | ||
> - add a compiled css file directly in pug, like `link(href='/assets/styles.css')` and add the source of the style in webpack entry.\ | ||
@@ -586,2 +582,6 @@ > Yes, in this case may be needed additional assets manifest plugin to replace original filename with hashed name. But this is the right way.\ | ||
> ⚠️ **Limitation for CSS**\ | ||
> The `@import` CSS rule is not supported. | ||
> This is a [BAD practice](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/page-speed-rules-and-recommendations?hl=en#avoid_css_imports), avoid CSS imports. | ||
> Use any CSS preprocessor like the Sass to create a style bundle using the preprocessor import. | ||
--- | ||
@@ -588,0 +588,0 @@ |
@@ -76,3 +76,3 @@ const fs = require('fs'); | ||
message += | ||
`${ansis.yellow('\nPossible reason:')} to require a style in pug, ` + | ||
`\n${ansis.yellow('Possible reason:')} to require a style in pug, ` + | ||
`add to webpack module.rules for this style the ${ansis.magenta( | ||
@@ -83,2 +83,4 @@ `type: 'asset/resource'` | ||
} | ||
} else if (/\.css$/.test(file) && file.indexOf('??ruleSet')) { | ||
message += `\nThe ${ansis.yellow('@import CSS rule')} is not supported. Avoid CSS imports!`; | ||
} | ||
@@ -114,5 +116,2 @@ | ||
PugPluginError(message, error); | ||
ansis.red.bgAnsi256(123)('dd'); | ||
ansis.black.italic.open; | ||
}; | ||
@@ -119,0 +118,0 @@ |
121
src/index.js
@@ -6,3 +6,3 @@ const vm = require('vm'); | ||
const { plugin, isWin } = require('./config'); | ||
const { isFunction, resource, pathToPosix } = require('./utils'); | ||
const { isFunction, resource, pathToPosix, outToConsole } = require('./utils'); | ||
const { extractHtml, extractCss } = require('./modules'); | ||
@@ -32,3 +32,4 @@ | ||
* Must be an absolute or a relative by the context path. | ||
* @property {function(string, EntryAssetInfo, Compilation): string | null} [postprocess = null] The post process for extracted content from entry. | ||
* @property {function(string, EntryAssetInfo, Compilation): string | null =} postprocess The post process for extracted content from entry. | ||
* @property {function(): string | null =} extract | ||
* @property {boolean} [verbose = false] Show the information at processing entry files. | ||
@@ -59,2 +60,3 @@ */ | ||
* @property {function(string, AssetInfo, Compilation): string} [postprocess = null] The post process for extracted content from entry. | ||
* @property {function(): string | null =} extract | ||
* @property {Array} resources | ||
@@ -128,2 +130,5 @@ * @property {boolean} [verbose = false] Show an information by handles of the entry in a postprocess. | ||
if (webpackOutputPublicPath == null || webpackOutputPublicPath === 'auto') publicPathException(); | ||
// initialize the resource module | ||
resource.init(__dirname, compiler.options.resolve || {}); | ||
resource.publicPath = webpackOutputPublicPath; | ||
@@ -148,2 +153,3 @@ | ||
postprocess, | ||
extract, | ||
verbose, | ||
@@ -164,2 +170,3 @@ } = this.options; | ||
if (module.postprocess) postprocess = module.postprocess; | ||
if (module.extract) extract = module.extract; | ||
} | ||
@@ -186,2 +193,3 @@ | ||
postprocess: isFunction(postprocess) ? postprocess : null, | ||
extract: isFunction(extract) ? extract : null, | ||
verbose, | ||
@@ -191,12 +199,10 @@ }; | ||
entry.filename = (pathData, assetInfo) => { | ||
// define lazy memoized getter for the property `filename`, it will be generated later | ||
if (!assetEntry.filename) { | ||
Object.defineProperty(assetEntry, 'filename', { | ||
get() { | ||
const filename = pathData.chunk.files.values().next().value; | ||
set(filename) { | ||
// replace the setter with value of resolved filename | ||
delete this.filename; | ||
this.file = path.join(this.outputPath, filename); | ||
this.assetFile = path.relative(webpackOutputPath, this.file); | ||
delete this.filename; | ||
return (this.filename = filename); | ||
this.filename = filename; | ||
}, | ||
@@ -215,26 +221,38 @@ }); | ||
const verbose = this.verbose; | ||
this.compilation = compilation; | ||
// render source code | ||
compilation.hooks.renderManifest.tap(plugin, (result, { chunk }) => { | ||
if (chunk instanceof compiler.webpack.HotUpdateChunk) return; | ||
const { chunkGraph } = compilation; | ||
const { HotUpdateChunk } = compiler.webpack; | ||
const filenameTemplate = chunk.filenameTemplate; | ||
let compiledResult = '', | ||
const entry = this.getEntryByName(chunk.name); | ||
let contentHashType = 'javascript', | ||
compiledResult = '', | ||
source; | ||
// don't hot update chunks | ||
if (chunk instanceof HotUpdateChunk) return; | ||
const entry = this.getEntryByName(chunk.name); | ||
// process only entries supported by this plugin | ||
if (!entry) return; | ||
entry.filename = compilation.getPath(filenameTemplate, { contentHashType, chunk }); | ||
resource.files = {}; | ||
for (const module of chunkGraph.getChunkModules(chunk)) { | ||
if (!module.resource) continue; | ||
if (this.isEntryModule(module) && chunkGraph.isEntryModuleInChunk(module, chunk)) { | ||
source = module.originalSource().source().toString(); | ||
} else if (module.type === 'asset/resource') { | ||
//const readableIdentifier = module.readableIdentifier(compilation.runtimeTemplate.requestShortener); | ||
const context = path.dirname(entry.importFile); | ||
const sourceFile = isWin ? pathToPosix(module.resource) : module.resource; | ||
const assetFile = module.buildInfo.filename; | ||
resource.files[sourceFile] = assetFile; | ||
const assetId = resource.getKey(context, module.rawRequest); | ||
const assetId2 = resource.getKey(context, sourceFile); | ||
// for resolve the asset file by full path or rawRequest, both variants are possible in source code | ||
resource.files[assetId] = assetFile; | ||
resource.files[assetId2] = assetFile; | ||
this.entryAssets.push({ | ||
@@ -250,3 +268,4 @@ entryFile: entry.importFile, | ||
// note: by any error in webpack config the source is empty | ||
compiledResult = this.extract(source, entry.importFile); | ||
compiledResult = this.compileSource(source, entry.importFile, entry.assetFile, entry.extract); | ||
//console.log(' ---> compiledResult', compiledResult); | ||
} | ||
@@ -257,15 +276,32 @@ | ||
filenameTemplate, | ||
pathOptions: { chunk, contentHashType: 'javascript' }, | ||
pathOptions: { | ||
chunk, | ||
contentHashType, | ||
}, | ||
identifier: `${plugin}.${chunk.id}`, | ||
hash: chunk.contentHash['javascript'], | ||
hash: chunk.contentHash[contentHashType], | ||
}); | ||
}); | ||
// compilation.hooks.chunkAsset.tap(plugin, (chunk, filename) => { | ||
// // TODO for next release | ||
// // - collect entry files for manifest to replace original name with hashed | ||
// const asset = compilation.getAsset(filename); | ||
// const assetInfo = compilation.assetsInfo.get(filename) || {}; | ||
// }); | ||
//compilation.hooks.chunkAsset.tap(plugin, (chunk, filename) => { | ||
// TODO for next release | ||
// - collect entry files for manifest to replace original name with hashed | ||
//const asset = compilation.getAsset(filename); | ||
//const assetInfo = compilation.assetsInfo.get(filename) || {}; | ||
//compilation.assetsInfo.delete(file); | ||
//const file = chunk.files.values().next().value; | ||
//const auxiliaryFile = chunk.auxiliaryFiles.values().next().value; | ||
//if (auxiliaryFile && auxiliaryFile.endsWith('.css') && file.endsWith('.css')) { | ||
//compilation.deleteAsset(filename); | ||
// TODO: this module is not in entry and was required | ||
//} | ||
//}); | ||
//compilation.hooks.buildModule.tap(plugin, (module) => { | ||
// if (/.(pug|jade)$/i.test(module.resourceResolveData.context.issuer)) { | ||
// // TODO: this module is not in entry and was required | ||
// } | ||
//}); | ||
// only here can be an asset deleted or emitted | ||
@@ -287,3 +323,2 @@ compilation.hooks.processAssets.tap( | ||
// the asset defined in webpack entry | ||
if (verbose) this.verboseEntry(entry); | ||
assetFile = entry.assetFile; | ||
@@ -293,2 +328,3 @@ postprocess = entry.postprocess; | ||
result = assets[filename].source(); | ||
if (verbose) this.verboseEntry(entry); | ||
} else { | ||
@@ -302,3 +338,3 @@ // the asset required in pug | ||
if (!entryAsset) { | ||
// remove double assets from webpack entry processed via `asset/resource` | ||
// remove double assets from webpack entry and processed via module `asset/resource` e.g. using require | ||
compilation.deleteAsset(filename); | ||
@@ -308,5 +344,6 @@ continue; | ||
postprocess = module.postprocess; | ||
assetFile = filename; | ||
source = assets[filename].source().toString(); | ||
postprocess = module.postprocess; | ||
result = this.compileSource(source, entryAsset.sourceFile, assetFile, module.extract); | ||
@@ -317,3 +354,2 @@ if (verbose) { | ||
} | ||
result = this.extract(source, entryAsset.sourceFile); | ||
} | ||
@@ -339,3 +375,3 @@ | ||
if (result != null) { | ||
// remove source asset to avoid creating needles js files | ||
// remove source asset to avoid creating needles js file | ||
compilation.deleteAsset(filename); | ||
@@ -351,9 +387,11 @@ compilation.emitAsset(assetFile, new RawSource(result, false)); | ||
/** | ||
* Extract the content from source generated by loaders such as `css-loader`, `html-loader`. | ||
* Compile the source generated by loaders such as `css-loader`, `html-loader`. | ||
* | ||
* @param {string} source The source generated by `css-loader`. | ||
* @param {string} sourceFile The full path of source file. | ||
* @returns {string} | ||
* @param {string} assetFile | ||
* @param {function} extract | ||
* @return {Buffer} | ||
*/ | ||
extract(source, sourceFile) { | ||
compileSource(source, sourceFile, assetFile, extract) { | ||
resource.context = path.dirname(sourceFile); | ||
@@ -365,3 +403,3 @@ source = this.toCommonJS(source); | ||
require: resource.require, | ||
// the `module.id` is required for `css-loader` | ||
// the `module.id` is required for `css-loader`, in module extractCss expected as source path | ||
module: { id: sourceFile }, | ||
@@ -381,5 +419,7 @@ }); | ||
// generated result of the `css-loader` has the own method `toString()` to concatenate code strings | ||
// see node_modules/css-loader/dist/runtime/sourceMaps.js | ||
return result.toString(); | ||
if (extract) { | ||
return extract(result, assetFile, this.compilation); | ||
} | ||
return result; | ||
} | ||
@@ -471,5 +511,3 @@ | ||
if (!entry) return; | ||
if (!this.hasVerboseOut) console.log('\n'); | ||
this.hasVerboseOut = true; | ||
console.log( | ||
outToConsole( | ||
`${ansis.black.bgYellow(`[${plugin}]`)} Compile the entry ${ansis.green(entry.name)}\n` + | ||
@@ -492,5 +530,3 @@ ` - filename: ${ | ||
verboseExtractAsset({ entryFile, sourceFile, assetFile }) { | ||
if (!this.hasVerboseOut) console.log('\n'); | ||
this.hasVerboseOut = true; | ||
console.log( | ||
outToConsole( | ||
`${ansis.black.bgYellow(`[${plugin}]`)} Extract the asset from ${ansis.green(entryFile)}\n` + | ||
@@ -507,1 +543,2 @@ ` - source: ${ansis.cyan(sourceFile)}\n` + | ||
module.exports.loader = require.resolve('@webdiscus/pug-loader'); | ||
//module.exports.loader = require.resolve('../../pug-loader'); // local dev only |
const { merge } = require('webpack-merge'); | ||
const { plugin } = require('./utils'); | ||
const { plugin } = require('./config'); | ||
const ansis = require('ansis'); | ||
const path = require('path'); | ||
const { outToConsole } = require('./utils'); | ||
@@ -13,3 +15,3 @@ /** | ||
*/ | ||
const extractHtml = (options = {}) => { | ||
const extractHtml = function (options = {}) { | ||
const defaultOptions = { | ||
@@ -45,7 +47,12 @@ test: /\.(html)$/, | ||
/** | ||
* The lightweight plugin module to extract the css from asset content. | ||
* todo test cases: | ||
* The plugin module to extract the css and source map from asset. | ||
* @note If the webpack.mode is `production` then `css-loader` minify the css self, | ||
* if is `development` then css is pretty formatted. | ||
* | ||
* TODO test: | ||
* - add supports a chunk name template with [id] | ||
* - https://github.com/webpack-contrib/mini-css-extract-plugin/tree/master/test/manual/src | ||
* - multiple entries, https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/test/cases/at-import-in-the-entry/webpack.config.js | ||
* TODO feature: | ||
* - add option `chunkFilename` https://webpack.js.org/configuration/output/#outputchunkfilename | ||
* | ||
@@ -55,4 +62,4 @@ * @param {ModuleOptions} options The custom options. | ||
*/ | ||
const extractCss = (options = {}) => { | ||
const defaultOptions = { | ||
const extractCss = function (options = {}) { | ||
this.options = { | ||
test: /\.(css|sass|scss)$/, | ||
@@ -66,2 +73,43 @@ enabled: true, | ||
/** | ||
* @param {array} sourceMaps | ||
* @param {string} assetFile | ||
* @param {Compilation} compilation | ||
* @returns {string} | ||
* @private | ||
*/ | ||
extract: (sourceMaps, assetFile, compilation) => { | ||
// only one css module per css asset file, because the @import in CSS is not supported | ||
const [item] = sourceMaps; | ||
const [sourceFile, cssCode, media, cssMapping, supports, layer] = item; | ||
let sourceMappingURL = '', | ||
mapFile; | ||
if (cssMapping) { | ||
const RawSource = compilation.compiler.webpack.sources.RawSource; | ||
const sourceMapping = new RawSource(JSON.stringify(cssMapping)); | ||
mapFile = assetFile + '.map'; | ||
sourceMappingURL = `\n/*# sourceMappingURL=${path.basename(mapFile)} */`; | ||
compilation.emitAsset(mapFile, sourceMapping); | ||
} | ||
if (this.options.verbose) { | ||
let verbose = | ||
ansis.black.bgYellow(`[${plugin}]`) + | ||
ansis.black.bgGreen(` Extract CSS `) + | ||
' from ' + | ||
ansis.cyan(sourceFile) + | ||
`\n` + | ||
` - ${ansis.magenta(assetFile)}\n`; | ||
if (mapFile) verbose += ` - ${ansis.magenta(mapFile)}\n`; | ||
outToConsole(verbose); | ||
} | ||
return cssCode + sourceMappingURL; | ||
}, | ||
/** | ||
* The post process for extracted CSS content. | ||
* This method can be overridden in module options. | ||
* | ||
* @param {string} content The css content generated by css-loader. | ||
@@ -73,7 +121,3 @@ * @param {EntryAssetInfo} info | ||
postprocess: (content, info, compilation) => { | ||
if (info.verbose) { | ||
console.log( | ||
ansis.black.bgYellow(`[${plugin}]`) + ansis.black.bgGreen(` Extract CSS `) + ansis.cyan(' ' + info.entryFile) | ||
); | ||
} | ||
// TODO user can handle here the pure CSS content | ||
return content; | ||
@@ -83,3 +127,5 @@ }, | ||
return merge(defaultOptions, options); | ||
this.options = merge(this.options, options); | ||
return this.options; | ||
}; | ||
@@ -86,0 +132,0 @@ |
@@ -0,4 +1,34 @@ | ||
// the 'enhanced-resolve' package already used in webpack, don't need to define it in package.json | ||
const ResolverFactory = require('enhanced-resolve'); | ||
const path = require('path'); | ||
const { resolveException } = require('./exceptions'); | ||
/** | ||
* @param {string} path The start path to resolve. | ||
* @param {{}} options The enhanced-resolve options. | ||
* @returns {function(context:string, request:string): string | false} | ||
*/ | ||
const getFileResolverSync = (path, options) => { | ||
const resolve = ResolverFactory.create.sync({ | ||
...options, | ||
aliasFields: [], | ||
conditionNames: [], | ||
descriptionFiles: [], | ||
exportsFields: [], | ||
mainFields: [], | ||
modules: [], | ||
mainFiles: [], | ||
extensions: [], | ||
preferRelative: true, | ||
}); | ||
return (context, request) => { | ||
if (!request) request = context; | ||
else path = context; | ||
return resolve(path, request); | ||
}; | ||
}; | ||
let resolveFile = null; | ||
const isFunction = (value) => typeof value === 'function'; | ||
@@ -20,2 +50,4 @@ | ||
const resource = { | ||
webpackOptionsResolve: {}, | ||
/** | ||
@@ -36,3 +68,19 @@ * @type {string} The context directory to require the file. | ||
init: (rootContext, resolveOptions) => { | ||
resolveFile = getFileResolverSync(rootContext, resolveOptions); | ||
}, | ||
/** | ||
* Get the key of asset for get/set it into/from cache. | ||
* | ||
* @note Very important to normalize the file. | ||
* The file can contain `path/to/../to/file` that is not equal to `path/to/file` for same file. | ||
* | ||
* @param {string} context | ||
* @param {string} file | ||
* @returns {symbol} | ||
*/ | ||
getKey: (context, file) => Symbol.for(context + '__' + path.join(file)), | ||
/** | ||
* Require the resource file from source in pug template. | ||
@@ -46,3 +94,5 @@ * | ||
const self = resource; | ||
const assetFile = self.files[file]; | ||
const context = self.context; | ||
const assetId = self.getKey(context, file); | ||
const assetFile = self.files[assetId]; | ||
@@ -54,10 +104,20 @@ if (assetFile) { | ||
// require only js code or json data | ||
const context = self.context || __dirname; | ||
const fullPath = require.resolve(file, { paths: [context] }); | ||
// TODO remove in next version | ||
//const fullPath = path.isAbsolute(file) ? file : path.join(context, file); | ||
return require(fullPath); | ||
} | ||
// fallback for `compile` method to try to resolve alias from resolve.plugins | ||
try { | ||
const resolvedFile = resolveFile(context, file); | ||
const assetId = self.getKey(context, resolvedFile); | ||
const assetFile = self.files[assetId]; | ||
if (assetFile) { | ||
// resolve web path of processed asset filename | ||
return path.posix.join(self.publicPath, assetFile); | ||
} | ||
} catch (error) { | ||
resolveException(file); | ||
} | ||
resolveException(file); | ||
@@ -87,2 +147,4 @@ }, | ||
const outToConsole = (...args) => process.stdout.write(args.join(' ') + '\n'); | ||
module.exports = { | ||
@@ -93,2 +155,3 @@ pathToPosix, | ||
shallowEqual, | ||
outToConsole, | ||
}; |
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
62898
10.24%812
18.2%17
30.77%1
Infinity%1
Infinity%