diff --git a/.eslintrc.json b/.eslintrc.json
index 576ec7a..4f1db28 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -20,7 +20,7 @@
 		"ecmaFeatures": {
 			"impliedStrict": true
 		},
-		"project": "tsconfig.eslint.json"
+		"project": "tsconfig.json"
 	},
 	"plugins": ["import", "@typescript-eslint"],
 	"reportUnusedDisableDirectives": true,
diff --git a/package.json b/package.json
index 155a827..d778f1c 100644
--- a/package.json
+++ b/package.json
@@ -6176,17 +6176,20 @@
 		"analyze:bundle": "webpack --env.analyzeBundle",
 		"analyze:deps": "webpack --env.analyzeDeps",
 		"build": "webpack --mode development",
+		"build:extension": "webpack --mode development --config-name extension",
+		"build:webviews": "webpack --mode development --config-name webviews",
 		"bundle": "webpack --mode production",
 		"clean": "git clean -Xdf -e !node_modules -e !node_modules/**/*",
 		"lint": "eslint src/**/*.ts --fix --cache",
+		"optimize:webviews": "webpack --config-name webviews --env.optimizeImages",
 		"pack": "vsce package --yarn",
 		"pretty": "prettier --config .prettierrc --loglevel warn --write .",
 		"pub": "vsce publish --yarn",
 		"rebuild": "yarn run reset && yarn run build",
 		"reset": "yarn run clean && yarn --frozen-lockfile",
 		"watch": "webpack --watch --mode development --info-verbosity verbose",
-		"webviews:optimize": "webpack --config-name webviews --env.optimizeImages",
-		"webviews:watch": "webpack --watch --config-name webviews --mode development --info-verbosity verbose",
+		"watch:extension": "webpack --watch --mode development --config-name extension --info-verbosity verbose",
+		"watch:webviews": "webpack --watch --mode development --config-name webviews --info-verbosity verbose",
 		"update:emoji": "pushd emoji && node ./shortcodeToEmoji.js && popd",
 		"vscode:prepublish": "yarn run bundle"
 	},
diff --git a/src/webviews/apps/.eslintrc.json b/src/webviews/apps/.eslintrc.json
new file mode 100644
index 0000000..ddda709
--- /dev/null
+++ b/src/webviews/apps/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+	"extends": ["../../../.eslintrc.json"],
+	"parserOptions": {
+		"project": "src/webviews/apps/tsconfig.json"
+	}
+}
diff --git a/src/webviews/apps/shared/appWithConfigBase.ts b/src/webviews/apps/shared/appWithConfigBase.ts
index bc30e6b..6a20bd4 100644
--- a/src/webviews/apps/shared/appWithConfigBase.ts
+++ b/src/webviews/apps/shared/appWithConfigBase.ts
@@ -7,11 +7,11 @@ import {
 	onIpcNotification,
 	UpdateConfigurationCommandType,
 } from '../../protocol';
-import { DOM } from './dom';
 import { App } from './appBase';
-import { Dates } from '../../../system/date';
+import { DOM } from './dom';
+import { getDateFormatter } from '../shared/date';
 
