webpack supports this via ts-node
We all know webpack configuration is an absolute nightmare between JavaScript's lack of a typing system combined with the fact that they still let you do all the ancient 1.x configuration. Via TypeScript we can leverage the typing system and best of all go through the types package and completely rip out all the 1.x configuration items to force everyone into using the new configuration items. I have it all working marvelously with webpack but the aspnet-webpack module expects it to be strictly in JavaScript which requires an extra compilation and transpilation step that I don't need anywhere else. Given Microsoft's commitment to TypeScript as the future of JavaScript I think this feature should be baked in as it will vastly help .NET developers be productive with configuring webpack.
import * as autoprefixer from "autoprefixer";
import * as CleanWebpackPlugin from "clean-webpack-plugin";
import * as cssnano from "cssnano";
import * as ExtractTextWebpackPlugin from "extract-text-webpack-plugin";
import * as path from "path";
import * as postcssFixes from "postcss-fixes";
import * as webpack from "webpack";
import * as webpackMerge from "webpack-merge";
export interface IBaseModule extends webpack.BaseModule {
preLoaders?: IRule[];
postLoaders?: IRule[];
}
export interface IModule extends IBaseModule {
rules: IRule[];
}
export interface IBaseRule extends webpack.BaseRule {
rules?: IRule[];
oneOf?: IRule[];
}
export interface IBaseDirectRule extends IBaseRule {
test: webpack.Condition | webpack.Condition[];
}
export interface IBaseSingleLoaderRule extends IBaseDirectRule {
loader: webpack.NewLoader;
}
export interface ILoaderRule extends IBaseSingleLoaderRule {
options?: { [name: string]: any };
}
export interface IUseRule extends IBaseDirectRule {
use: webpack.NewLoader | webpack.NewLoader[];
}
export interface IRulesRule extends IBaseRule {
rules: IRule[];
}
export interface IOneOfRule extends IBaseRule {
oneOf: IRule[];
}
export type IRule = ILoaderRule | IUseRule | IRulesRule | IOneOfRule;
export interface IConfig extends webpack.Configuration {
module?: IModule;
resolve?: webpack.NewResolve;
resolveLoader?: webpack.NewResolveLoader;
}
module.exports = () => {
const isProdBuild: boolean = process.argv.indexOf("-p") !== -1;
const outputFolder: string = "./src/wwwroot/dist";
const tsConfigFile: string = "./config/tsconfig.json";
const fileName: string = "[name].[ext]";
const fontFolder: string = `fonts/${fileName}`;
const imageFolder: string = `media/[ext]/${fileName}`;
const urlLimit: number = 10000;
const sharedConfig = (): IConfig => ({
devtool: isProdBuild ? "source-map" : "eval-source-map",
module: {
rules: [
{
enforce: "pre",
test: /\.(tsx|ts)?$/,
use: {
loader: "tslint-loader",
options: {
emitErrors: isProdBuild,
tsConfigFile,
},
},
},
{
test: /\.(tsx|ts)?$/,
use: {
loader: "awesome-typescript-loader",
options: {
configFileName: tsConfigFile,
},
},
},
{
enforce: "pre",
test: /\.js$/,
use: {
loader: "source-map-loader",
},
},
{
test: /\.(jpe?g|png|gif)$/i,
use: {
loader: "file-loader",
options: {
name: imageFolder,
},
},
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "image/svg+xml",
name: imageFolder,
},
},
},
],
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, outputFolder),
publicPath: "/dist/",
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
],
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
});
const clientConfig = webpackMerge(sharedConfig(), {
entry: {
app: "./src/client.tsx",
},
module: {
rules: [
{
test: /(\.css|\.scss|\.sass)$/,
use: ExtractTextWebpackPlugin.extract({
fallback: "style-loader",
use: [{
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: true,
}}, {
loader: "postcss-loader",
options: {
plugins: [
autoprefixer({
browsers: ["last 2 versions"],
}),
postcssFixes({
preset: "recommended",
}),
].concat(isProdBuild ? [
cssnano({
calc: false,
safe: true,
}),
] : []),
sourceMap: true,
}}, {
loader: "sass-loader",
options: {
sourceMap: true,
},
}],
}),
},
{
test: /\.eot(\?v=\d+.\d+.\d+)?$/,
use: {
loader: "file-loader",
options: {
name: fontFolder,
},
},
},
{
test: /\.ico$/,
use: {
loader: "file-loader",
options: {
name: fileName,
},
},
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "application/font-woff",
name: fontFolder,
},
},
},
{
test: /\.[ot]tf(\?v=\d+.\d+.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "application/octet-stream",
name: fontFolder,
},
},
},
],
},
plugins: [
new CleanWebpackPlugin([outputFolder]),
new webpack.optimize.CommonsChunkPlugin({
minChunks(module: IConfig) {
return module.context && module.context.indexOf("node_modules") !== -1;
},
name: "vendor",
}),
new webpack.optimize.CommonsChunkPlugin({
minChunks: Infinity,
name: "manifest",
}),
new ExtractTextWebpackPlugin({
allChunks: true,
filename: "css/[name].css",
}),
],
target: "web",
} as IConfig);
const serverConfig = webpackMerge(sharedConfig(), {
entry: {
server: "./src/server.tsx",
},
output: {
libraryTarget: "commonjs",
},
resolve: {
mainFields: ["main"],
},
target: "node",
} as IConfig);
return [ clientConfig, serverConfig ];
};
Author's note this assumes you are using webpack -p to run your production builds which implicitly adds all the node env setup as well as uglify plugins so there is no need to manually configure them anymore.
Here is the drop in JavaScript replacement which vastly simplifies the config process getting rid of the vendor.config entirely if you want to test it out.
const autoprefixer = require('autoprefixer');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const CleanWebpackPlugin = require("clean-webpack-plugin");
const cssnano = require('cssnano');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
const path = require('path');
const postcssFixes = require('postcss-fixes');
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
module.exports = () => {
const isProdBuild = process.argv.indexOf("-p") !== -1;
const outputFolder = './wwwroot/dist';
const fileName = "[name].[ext]";
const imageFolder = `media/[ext]/${fileName}`;
const fontFolder = `fonts/${fileName}`;
const urlLimit = 10000;
// Configuration in common to both client-side and server-side bundles
const sharedConfig = () => ({
devtool: isProdBuild ? "source-map" : "eval-source-map",
module: {
rules: [
{
enforce: "pre",
test: /\.(tsx|ts)?$/,
use: {
loader: "tslint-loader",
options: {
emitErrors: isProdBuild,
},
},
},
{
test: /\.(tsx|ts)?$/,
use: {
loader: "awesome-typescript-loader",
},
},
{
enforce: "pre",
test: /\.js$/,
use: {
loader: "source-map-loader",
},
},
{
test: /\.(jpe?g|png|gif)$/i,
use: {
loader: "file-loader",
options: {
name: imageFolder,
},
},
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "image/svg+xml",
name: imageFolder,
},
},
},
],
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, outputFolder),
publicPath: '/dist/',
},
plugins: [
new CheckerPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
],
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
stats: {
modules: false,
},
});
// Configuration for client-side bundle suitable for running in browsers
const clientConfig = webpackMerge(sharedConfig(), {
entry: {
app: './ClientApp/boot-client.tsx'
},
module: {
rules: [
{
test: /(\.css|\.scss|\.sass)$/,
use: ExtractTextWebpackPlugin.extract({
fallback: "style-loader",
use: [{
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: true,
}}, {
loader: "postcss-loader",
options: {
plugins: [
autoprefixer({
browsers: ["last 2 versions"],
}),
postcssFixes({
preset: "recommended",
}),
].concat(isProdBuild ? [
cssnano({
calc: false,
safe: true,
}),
] : []),
sourceMap: true,
}}, {
loader: "sass-loader",
options: {
sourceMap: true,
},
}],
}),
},
{
test: /\.eot(\?v=\d+.\d+.\d+)?$/,
use: {
loader: "file-loader",
options: {
name: fontFolder,
},
},
},
{
test: /\.ico$/,
use: {
loader: "file-loader",
options: {
name: fileName,
},
},
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "application/font-woff",
name: fontFolder,
},
},
},
{
test: /\.[ot]tf(\?v=\d+.\d+.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: urlLimit,
mimetype: "application/octet-stream",
name: fontFolder,
},
},
},
],
},
plugins: [
new CleanWebpackPlugin([outputFolder]),
new webpack.optimize.CommonsChunkPlugin({
minChunks: function (module) {
// this assumes your vendor imports exist in the node_modules directory
return module.context && module.context.indexOf("node_modules") !== -1;
},
name: "vendor",
}),
new webpack.optimize.CommonsChunkPlugin({
minChunks: Infinity,
name: "manifest",
}),
new ExtractTextWebpackPlugin({
allChunks: true,
filename: "css/[name].css",
}),
],
target: "web",
});
// Configuration for server-side (prerendering) bundle suitable for running in Node
const serverConfig = webpackMerge(sharedConfig(), {
entry: {
server: "./ClientApp/boot-server.tsx",
},
resolve: {
mainFields: ['main'],
},
output: {
libraryTarget: 'commonjs',
},
target: "node",
});
return [clientConfig, serverConfig];
};
Does anyone maintain this project or is it a tombstone?
Thanks for the suggestion! I think what we'd do is add an extra bool flag to WebpackDevMiddlewareOptions called UseTypeScriptConfig or similar. If this flag was set, then:
webpack.config.ts instead of webpack.config.jsaspnet-webpack would run require('ts-node/register') during startup, before any calls to Webpack.This appears to be sufficient to get a TypeScript config file to work.
Does anyone maintain this project or is it a tombstone?
Awesome @SteveSandersonMS glad it is something that simple!
Also I really want to run aspnet-webpack from the parent folder so I can keep the config and package stuff out of the /src folder in my repository. Unfortunately node's require function doesn't look for the node_modules folder one level up would this be a similarly easy fix?
I'm happy to contribute PRs to the templates project so we can implement ASP.NET Razor Pages, config via typescript, & simplified webpack.config (no need for a separate vendor file). Do you want me to open issues for each then do the PRs there?
we can implement ASP.NET Razor Pages, config via typescript, & simplified webpack.config (no need for a separate vendor file). Do you want me to open issues for each then do the PRs there?
This issue already covers "config via TypeScript". For the other items, those are not things we have plans to include and maintain in this repo (this might change at some point, particularly the Razor Pages item, but it's not decided yet). So no need for extra issues.
If you're keen to do a PR for "config via TypeScript" following the design described above then please go ahead :)
@SteveSandersonMS how do we get community contributions into this repository? PR 1241 has been sitting there for over a month. I also really want the CSP/Nonce PR in as well. I hate having to have unsafe-inline allowed in my CSP.
There isn't an SLA on how quickly things get merged. We're working within a larger ASP.NET shipping schedule and make choices about what we prioritise to include in each release.
As a side note I'm now using the tsconfig-paths package so ts-node can load a stripped down tsconfig file that I have setup just for webpack.
tsconfig.webpack.config
{
"compilerOptions": {
"module": "commonjs",
"target": "es5"
}
}
webpack.config.hmr.js
require('ts-node/register');
process.env.TS_NODE_PROJECT = './tsconfig.webpack.json';
module.exports = require('./webpack.config.ts').default;
Most helpful comment
Thanks for the suggestion! I think what we'd do is add an extra bool flag to
WebpackDevMiddlewareOptionscalledUseTypeScriptConfigor similar. If this flag was set, then:webpack.config.tsinstead ofwebpack.config.jsaspnet-webpackwould runrequire('ts-node/register')during startup, before any calls to Webpack.This appears to be sufficient to get a TypeScript config file to work.
https://github.com/aspnet/JavaScriptServices/pulse