jantimon / html-webpack-plugin

Simplifies creation of HTML files to serve your webpack bundles
MIT License
10.71k stars 1.31k forks source link

templateContent: template argument contains loader path #1139

Closed cosmoKenney closed 5 years ago

cosmoKenney commented 5 years ago

Expected behaviour

The options.templateContent function should receive a template path. In arguments[0].htmlWebpackPlugin.options.template

Current behaviour

options.templateContent function receives a "loader" prefixed template path. In arguments[0].htmlWebpackPlugin.options.template E.g.: C:\TestProjects\webpac-multi-entry-test\node_modules\html-webpack-plugin\lib\loader.js!C:\TestProjects\webpac-multi-entry-test\modules\module-one\module-one.html is what I received.

Environment

Node.js v10.12.0
win32 10.0.17134
npm --version: 6.4.1
npm ls webpack: 4.28.4
npm ls html-webpack-plugin: 4.0.0-beta.0

Config

const path = require( "path" );
const webpack = require( "webpack" );

const clientRoot = path.resolve( __dirname, "./" );
const tsConfig = path.resolve( __dirname, './tsconfig.json' );

const CleanWebpackPlugin = require( 'clean-webpack-plugin' );
const ExtractTextPlugin = require( "extract-text-webpack-plugin" );
const HtmlWebpackPlugin = require( "html-webpack-plugin" );
const HtmlWebpackHarddiskPlugin = require( 'html-webpack-harddisk-plugin' );

const extractStylesPlugin = new ExtractTextPlugin( "css/app.css" );
//const extractStylesVendorPlugin = new ExtractTextPlugin( "css/vendor.css" );
const templateProcessor = function() {
    var al = arguments.length;
    var a1 = arguments[0];
};
var webpackConfig = {
    mode: 'development',
    optimization: {
        splitChunks: {
            chunks: "all",
            cacheGroups: {
                common: {
                    test: /[\\/]scripts[\\/]/,
                    minSize: 0
                }
            }
        }
    },
    context: clientRoot,
    entry: {
        "index": "./modules/main/index",
        "module-one": "./modules/module-one/module-one"
    },
    output: {
        path: path.resolve( __dirname, "assets" ),
        filename: "js/[name].js",
        publicPath: "/assets"
    },
    resolve: { extensions: [ ".ts", ".js", ".css", ".scss", "*" ], modules: [ clientRoot, "node_modules" ], },
    module: {
        rules: [
            {
                test: /\.s?css$/, exclude: /node_modules\/.*\.s?css$/,
                use: extractStylesPlugin.extract( {  fallback: "style-loader",
                    use: [
                        { loader: "css-loader?sourceMap=true" },
                        { loader: "resolve-url-loader?sourceMap=true" },
                        { loader: "sass-loader?sourceMap=true" },
                    ]
                } )
            },
            { test: /\.js$/, loader: 'script-loader', exclude: /node_modules/ },
            { test: /\.ts$/, loader: 'ts-loader', exclude: /node_modules/ }
        ]
    },
    plugins: [
        new CleanWebpackPlugin( ['assets/*.*', 'assets/js/*.*', 'assets/img/*.*', 'assets/css/*.*'], { verbose: true } ),
        extractStylesPlugin,
        new HtmlWebpackPlugin( {
            alwaysWriteToDisk: true,
            chunks: ['module-one'],
            //excludeChunks: 'index',
            filename: "./modules/module-one/module-one.aspx",
            template: "./modules/module-one/module-one.aspx",
            templateContent: templateProcessor,
            inject: true, //'body',
            chunkSortMode: "dependency",
            hash: true
        } ),
        new HtmlWebpackHarddiskPlugin()
    ]
};
module.exports = webpackConfig;

The template is an aspx page that uses a master page, so there are no <head> or <body> tags:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Dashboard.aspx.cs"  MasterPageFile="~/StandardPage.Master" Inherits="MyNameSpace.Web.Dashboard" %>
<asp:Content ID="Content1" ContentPlaceHolderID="headStyleSection" runat="server">
    <!-- I NEED TO INSERT CSS link TAGS HERE -->
