aurelia / webpack-plugin

A plugin for webpack that enables bundling Aurelia applications.
MIT License
90 stars 36 forks source link

[Webpack 5] The custom elements imported from HTML are ignored in production build but works under 'webpack serve' #181

Closed graycrow closed 3 years ago

graycrow commented 3 years ago

I'm submitting a bug report

Please tell us about your environment:

Current behavior: I keep upgrading to Webpack 5. I have reached a state where both the development build and the production build are built without errors and run without errors. But only the development build created with webpack serve works as it should. The production build is displayed with many custom elements missing from the page.

Looks like all the custom elements imported from the html pages using require from tags are ignored. With enabled dev logging, I can see that all of them are listed under DEBUG [templating] importing resources for [page-file-name] but then nothing happens and their own dependencies not being imported. Also I have no errors on the browser console. Only if I try to open a different (non-default) route, I get errors like ERROR [app-router] Error: No Aurelia APIs are defined for the element: "DIV".

Here is my Webpack config.

const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const svgToMiniDataURI = require("mini-svg-data-uri");
const project = require("./aurelia_project/aurelia.json");
const { AureliaPlugin, ModuleDependenciesPlugin } = require("aurelia-webpack-plugin");
const { ProvidePlugin } = require("webpack");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

// config helpers:
const ensureArray = (config) => (config && (Array.isArray(config) ? config : [config])) || [];
const when = (condition, config, negativeConfig) =>
    condition ? ensureArray(config) : ensureArray(negativeConfig);

// primary config:
const outDir = path.resolve(__dirname, project.platform.output);
const srcDir = path.resolve(__dirname, "src");
const nodeModulesDir = path.resolve(__dirname, "node_modules");
const baseUrl = "/";

const cssRules = [
    { loader: "css-loader", options: { esModule: false } },
    {
        loader: "postcss-loader",
        options: {
            postcssOptions: {
                plugins: [
                    "autoprefixer",
                    [
                        "cssnano",
                        {
                            preset: ["default", { normalizePositions: false }],
                        },
                    ],
                ],
            },
        },
    },
];

const sassRules = [
    {
        loader: "sass-loader",
        options: {
            sassOptions: {
                includePaths: ["node_modules"],
            },
        },
    },
];

const lessRules = [
    {
        loader: "less-loader",
        options: {
            lessOptions: {
                includePaths: ["node_modules"],
            },
        },
    },
];