-const dateFormatter = Dates.getFormatter(new Date('Wed Jul 25 2018 19:18:00 GMT-0400'));
+const dateFormatter = getDateFormatter(new Date('Wed Jul 25 2018 19:18:00 GMT-0400'));
 
 export abstract class AppWithConfig<TState extends AppStateWithConfig> extends App<TState> {
 	private _changes = Object.create(null) as Record<string, any>;
diff --git a/src/webviews/apps/shared/date.ts b/src/webviews/apps/shared/date.ts
new file mode 100644
index 0000000..e3c92a0
--- /dev/null
+++ b/src/webviews/apps/shared/date.ts
@@ -0,0 +1,16 @@
+'use strict';
+import dayjs from 'dayjs';
+import advancedFormat from 'dayjs/plugin/advancedFormat';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(advancedFormat);
+dayjs.extend(relativeTime);
+
+export interface DateFormatter {
+	fromNow(): string;
+	format(format: string): string;
+}
+
+export function getDateFormatter(date: Date): DateFormatter {
+	return dayjs(date);
+}
diff --git a/src/webviews/apps/tsconfig.json b/src/webviews/apps/tsconfig.json
new file mode 100644
index 0000000..8768bac
--- /dev/null
+++ b/src/webviews/apps/tsconfig.json
@@ -0,0 +1,11 @@
+{
+	"extends": "../../../tsconfig.json",
+	"compilerOptions": {
+		"allowSyntheticDefaultImports": true,
+		"esModuleInterop": true,
+		"lib": ["dom", "dom.iterable", "es2019"],
+		"outDir": "dist/webviews"
+	},
+	"include": ["../../config.ts", "../protocol.ts", "**/*"],
+	"exclude": ["node_modules", "test"]
+}
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
deleted file mode 100644
index ac24808..0000000
--- a/tsconfig.eslint.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-	"extends": "./tsconfig.json",
-	"compilerOptions": {
-		"lib": ["dom", "dom.iterable", "es2018"]
-	},
-	"exclude": ["node_modules", "test"]
-}
diff --git a/tsconfig.webviews.json b/tsconfig.webviews.json
deleted file mode 100644
index cb2d112..0000000
--- a/tsconfig.webviews.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-	"extends": "./tsconfig.json",
-	"compilerOptions": {
-		"lib": ["dom", "dom.iterable", "es2019"],
-		"outDir": "dist/webviews"
-	},
-	"include": ["src/config.ts", "src/webviews/protocol.ts", "src/webviews/apps/**/*"],
-	"exclude": ["node_modules", "test"]
-}
diff --git a/webpack.config.js b/webpack.config.js
index da0d44d..be6ca2f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -85,7 +85,7 @@ module.exports =
 
 		if (env.analyzeBundle || env.analyzeDeps) {
 			env.optimizeImages = false;
-		} else if (!env.optimizeImages && !fs.existsSync(path.resolve(__dirname, 'images/settings'))) {
+		} else if (!env.optimizeImages && !fs.existsSync(path.join(__dirname, 'images/settings'))) {
 			env.optimizeImages = true;
 		}
 