</asp:Content>
<asp:Content ID="Content6" ContentPlaceHolderID="footerPlaceHolder" runat="server">
    <!-- I NEED TO INSERT JS script TAGS HERE -->
</asp:Content>

Relevant Links

I have a stale question on SO that describes the issue I'm trying to solve here: https://stackoverflow.com/questions/54117089/use-html-webpack-plugin-to-inject-bundle-script-tags-into-aspcontent-tag-in-l

Additional context

Ultimately, all I need to do is inject my style and script bundles (via <link> and <script> tags) into my .aspx pages -- in place in the project folder structure. But the problem is that html-webpack-plugin requires an <head> and <body> tag for that. So, I thought that I could use a templateContent function to find a token within the correct <asp:Content> element and replace that token with the set of script tags. But I'm stuck on opening the aspx file that is mentioned in the template parameter. Is there as an actual way to load the loader prefixed template path into a string within the templateContnet function?

I will use regex to find the token (an html comment like <!--script-bundles--><!--/script-bundles-->) and wrap the style/script tags with them so they end up looking like this and the whole process will be repeatable on subsequent compiles: <!--script-bundles--><script src="script1.js"></script><!--/script-bundles-->

cosmoKenney commented 5 years ago

@jantimon, given the above, it would be very cool to be able to prove an object to the inject option that has two properties that accept regex:

        new HtmlWebpackPlugin( {
            template: "./modules/module-one/module-one.aspx",
            inject: {
                css: /<!--bundled-css-->[\s\S]*?<!--\/bundled-css-->/mgi,
                js: /<!--bundled-js-->[\s\S]*?<!--\/bundled-js-->/mgi,
            }
        } ),

... those would tell the plugin where to inject the css and js bundles.

jantimon commented 5 years ago

Yes that should be possible. One way would be to write a custom webpack loader which returns a function to generate the final output.

The loader and this function will receive all information necessary :)

In any case you should disable the inject feature which is not meant for custom templates without a head tor body tag

The new beta version (4.0.0) allows to output all ja script tags with a single line of code (same for css)

cosmoKenney commented 5 years ago

@jantimon Huh, that's interesting. Would the loader have access to each html-webpack-plugin instance configuration?

cosmoKenney commented 5 years ago

@jantimon going back to my original post in this thread, how do I load the template from within the templateContent function? I can't just pass the <loader>!<template path> to an fs instance, can I? The options.template property in the arguments to the templateContent function has this value in it: C:\TestProjects\webpac-multi-entry-test\node_modules\html-webpack-plugin\lib\loader.js!C:\TestProjects\webpac-multi-entry-test\modules\module-one\module-one.html so it's not a valid path.

cosmoKenney commented 5 years ago

@jantimon I tried to do the following, but your plugin is making the filename option a path relative to the webpack build output.path. So the the template output is not being written where I need it. I guess I am going to have to go back to the server-side-include hack and place all my styles and scripts into the <head>. Trial webpack.config.js:

const path = require( "path" );
const webpack = require( "webpack" );
const fs = require( "fs" );
const clientRoot = path.resolve( __dirname, "./" );
const tsConfig = path.resolve( __dirname, './tsconfig.json' );

const CleanWebpackPlugin = require( 'clean-webpack-plugin' );
const ExtractTextPlugin = require( "extract-text-webpack-plugin" );
const HtmlWebpackPlugin = require( "html-webpack-plugin" );
const HtmlWebpackHarddiskPlugin = require( 'html-webpack-harddisk-plugin' );

const extractStylesPlugin = new ExtractTextPlugin( "css/app.css" );
//const extractStylesVendorPlugin = new ExtractTextPlugin( "css/vendor.css" );