module.exports = function (env, { analyze }) {
    const production =
        env.NODE_ENV === "production" || env.production || process.env.NODE_ENV === "production";
    const extractCss = true;
    return {
        target: "web",
        resolve: {
            extensions: [".ts", ".js"],
            modules: [srcDir, "node_modules"],
            alias: {
                // Enforce single aurelia-binding, to avoid v1/v2 duplication due to
                // out-of-date dependencies on 3rd party aurelia plugins
                "aurelia-binding": path.resolve(__dirname, "../node_modules/aurelia-binding"),
            },
        },
        entry: {
            app: ["aurelia-bootstrapper"],
        },
        mode: production ? "production" : "development",
        output: {
            path: outDir,
            publicPath: baseUrl,
            filename: production
                ? "src/[name].[contenthash].bundle.js"
                : "src/[name].[fullhash].bundle.js",
            chunkFilename: production
                ? "src/[name].[contenthash].chunk.js"
                : "src/[name].[fullhash].chunk.js",
        },
        optimization: {
            moduleIds: "deterministic",
            minimizer: [
                new TerserPlugin({
                    parallel: true,
                    terserOptions: {
                        ecma: 2015,
                    },
                }),
                new CssMinimizerPlugin(),
            ],
        },
        performance: { hints: false },
        devServer: {
            contentBase: outDir,
            historyApiFallback: true,
            disableHostCheck: true,
            https: production
                ? false
                : {
                      key: fs.readFileSync("../certificates/development.key"),
                      cert: fs.readFileSync("../certificates/development.crt"),
                  },
            hot: project.platform.hmr,
            port: project.platform.port,
            host: project.platform.host,
        },
        devtool: production ? undefined : "inline-source-map",
        module: {
            rules: [
                {
                    test: /\.css$/i,
                    issuer: [{ not: [/\.html$/i] }],
                    use: extractCss
                        ? [
                              {
                                  loader: MiniCssExtractPlugin.loader,
                              },
                              ...cssRules,
                          ]
                        : ["style-loader", ...cssRules],
                },
                {
                    test: /\.css$/i,
                    // CSS required in templates cannot be extracted safely
                    // because Aurelia would try to require it again in runtime
                    use: cssRules,
                    issuer: /\.html?$/i,
                },
                {
                    test: /\.scss$/i,
                    use: extractCss
                        ? [
                              {
                                  loader: MiniCssExtractPlugin.loader,
                              },
                              ...cssRules,
                              ...sassRules,
                          ]
                        : ["style-loader", ...cssRules, ...sassRules],
                    issuer: /\.[tj]s$/i,
                },
                {
                    test: /\.scss$/,
                    use: [...cssRules, ...sassRules],
                    issuer: /\.html?$/i,
                },
                {
                    test: /\.less$/i,
                    use: extractCss
                        ? [
                              {
                                  loader: MiniCssExtractPlugin.loader,
                              },
                              ...cssRules,
                              ...lessRules,
                          ]
                        : ["style-loader", ...cssRules, ...lessRules],
                    issuer: /\.[tj]s$/i,
                },
                {
                    test: /\.less$/i,
                    use: [...cssRules, ...lessRules],
                    issuer: /\.html?$/i,
                },
                { test: /\.html$/i, loader: "html-loader", options: { esModule: false } },
                { test: /\.ts$/i, loader: "ts-loader" },
                {
                    test: /\.svg$/i,
                    type: "asset/inline",
                    generator: {
                        dataUrl: (content) => {
                            content = content.toString();
                            return svgToMiniDataURI(content);
                        },
                    },
                },
                {
                    test: /\.(png|gif|jpg|cur)$/i,
                    type: "asset",
                    parser: {
                        dataUrlCondition: {
                            maxSize: 8192,
                        },
                    },
                },
                {
                    test: /\.woff2(\?v=[0-9]\.[0-9]\.[0-9])?$/i,
                    type: "asset",
                    parser: {
                        dataUrlCondition: {
                            maxSize: 10000,
                        },
                    },
                },
                {
                    test: /\.(ttf|eot|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/i,
                    type: "asset/resource",
                },
                {
                    test: /environment\.json$/i,
                    use: [
                        {
                            loader: "app-settings-loader",
                            options: { env: production ? "production" : "development" },
                        },
                    ],
                },
                { test: /translation\.json$/i, type: "asset/source" },
            ],
        },
        plugins: [
            new AureliaPlugin(),
            new ProvidePlugin({
                Promise: ["promise-polyfill", "default"],
                Buffer: ["buffer", "Buffer"],
            }),
            new ModuleDependenciesPlugin({
                "aurelia-testing": ["./compile-spy", "./view-spy"],
                "aurelia-i18n": [
                    { name: "locales/cs-CZ/translation.json" },
                    { name: "locales/en-US/translation.json" },
                ],
                "aurelia-view-manager": [
                    "views/bootstrap4/autocomplete.html",
                    "views/bootstrap4/pager.html",
                ],
                "aurelia-pager": ["./pager"],
            }),
            new HtmlWebpackPlugin({
                template: "index.ejs",
                filename: "index.html",
                metadata: {
                    baseUrl,
                },
            }),
            // ref: https://webpack.js.org/plugins/mini-css-extract-plugin/
            ...when(
                extractCss,
                new MiniCssExtractPlugin({
                    // updated to match the naming conventions for the js files
                    filename: "css/[name].[contenthash].bundle.css",
                    chunkFilename: "css/[name].[contenthash].chunk.css",
                })
            ),
            new CopyWebpackPlugin({
                patterns: [{ from: "static", to: outDir, globOptions: { ignore: [".*"] } }],
            }),
            ...when(analyze, new BundleAnalyzerPlugin()),
        ],
    };
};

I would appreciate any help or feedback.

bigopon commented 3 years ago

Are the ignore elements with relative specifier? (starts with . e.x ./ or ../) I suspect the root cause would be similar to #178

graycrow commented 3 years ago

Are the ignore elements with relative specifier? (starts with . e.x ./ or ../) I suspect the root cause would be similar to #178

Yes, they are.

graycrow commented 3 years ago

Looks like I found the reason/solution for this issue. For some unknown reason, I had "module": "amd" in the tsconfig.json. After I changed it to ESNext, the ignored custom elements started working. But "module": "ESNext" opened yet another layer of issues, e.g. not calling many plugin configure functions because webpack mangles the names of exported functions and also excludes them as unused. I'm digging into it now and probably will open a separate issue.