blob: 0e84c498e185fdbed916bef31af16f77684ebdaf [file] [log] [blame]
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The buildCommonWebpackConfig() function generates a common configuration for
// Webpack. You can include it at the start of your webpack.config.ts and then
// make modifications to it from there.
//
// Entry points for pages under the "pages" subdirectory are added to the
// configuration by default, as well as demo pages for modules under the
// "modules" subdirectory if running in development mode.
//
// Users can add any other entry points as needed. See webpack.config.ts in
// this directory as an example.
//
// A webpack.config.ts can be as simple as:
//
// import buildCommonWebpackConfig from 'pulito';
// import * as webpack from 'webpack';
//
// const configFactory: webpack.ConfigurationFactory =
// (_, args) => buildCommonWebpackConfig(__dirname, args.mode);
//
// export = configFactory;
//
// An application that requires additional entry points might define them as
// follows:
//
// import buildCommonWebpackConfig from 'pulito';
// import * as webpack from 'webpack';
// import HtmlWebpackPlugin from 'html-webpack-plugin';
//
// const configFactory: webpack.ConfigurationFactory = (_, args) => {
// const config = buildCommonWebpackConfig(__dirname, args.mode);
// (config.entry as webpack.Entry)['index'] = './pages/index.js'
// config.plugins.push(
// new HtmlWebpackPlugin({
// filename: 'index.html',
// template: './pages/index.html',
// chunks: ['index'],
// })
// );
// return config;
// }
//
// export = configFactory;
//
// To build the application please run one of the following commands:
//
// # Includes both the application and demo pages.
// $ npx webpack --mode=development
//
// # Only includes the application pages.
// $ npx webpack --mode=production
//
// Notes:
// - args.mode will be set to either "production", "development", or ""
// depending on the --mode flag passed to the "npx webpack" command.
// - Applications do not need to include in their package.json file any of
// the plugins and loaders used in this file, as those dependencies are
// satisfied in Pulito's own package.json file.
// - Any other plugins or loaders used in an application's webpack.config.ts
// file should be declared as dependencies in that application's
// package.json file.
import * as webpack from 'webpack';
import * as glob from 'glob';
import * as path from 'path';
import * as fs from 'fs';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import 'webpack-dev-server';
/** Represents an HTML file and a companion TypeScript or JavaScript file. */
interface HtmlAndTsOrJsFilePair {
html: string,
tsOrJs: string,
};
/**
* Finds all HTML/TypeScript and HTML/JavaScript file pairs with the same base name in the given
* directory.
*
* Prints out an error if an HTML file is found without a companion TypeScript or JavaScript file,
* or if both are found. Any such HTML files are be excluded from the results.
*/
function findHtmlAndTsOrJsFilePairs(directory: string, htmlGlob = '*.html'): HtmlAndTsOrJsFilePair[] {
const pagesFound: HtmlAndTsOrJsFilePair[] = [];
const htmlFiles = glob.sync(path.resolve(directory, htmlGlob));
htmlFiles.forEach(htmlFile => {
const tsFile = htmlFile.replace(/\.html$/, '.ts');
const jsFile = htmlFile.replace(/\.html$/, '.js');
const tsFileExists = fs.existsSync(tsFile);
const jsFileExists = fs.existsSync(jsFile);
// Fail if neither a TypeScript nor a JavaScript file is provided.
if (!tsFileExists && !jsFileExists) {
console.log(`WARNING: Page ${htmlFile} needs either a ${tsFile} or a ${jsFile} file.`);
return;
}
// Fail if both a TypeScript and a JavaScript file are provided.
if (tsFileExists && jsFileExists) {
console.log(`WARNING: Page ${htmlFile} cannot have both ${tsFile} and ${jsFile} files.`);
return;
}
pagesFound.push({
html: htmlFile,
tsOrJs: tsFileExists ? tsFile : jsFile
});
});
return pagesFound;
}
// Production minification settings for HTML pages.
const minifyOptions: HtmlWebpackPlugin.MinifyOptions = {
caseSensitive: true,
collapseBooleanAttributes: true,
collapseWhitespace: true,
// This handles CSS minification in the .js files. For options involving minifying .[s]css files,
// see ./**/postcss.config.js
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
};
/**
* Looks for pages (consisting of *.html and *.ts or .js file pairs) inside pagesDirectory, and
* adds them to the Webpack configuration.
*
* Each page gets its own entry point and HtmlWebpackPlugin instance in the Webpack configuration.
*/
function addApplicationPages(
pagesDirectory: string,
webpackConfig: webpack.Configuration,
minifyOutput: boolean): void {
// Find all HTML pages under the "pages" directory, along with their respective TS or JS files.
findHtmlAndTsOrJsFilePairs(pagesDirectory).forEach(pair => {
const chunkName = path.basename(pair.html, '.html');
// Add TypeScript / JavaScript entry point.
(webpackConfig.entry as webpack.Entry)[chunkName] = pair.tsOrJs;
// Add output HTML page.
webpackConfig.plugins!.push(
new HtmlWebpackPlugin({
filename: path.basename(pair.html),
template: pair.html,
chunks: [chunkName],
minify: minifyOutput ? minifyOptions : false
})
)
});
}
/**
* Looks for demo pages (consisting of *-demo.html and *-demo.ts or .js file pairs) inside all
* subdirectories of modulesRootDir, and adds them to the Webpack configuration.
*
* Each page gets its own entry point and HtmlWebpackPlugin instance in the Webpack configuration.
*
* Will throw an exception if any modules are found with more than one demo page.
*/
function addDemoPages(modulesRootDir: string, webpackConfig: webpack.Configuration): void {
// Find all module directories.
const moduleDirectories =
fs.readdirSync(modulesRootDir)
.map(f => path.join(modulesRootDir, f))
.filter(f => fs.lstatSync(f).isDirectory());
// We will populate this array with demo pages found in the module directories.
const demoPages: {moduleName: string, html: string, tsOrJs: string}[] = [];
// Search for demo pages inside each module. At most 1 demo page per module is allowed.
moduleDirectories.forEach(moduleDir => {
const pairs = findHtmlAndTsOrJsFilePairs(moduleDir, "*-demo.html");
// At most 1 demo page per module.
if (pairs.length > 1) {
throw 'Only one demo page is allowed per module: ${directory}';
}
// Keep the first and only demo page, or skip if none is found.
if (pairs.length == 0) return;
const pair = pairs[0];
demoPages.push({moduleName: path.basename(moduleDir), html: pair.html, tsOrJs: pair.tsOrJs});
});
// Add demo page entry points and HTML plugins to the Webpack configuration.
demoPages.forEach(page => {
// Add TypeScript / JavaScript entry point.
(webpackConfig.entry as webpack.Entry)[page.moduleName] = page.tsOrJs;
// Add output HTML page.
webpackConfig.plugins!.push(
new HtmlWebpackPlugin({
filename: page.moduleName + '.html',
template: page.html,
chunks: [page.moduleName],
})
);
});
}
/**
* Builds the common Webpack configuration.
*
* @param dirname Application's root directory containing the "modules" and "pages" subdirectories.
* @param mode Mode string as in the CliConfigOptions interface's "mode" field.
* @param neverMinifyHtml If true, HTML minification is disabled, even in production mode. Use this
* e.g. when the HTML files contain Go template tags.
*/
function buildCommonWebpackConfig(
dirname: string,
mode?: 'development' | 'production' | 'none',
neverMinifyHtml = false): webpack.Configuration {
// Convenience constants. Defaults to production if e.g. "npx webpack" is invoked without
// specifying a mode via the --mode fag.
const devMode = mode == 'development';
const prodMode = !devMode;
const configuration: webpack.Configuration = {
entry: {
// Will be populated with application and demo pages.
},
resolve: {
extensions: ['.ts', '.js']
},
output: {
path: path.resolve(dirname, 'dist'),
filename: '[name]-bundle.js?[chunkhash]',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
// The two below options are required to access the dev server from a different host (e.g.
// serve from workstation, access from laptop).
host: '0.0.0.0',
disableHostCheck: true,
},
devtool: devMode ? 'inline-source-map' : false,
mode: devMode ? 'development' : 'production',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.[s]?css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
},
{
loader: 'css-loader',
options: {
importLoaders: 2, // postcss-loader and sass-loader.
},
},
{
loader: 'postcss-loader',
options: {
config: {
// This file handles minification, auto-prefixing, etc. See there for configuing
// those plugins.
//
// The PostCSS config file must be named postcss.config.js, so we store the
// different configs in different directories.
path: path.resolve(__dirname, devMode ? 'prod' : 'dev', 'postcss.config.js'),
},
},
},
{
// Since SCSS is a superset of CSS we can always apply this loader.
loader: 'sass-loader',
options: {
includePaths: [__dirname],
}
}
],
},
{
test: /\.html$/,
use: [
{
loader:'html-loader',
options: {
name: '[name].[ext]',
},
}
],
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]-bundle.css?[hash]',
}),
new CleanWebpackPlugin(),
]
};
// Add application pages.
addApplicationPages(
path.resolve(dirname, 'pages'),
configuration,
neverMinifyHtml ? false : prodMode);
// Add demo pages.
if (devMode) {
addDemoPages(path.resolve(dirname, 'modules'), configuration);
}
return configuration;
};
export default buildCommonWebpackConfig;