@@ -171,7 +171,7 @@ function getExtensionConfig(mode, env) {
 			rules: [
 				{
 					exclude: /\.d\.ts$/,
-					include: path.resolve(__dirname, 'src'),
+					include: path.join(__dirname, 'src'),
 					test: /\.tsx?$/,
 					use: {
 						loader: 'ts-loader',
@@ -185,7 +185,7 @@ function getExtensionConfig(mode, env) {
 		},
 		resolve: {
 			alias: {
-				'universal-user-agent': path.resolve(__dirname, 'node_modules/universal-user-agent/dist-node/index.js'),
+				'universal-user-agent': path.join(__dirname, 'node_modules/universal-user-agent/dist-node/index.js'),
 			},
 			extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
 			symlinks: false,
@@ -209,10 +209,12 @@ function getExtensionConfig(mode, env) {
  * @returns { WebpackConfig }
  */
 function getWebviewsConfig(mode, env) {
+	const basePath = path.join(__dirname, 'src/webviews/apps');
+
 	const clean = ['**/*'];
 	if (env.optimizeImages) {
 		console.log('Optimizing images (src/webviews/apps/images/settings/*.png)...');
-		clean.push(path.resolve(__dirname, 'images/settings/*'));
+		clean.push(path.join(__dirname, 'images/settings/*'));
 	}
 
 	const cspPolicy = {
@@ -235,12 +237,12 @@ function getWebviewsConfig(mode, env) {
 			async: false,
 			eslint: {
 				enabled: true,
-				files: path.resolve(__dirname, 'src/webviews/apps/**/*.ts'),
+				files: path.join(basePath, '**/*.ts'),
 				options: { cache: true },
 			},
 			formatter: 'basic',
 			typescript: {
-				configFile: path.resolve(__dirname, 'tsconfig.webviews.json'),
+				configFile: path.join(basePath, 'tsconfig.json'),
 			},
 		}),
 		new MiniCssExtractPlugin({
@@ -250,7 +252,7 @@ function getWebviewsConfig(mode, env) {
 			template: 'rebase/rebase.html',
 			chunks: ['rebase', 'rebase-styles'],
 			excludeAssets: [/.+-styles\.js/],
-			filename: path.resolve(__dirname, 'dist/webviews/rebase.html'),
+			filename: path.join(__dirname, 'dist/webviews/rebase.html'),
 			inject: true,
 			inlineSource: mode === 'production' ? '.css$' : undefined,
 			cspPlugin: {
@@ -279,7 +281,7 @@ function getWebviewsConfig(mode, env) {
 			template: 'settings/settings.html',
 			chunks: ['settings', 'settings-styles'],
 			excludeAssets: [/.+-styles\.js/],
-			filename: path.resolve(__dirname, 'dist/webviews/settings.html'),
+			filename: path.join(__dirname, 'dist/webviews/settings.html'),
 			inject: true,
 			cspPlugin: {
 				enabled: true,
@@ -307,7 +309,7 @@ function getWebviewsConfig(mode, env) {
 			template: 'welcome/welcome.html',
 			chunks: ['welcome', 'welcome-styles'],
 			excludeAssets: [/.+-styles\.js/],
-			filename: path.resolve(__dirname, 'dist/webviews/welcome.html'),
+			filename: path.join(__dirname, 'dist/webviews/welcome.html'),
 			inject: true,
 			cspPlugin: {
 				enabled: true,
@@ -336,11 +338,11 @@ function getWebviewsConfig(mode, env) {
 		new ImageminPlugin({
 			disable: !env.optimizeImages,
 			externalImages: {
-				context: path.resolve(__dirname, 'src/webviews/apps/images'),
-				sources: glob.sync('src/webviews/apps/images/settings/*.png'),
-				destination: path.resolve(__dirname, 'images'),
+				context: path.join(basePath, 'images'),
+				sources: glob.sync(path.join(basePath, 'images/settings/*.png')),
+				destination: path.join(__dirname, 'images'),
 			},
-			cacheFolder: path.resolve(__dirname, 'node_modules', '.cache', 'imagemin-webpack-plugin'),
+			cacheFolder: path.join(__dirname, 'node_modules', '.cache', 'imagemin-webpack-plugin'),
 			gifsicle: null,
 			jpegtran: null,
 			optipng: null,
@@ -355,7 +357,7 @@ function getWebviewsConfig(mode, env) {
 
 	return {
 		name: 'webviews',
-		context: path.resolve(__dirname, 'src/webviews/apps'),
+		context: basePath,
 		entry: {
 			rebase: ['./rebase/rebase.ts'],
 			'rebase-styles': ['./scss/rebase.scss'],
@@ -369,19 +371,19 @@ function getWebviewsConfig(mode, env) {
 		devtool: mode === 'production' ? undefined : 'eval-source-map',
 		output: {
 			filename: '[name].js',
-			path: path.resolve(__dirname, 'dist/webviews'),
+			path: path.join(__dirname, 'dist/webviews'),
 			publicPath: '#{root}/dist/webviews/',
 		},
 		module: {
 			rules: [
 				{
 					exclude: /\.d\.ts$/,
-					include: path.resolve(__dirname, 'src'),
+					include: path.join(__dirname, 'src'),
 					test: /\.tsx?$/,
 					use: {
 						loader: 'ts-loader',
 						options: {
-							configFile: 'tsconfig.webviews.json',
+							configFile: path.join(basePath, 'tsconfig.json'),
 							experimentalWatchApi: true,
 							transpileOnly: true,
 						},
@@ -413,7 +415,7 @@ function getWebviewsConfig(mode, env) {
 		},
 		resolve: {
 			extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
-			modules: [path.resolve(__dirname, 'src/webviews/apps'), 'node_modules'],
+			modules: [basePath, 'node_modules'],
 			symlinks: false,
 		},
 		plugins: plugins,