const cssRex = /<!--bundled-css-->[\s\S]*?<!--\/bundled-css-->/mgi;
const jsRex = /<!--bundled-js-->[\s\S]*?<!--\/bundled-js-->/mgi;
const templateProcessor = function() {
    let files =arguments[0].htmlWebpackPlugin.files;
    let options = arguments[0].htmlWebpackPlugin.options;
    let css = `<!--bundled-css-->
    `;
    let js = `<!--bundled-js-->
    `;
    for ( let c = 0 ; c < files.css.length; c++ )
    {
        let cssPath = files.css[c];
        css += `<link href="${cssPath}" rel="stylesheet" type="text/css" />
        `;
    }
    for ( let j = 0 ; j < files.js.length; j++ )
    {
        let jsPath = files.js[j];
        js += `<script type="text/javascript" src="${jsPath}"></script>
        `;
    }
    css += '<!--/bundled-css-->';
    js += '<!--/bundled-js-->';

    let tmpl = options.inplaceTemplate;
    let finPath = path.resolve( __dirname, tmpl );
    let fin = fs.readFileSync( finPath, 'utf8' );
    fin = fin.replace( cssRex, css ).replace( jsRex, js );
    return fin;
};

var webpackConfig = {
    mode: 'development',
    optimization: {
        splitChunks: {
            chunks: "all",
            cacheGroups: {
                common: {
                    test: /[\\/]scripts[\\/]/,
                    minSize: 0
                }
            }
        }
    },
    context: clientRoot,
    entry: {
        "index": "./modules/main/index",
        "module-one": "./modules/module-one/module-one"
    },
    output: {
        path: path.resolve( __dirname, "assets" ),
        filename: "js/[name].js",
        publicPath: "/assets"
        //pathinfo: true <-- defualts to true in dev/false in prod
    },
    resolve: {
        extensions: [ // finally figured this out. if you specify "extensions" you must include all that you wish to support
            ".ts", ".js",
            ".css", ".scss",
            "*" // allow extensions on imports
        ],
        modules: [
            clientRoot,
            "node_modules"
        ],
    },
    module: {
        rules: [
            // Extract our app's CSS into the bundle
            {
                test: /\.s?css$/,
                exclude: /node_modules\/.*\.s?css$/,
                use: extractStylesPlugin.extract( {
                    fallback: "style-loader",
                    //publicPath: "../",
                    use: [
                        { loader: "css-loader?sourceMap=true" },
                        { loader: "resolve-url-loader?sourceMap=true" },
                        { loader: "sass-loader?sourceMap=true" },
                    ]
                } )
            },

            {
                test: /\.js$/,
                loader: 'script-loader',
                exclude: /node_modules/
            },
            {
                test: /\.ts$/,
                loader: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin( ['assets/*.*', 'assets/js/*.*', 'assets/img/*.*', 'assets/css/*.*'], { verbose: true } ),
        extractStylesPlugin,
        //extractStylesVendorPlugin,
        new HtmlWebpackPlugin( {
            chunks: ['index'],
            alwaysWriteToDisk: true,
            filename: "./modules/main/index.html",
            inplaceTemplate: "./modules/main/index.html",
            templateContent: templateProcessor,
            inject: false,
            chunkSortMode: "dependency",
            hash: true
        } ),
        new HtmlWebpackPlugin( {
            alwaysWriteToDisk: true,
            chunks: ['module-one'],
            filename: "./modules/module-one/module-one.html",
            inplaceTemplate: "./modules/module-one/module-one.html",
            templateContent: templateProcessor,
            inject: false,
            chunkSortMode: "dependency",
            hash: true
        } ),
        new HtmlWebpackHarddiskPlugin()
    ]
};
module.exports = webpackConfig;
jantimon commented 5 years ago

The output path is the root of your web folder. You can use the copy plugin to move files to a different directory.

If you use the templateContent function you can’t access the source file and miss most of the caching features.

A loader like the following code will give you access to everything you need:

module.export = function(source, options) {
  return function(templateParameter) {
     console.log(source, options, templateParameter);
     return 'html code'
  }
}
cosmoKenney commented 5 years ago

@jantimon, thank for responding again. Your response made me realize I've been stubbornly keeping the project's "assets" folder as my output.path. But I could just as easily use ./. As for the custom loader, are you saying I should add the custom loader in module.rules as a loader for my .aspx files? And that I should transform the input file within the loader?

cosmoKenney commented 5 years ago

I tried doing this in my webpack config:

const aspxLoader = require( './aspx-loader' );
//... in module.rules:
            {
                test: /\.aspx$/,
                use: aspxLoader,
                exclude: /node_modules/
            }

I have a breakpoint set in aspx-loader.js but I don't get access to the htmlWebpackPlugin.files object there. So how access the .css and .js arrays? templateParameter is an object:

templateParameter = {resource: "C:\TestProjects\webpac-multi-entry-test\modules\main\index.aspx", realResource: "C:\TestProjects\webpac-multi-entry-test\modules\main\index.aspx", resourceQuery: "", issuer: "", compiler: "HtmlWebpackCompiler"}

My html-webpack-plugin configs look like this:

        new HtmlWebpackPlugin( {
            chunks: ['index'],
            alwaysWriteToDisk: true,
            filename: "./modules/main/index.html",
            template: "./modules/main/index.html",
            //inplaceTemplate: "./modules/main/index.html",
            //templateContent: templateProcessor,
            inject: false,
            chunkSortMode: "dependency",
            hash: true
        } ),

aspx-loader.js:

module.exports = function(source, options)
{
    return function (templateParameter)
    {
        console.log(source, options, templateParameter); 
        return 'html code'
    }
}
cosmoKenney commented 5 years ago

oops...

jantimon commented 5 years ago

Are you sure that's the template parameter?
It looks like a loader options object.

Also make sure that filename and template don't use the same value. template is input and filename is output.

You can also use a loader inline if you like to:

template: "!!" + require.resolve("./aspx-loader") + "!./modules/main/index.html",

cosmoKenney commented 5 years ago

@jantimon, yep, that is templateParameter, I copied and pasted directly from the debugger. Also, I pasted the config with '.html' extensions, but my actual config is using the '.aspx' extensions for template and filename. That one had my scratching my head for a while -- wondering why my loader wasn't getting called ;-)

BTW, I was reading up a little on webpack custom loader docs (https://webpack.js.org/contribute/writing-a-loader/) and AFAICT the first one in the chain gets called with a different parameter set. But i could be wrong about that.

As for the input/output (template/filename) I need both to be the same. I cannot maintain over fifty separate templates in the project, and none of the other developers will be able to figure out that they need to modify the contents of the templates and not the actual aspx pages. I know I am living in a very atypical use case here, but I'm not sure how to work around the issue.

What I might end up doing is going back to the original plan of having html-webpack-plugin write only the script and link tags to an otherwise empty output file. I think for every aspx page I can use two instances of html-webpack-plugin. One to create a file with only the link (styles) tags. And one to create a file with only the script tags. I can then server-side-include those files into my aspx pages in the appropriate location: styles in the header asp:Content tag, and scripts in the footer asp:Content tag.

I had planned to put a pair of comment tags in the two locations of each aspx page and using the custom loader or a templateContent function, to regex in the style/scripts along with the same pair of comment tags. That way the same file could be use as the template over and over, since the comment tags would stay there wrapping the style/scripts. In theory it should be possible.

Over time the project will get migrated to a different technology, or will be turned into a SPA. But for now I need to get rid of all the nuget script and style stuff and the custom minifier and bundler. The complexity is killing us.

One question, if I use a custom loader, will that stop html-webpack-plugin from processing the template with the default template processor?

cosmoKenney commented 5 years ago

I tried the inline loader but it crashes webpack. I've spent way too much time on this. I'm going back to the 2 SSI for each aspx file. I can at least get that working in 10 minutes. The custom loader is just too much to learn for this project.

cosmoKenney commented 5 years ago

Just updating this now that it's being closed so that it is on record that this was not a problem with the html-webpack-plugin. My problems were all based on trying to shoehorn webpack into a 15 year old asp.net webforms project with over 50 entry pages using master pages. Using two instances of the html-webpack-plugin to create a server-side-include file for each entry .aspx page solves my problem. One instance will generate the css (<link> tags) in one include file. And the other instance will create all the <script> tags in a separate file. It won't build fast, but I don't care at this point. I'm going to be refactoring each of these pages as they get touched in the future. So, thanks to @jantimon for all the timely responses!