1st commit

This commit is contained in:
Damien HENRY 2022-06-04 20:04:25 +02:00
commit 159683d7ba
17 changed files with 3540 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vscode

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "ui"]
path = ui
url = https://github.com/mydraft-cc/ui.git

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
ui/*

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "mydraftcc-nodejs-server",
"version": "1.0.0",
"description": "NodeJs Server For mydraftcc/ui",
"scripts": {
"populatepublicserver": "rsync -ravH ui/build/* server/public/"
},
"author": "damien HENRY - https://www.mytinydc.com",
"license": "MIT Licence"
}

49
patch.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
###############
## D.HENRY (https://www.mytindydc.com)
# Patchs are apply to be API nodejs compliant
# UserReport because i don't want third party in the running app
################################
echo "[INFO]Starting applying patchs..."
## Usefull for nodejs-server
match="(const API_URL.*)(https:\/\/api.mydraft.cc)(.*)$"
file="ui/src/wireframes/model/actions/loading.ts"
echo "[Patching]$match-$file"
# i replace https://api.mydraft.cc with ''
sed -i -E "s,$match,\1\3," "$file"
echo " [INFO]done"
## Usefull for nodejs-server
echo "[Patching]Replacing load route / with /get"
sed -i 's,${API_URL}/${args.tokenToRead},${API_URL}/get/${args.tokenToRead},' "$file"
if [ "$?" != "0" ];then exit 1;fi
echo " [INFO]done"
## Usefull for nodejs-server
## ContentType ERR Content-Type and application/json (needed for express body-parser)
echo "[Patching]ContentType-application/json"
sed -i -E "s,ContentType.*text\/json(.*)$,'Content-Type': 'application\/json\1," "$file"
if [ "$?" != "0" ];then exit 1;fi
echo " [INFO]done"
## Usefull for ... me :)
echo "[Patching]Removing UserReport from application"
## Remove UserReport support
if [ -f "ui/src/core/react/UserReport.tsx" ];then
rm -r ui/src/core/react/UserReport.tsx
if [ "$?" != "0" ];then exit 1;fi
fi
## Patch tsx
files=$(grep -r "UserReport" ui/src | awk -F ":" '{print $1}' |uniq |xargs)
for f in $files
do
echo " [INFO]Patching UserReport in $f"
sed -i -E 's/.*UserReport.*//' "$f"
if [ "$?" != "0" ];then exit 1;fi
done
echo " [INFO]done"

24
server/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
/data/*
/logs/*
# testing
/coverage
/logs/*
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

9
server/README.md Normal file
View File

@ -0,0 +1,9 @@
# minimalist Nodejs server for mydraft application (https:/mydraft.cc)
## Swagger
## Installation (Docker)
## Execute
pay **ATTENTION**

138
server/app.js Normal file
View File

@ -0,0 +1,138 @@
require("include-path")(["./lib"]);
const express = require("express");
//to parser body as json fetch must have parameter
//headers: {
// 'Content-Type': 'application/json',
//},
const bodyParser = require("body-parser");
const path = require("path");
const cookieParser = require("cookie-parser");
const http = require("http");
//Helpers
const HelperXhr = require("HelperXhr");
//Logs nodejs
//--> http: morgan
const morgan = require("morgan");
const logger = require("logger");
//cors
const cors = require("cors");
//framework express
const app = express();
//Body parser
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//API routes
const routes = require("./routes.js");
//Swagger UI
const swaggerUi = require("swagger-ui-express");
const YAML = require("yamljs");
const swaggerDocumentCore = YAML.load("./openapi.yaml");
const swaggeroptions = {
explorer: true,
};
//cors options - no options
app.use(cors({}));
/**
* explainations : https://dev.to/p0oker/why-is-my-browser-sending-an-options-http-request-instead-of-post-5621
*/
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
res.header(
"Access-Control-Allow-Headers",
"Content-Type, InstantKey, Content-Length, X-Requested-With"
);
//OPTIONS methods interception
if ("OPTIONS" === req.method) {
res.sendStatus(200);
} else {
next();
}
});
// listening port server
app.set("PORT", process.env.PORT || "4000");
// listening IP address server
app.set("IPADDRESS", process.env.IPADDRESS || "0.0.0.0");
// Max size allowed to upload Json files (Default is 2Mo)
app.set("JSONMAXSIZE", process.env.JSONMAXSIZE || 2 * 1024 * 1024);
//logger become global to controller via req.app.get('logger')
app.set("logger", logger);
//Application logs - format see wiki about morgan logs
let loghttpformat = "combined";
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
//set log http
app.use(morgan(loghttpformat));
// Routes
try {
// route / load public content
app.use("/", express.static(path.join(__dirname, "public")));
// route /api-docs load Swagger UI
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(swaggerDocumentCore, swaggeroptions)
);
// route /xxx-xxx-xxxx where xxx-xxx-xxx is a diagram Id
app.use(
"/:diagramId",
express.static(path.join(__dirname, "public", "index.html"))
);
// API Routes
app.use(routes);
} catch (err) {
// logger.error(err.toString(), err);
logger.error(err.toString(), err);
}
// resource not found - 404 and forward to error handler
app.use(function (req, res, next) {
console.log("Resource is missing");
req.statusCode = 404;
next(new Error(), req, res);
});
// Global error handler
app.use(function (err, req, res, next) {
let statuscode = req.statusCode; //404 received ???
if (!statuscode) statuscode = 500;
if (statuscode !== 404) {
logger.error(err.toString(), err);
message = "Internal Error - see logs";
} else {
message = "404-Resource Not Found";
}
if (HelperXhr.isXhrRequest(req)) {
res.status(statuscode).json({ error: message });
} else {
res.status(statuscode).send("<html><h1>" + message + "</h1></html>");
}
});
const httpServer = http.createServer(app);
//catch httpServer Errors type uncaughtException :EADDRNOTAVAIL EADDRINUSE
process.on("uncaughtException", function (err) {
logger.error(err.toString(), err);
});
//start server
httpServer.listen(app.get("PORT"), app.get("IPADDRESS"), function () {
logger.info({
listeningonipaddress: app.get("IPADDRESS"),
listeningonport: app.get("PORT"),
});
});
module.exports = app;

55
server/lib/HelperXhr.js Normal file
View File

@ -0,0 +1,55 @@
"use strict";
const Libsecurity = require("Libsecurity");
class HelperXhr {
/**
* Return req.xhr state to determine if request is xhr type
* @param {object} req - req Express var
* @return {boolean}
*/
static isXhrRequest(req) {
if (req && req.headers && req.headers.accept) {
return req.headers.accept.indexOf("json") > -1;
}
return false;
}
/**
* Push received Data on object
* @return object
*/
static setSettings(req) {
let obj = {};
console.log(req);
try {
if (req.method.match(/POST|PUT/i)) {
obj = Object.keys(req.body).length > 0 ? req.body : {};
} else if (req.method.match(/GET/i)) {
//no body with GET method !! use query.data
obj = req.query && req.query.data ? JSON.parse(req.query.data) : {};
} else {
throw new Error(
"HelperXhr::setSettings-req.method not supported-" + req.method
);
}
// // Parsing req.body API receive only json so string is JSON
// if (typeof obj === "string") {
// obj = JSON.parse(obj);
// }
//check size
if (typeof obj === "object") {
Libsecurity.jsonSizeIsAcceptable(obj, req.app.get("JSONMAXSIZE"));
}
//url paramèters are also put in object
if (req.params && typeof req.params === "object") {
for (let param in req.params) {
obj[param] = req.params[param];
}
}
return obj;
} catch (error) {
//if called from controller to call express error handler
throw error;
}
}
}
module.exports = HelperXhr;

36
server/lib/Libsecurity.js Normal file
View File

@ -0,0 +1,36 @@
"use strict";
class Libsecurity {
/**
*
* @param {object} obj
* @param {number} maxsize
* @returns
*/
static jsonSizeIsAcceptable(obj, maxsize) {
const size = JSON.stringify(obj).length;
if (typeof obj === "object" && size > maxsize)
throw new Error(
"Warning Date received exceed defined max size - Libsecurity",
"size received:",
obj.length,
"acceptable",
size
);
return true;
}
/**
*
* @param {string} str - filename to sanitize
* @returns
*/
static sanitizeFileName(str) {
return str
.replace(/(.*\/)|(\/.*)/g, "")
.replace(/\.\./g, "")
.replace(/;/g, "");
}
}
module.exports = Libsecurity;

47
server/lib/logger.js Normal file
View File

@ -0,0 +1,47 @@
const { createLogger, format, transports, config } = require("winston");
//exception log filename
const exceptionlogfile = __dirname + "/../logs/exceptions.log";
const rejectionslogfile = __dirname + "/../logs/rejections.log";
const errorslogfile = __dirname + "/../logs/errors.log";
const debugslogfile = __dirname + "/../logs/debug.log";
/**
* Levels winston
* {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
*/
const logger = createLogger({
transports: [
new transports.Console({
level: "info",
format: format.combine(
// format.colorize(),
format.timestamp(),
format.json()
),
}),
new transports.File({
level: "error",
format: format.combine(format.timestamp(), format.json()),
filename: errorslogfile,
}),
new transports.File({
level: "debug",
format: format.combine(format.timestamp(), format.json()),
filename: debugslogfile,
}),
],
exceptionHandlers: [new transports.File({ filename: exceptionlogfile })],
rejectionHandlers: [new transports.File({ filename: rejectionslogfile })],
//see winston documentation https://www.npmjs.com/package/winston#logging-levels
exitOnError: false,
});
module.exports = logger;

167
server/openapi.yaml Normal file
View File

@ -0,0 +1,167 @@
openapi: 3.0.0
info:
version: 3.0.0
description: "Nodejs API server for https://mydraft.cc - UI"
title: "Mydraft Nodejs server"
contact:
website: www.mytinydc.com
license:
name: "MIT Licence"
url: "https://mit-license.org/"
tags:
- name: "manage"
description: "Load/Save draft"
servers:
- url: 'http://localhost:4000'
components:
schemas:
serverresponsestore:
type: object
properties:
writeToken:
type: string
readToken:
type: string
internalerror:
type: object
properties:
error:
type: string
diagrammestructure:
type: array
items:
type: object
paths:
/:
post:
tags:
- "manage"
summary: "Store new draft document"
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
examples:
JsonOK:
value: [{"type":"diagram/add","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256184083}},{"type":"items/addVisual","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256186400,"shapeId":"82170ec5-2cc5-c0f2-1414-38e8e477dd01","renderer":"Button","position":{"x":199.5,"y":119.30000305175781}}}]
badFormat:
value: 'This is not JSON format'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/serverresponsestore'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication
/{tokenToRead}/{tokenToWrite}:
put:
tags:
- "manage"
summary: "Update draft document"
parameters:
- name: "tokenToRead"
in: "path"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
- name: "tokenToWrite"
in: "path"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
examples:
JsonOK:
value: [{"type":"diagram/add","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256184083}},{"type":"items/addVisual","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256186400,"shapeId":"82170ec5-2cc5-c0f2-1414-38e8e477dd01","renderer":"Button","position":{"x":199.5,"y":119.30000305175781}}}]
badFormat:
value: 'This is not JSON format'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/serverresponsestore'
"400":
description: "probleme with body content JSON structure"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication
/get/{diagramID}:
get:
tags:
- "manage"
summary: "return JSON Object"
parameters:
- name: "diagramID"
in: "path"
description: "diagramme ID"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
RunDir:
value: '../f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication

2493
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
server/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "mydraftcc-nodejs-server",
"version": "1.0.0",
"description": "built by https://wwww.mytinydc.com - Mydraftcc - NodesJs server",
"author": "Damien HENRY - https://www.mytinydc.com",
"license": "MIT License",
"main": "app.js",
"directories": {
"lib": "lib"
},
"scripts": {
"debug-startserver": "IPADDRESS=0.0.0.0 PORT=4000 JSONMAXSIZE=2097152 nodemon -w ./ app.js"
},
"dependencies": {
"axios": "^0.21.4",
"body-parser": "^1.20.0",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"ejs": "^3.1.7",
"express": "~4.17.1",
"express-session": "^1.17.1",
"http-errors": "~1.6.3",
"include-path": "^0.4.7",
"morgan": "~1.9.1",
"swagger-ui-express": "^4.4.0",
"winston": "^3.3.3",
"yamljs": "^0.3.0"
}
}

95
server/routes.js Normal file
View File

@ -0,0 +1,95 @@
"use strict";
/**
* Not use logger here use try catch and next(Error) in catch bloc to call the global error handler
*/
const express = require("express");
const router = express.Router();
const HelperXhr = require("HelperXhr");
const Libsecurity = require("Libsecurity");
const fs = require("fs");
const path = require("path");
//Dir storage
const datadir = __dirname + "/data";
let response = "";
let codestatus = 500;
/**
* see swagger model
* Store json file
*/
router.post("/", async function (req, res, next) {
try {
//check data received
let xhr = HelperXhr.setSettings(req);
console.log(xhr);
if (Array.isArray(xhr) && xhr[0].payload.diagramId) {
const id = xhr[0].payload.diagramId;
if (id) {
if (fs.existsSync(datadir)) {
fs.writeFileSync(datadir + "/" + id, JSON.stringify(xhr));
codestatus = 200;
response = { writeToken: id, readToken: id };
}
}
} else {
throw new Error("Json structure is wrong");
}
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
/**
* see swagger model
*/
router.put("/:tokenWrite/:tokenRead", async function (req, res, next) {
try {
const tokenWrite = req.params.tokenWrite;
const tokenRead = req.params.tokenRead;
//check data received
let xhr = HelperXhr.setSettings(req);
if (
Array.isArray(xhr) &&
xhr[0] &&
xhr[0].payload &&
xhr[0].payload.diagramId &&
tokenWrite &&
tokenWrite === tokenRead &&
tokenWrite === xhr[0].payload.diagramId
) {
const id = xhr[0].payload.diagramId;
if (id) {
fs.writeFileSync(datadir + "/" + id, JSON.stringify(xhr));
codestatus = 200;
response = { writeToken: id, readToken: id };
}
} else {
throw new Error("Json structure is wrong");
}
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
/**
* 2 cases
* - isXhr request return json
* - else public/index.html
*/
router.get("/get/:diagramId", function (req, res, next) {
try {
const id = Libsecurity.sanitizeFileName(req.params.diagramId);
response = JSON.parse(fs.readFileSync(datadir + "/" + id, "utf8"));
codestatus = 200;
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
module.exports = router;

1
start.sh Executable file
View File

@ -0,0 +1 @@
/usr/bin/nodejs app.js

View File

@ -0,0 +1,382 @@
/* eslint-disable prefer-spread */
/* eslint-disable prefer-rest-params */
/* eslint-disable global-require */
const webpack = require('webpack');
const path = require('path');
const appRoot = path.resolve(__dirname, '..');
function root() {
const newArgs = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [appRoot].concat(newArgs));
}
const plugins = {
// https://github.com/webpack-contrib/mini-css-extract-plugin
MiniCssExtractPlugin: require('mini-css-extract-plugin'),
// https://github.com/dividab/tsconfig-paths-webpack-plugin
TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'),
// https://github.com/aackerman/circular-dependency-plugin
CircularDependencyPlugin: require('circular-dependency-plugin'),
// https://github.com/jantimon/html-webpack-plugin
HtmlWebpackPlugin: require('html-webpack-plugin'),
// https://webpack.js.org/plugins/terser-webpack-plugin/
TerserPlugin: require('terser-webpack-plugin'),
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
CssMinimizerPlugin: require('css-minimizer-webpack-plugin'),
// https://webpack.js.org/plugins/eslint-webpack-plugin/
ESLintPlugin: require('eslint-webpack-plugin'),
// https://github.com/webpack-contrib/stylelint-webpack-plugin
StylelintPlugin: require('stylelint-webpack-plugin'),
// https://www.npmjs.com/package/webpack-bundle-analyzer
BundleAnalyzerPlugin: require('webpack-bundle-analyzer').BundleAnalyzerPlugin,
// https://github.com/jantimon/favicons-webpack-plugin
FaviconsWebpackPlugin: require('favicons-webpack-plugin'),
// https://github.com/GoogleChrome/workbox/tree/master/packages/workbox-webpack-plugin
GenerateSW: require('workbox-webpack-plugin').GenerateSW,
};
module.exports = function configure(env) {
const isProduction = env && env.production;
const isTests = env && env.target === 'tests';
const isTestCoverage = env && env.coverage;
const isAnalyzing = isProduction && env.analyze;
const config = {
mode: isProduction ? 'production' : 'development',
/**
* Source map for Karma from the help of karma-sourcemap-loader & karma-webpack.
*
* See: https://webpack.js.org/configuration/devtool/
*/
devtool: isProduction ? false : 'inline-source-map',
/**
* Options affecting the resolving of modules.
*
* See: https://webpack.js.org/configuration/resolve/
*/
resolve: {
/**
* An array of extensions that should be used to resolve modules.
*
* See: https://webpack.js.org/configuration/resolve/#resolve-extensions
*/
extensions: ['.ts', '.tsx', '.js', '.mjs', '.css', '.scss'],
modules: [
root('src'),
root('src', 'style'),
root('node_modules'),
],
plugins: [
new plugins.TsconfigPathsPlugin({
configFile: 'tsconfig.json',
}),
],
},
/**
* Options affecting the normal modules.
*
* See: https://webpack.js.org/configuration/module/
*/
module: {
/**
* An array of Rules which are matched to requests when modules are created.
*
* See: https://webpack.js.org/configuration/module/#module-rules
*/
rules: [{
test: /\.html$/,
use: [{
loader: 'raw-loader',
}],
}, {
test: /\.d\.ts?$/,
use: [{
loader: 'ignore-loader',
}],
include: [/node_modules/],
}, {
test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[contenthash].[ext]',
// Store the assets in custom path because of fonts need relative urls.
outputPath: 'assets',
},
}],
}, {
test: /\.css$/,
use: [{
loader: plugins.MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader',
}, {
loader: 'postcss-loader',
}],
}],
},
plugins: [
/**
* Puts each bundle into a file without the hash.
*
* See: https://github.com/webpack-contrib/mini-css-extract-plugin
*/
new plugins.MiniCssExtractPlugin({
filename: '[name].css',
}),
new webpack.LoaderOptionsPlugin({
options: {
htmlLoader: {
/**
* Define the root for images, so that we can use absolute urls.
*
* See: https://github.com/webpack/html-loader#Advanced_Options
*/
root: root('src', 'images'),
},
context: '/',
},
}),
new plugins.FaviconsWebpackPlugin({
// Favicon source logo
logo: 'src/images/logo-square.png',
// Favicon app title
title: 'MyDraft',
favicons: {
appName: 'mydraft.cc',
appDescription: 'Open Source Wireframe Editor',
developerName: 'Sebastian Stehle',
developerUrl: 'https://sstehle.com',
start_url: '/',
},
}),
new plugins.StylelintPlugin({
files: '**/*.scss',
}),
/**
* Detect circular dependencies in app.
*
* See: https://github.com/aackerman/circular-dependency-plugin
*/
new plugins.CircularDependencyPlugin({
exclude: /([\\/]node_modules[\\/])/,
// Add errors to webpack instead of warnings
failOnError: true,
}),
],
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
historyApiFallback: true,
proxy: {
context: () => true,
target: "http://localhost:4000",
},
},
};
if (!isTests) {
/**
* The entry point for the bundle. Our React app.
*
* See: https://webpack.js.org/configuration/entry-context/
*/
config.entry = {
src: './src/index.tsx',
};
if (isProduction) {
config.output = {
/**
* The output directory as absolute path (required).
*
* See: https://webpack.js.org/configuration/output/#output-path
*/
path: root('/build/'),
publicPath: './',
/**
* Specifies the name of each output file on disk.
*
* Do NOT append hash to service worker in development mode, so we can load them directly.
*
* See: https://webpack.js.org/configuration/output/#output-filename
*/
filename: (pathData) => {
return pathData.chunk.name === 'src' ? '[name].[contenthash:8].js' : '[name].js';
},
/**
* The filename of non-entry chunks as relative path inside the output.path directory.
*
* See: https://webpack.js.org/configuration/output/#output-chunkfilename
*/
chunkFilename: '[id].[contenthash].chunk.js',
};
} else {
config.output = {
filename: '[name].[contenthash].js',
/**
* Set the public path, because we are running the website from another port (5000).
*/
publicPath: 'https://localhost:3002/',
/*
* Fix a bug with webpack dev server.
*
* See: https://github.com/webpack-contrib/worker-loader/issues/174
*/
globalObject: 'this',
};
}
config.plugins.push(
new plugins.HtmlWebpackPlugin({
hash: true,
chunks: ['src'],
chunksSortMode: 'manual',
template: 'src/index.html',
}),
new plugins.HtmlWebpackPlugin({
hash: true,
chunks: ['src'],
chunksSortMode: 'manual',
template: 'src/index.html',
filename: '404.html',
}),
);
config.plugins.push(
new plugins.ESLintPlugin({
files: [
'./src/**/*.ts',
],
}),
);
}
if (isProduction) {
config.optimization = {
minimizer: [
new plugins.TerserPlugin({
terserOptions: {
compress: true,
ecma: 5,
mangle: true,
output: {
comments: false,
},
safari10: true,
},
extractComments: true,
}),
new plugins.CssMinimizerPlugin({}),
],
};
config.performance = {
hints: false,
};
}
if (isTestCoverage) {
// Do not instrument tests.
config.module.rules.push({
test: /\.ts[x]?$/,
use: [{
loader: 'ts-loader',
}],
include: [/\.(e2e|spec)\.ts$/],
});
// Use instrument loader for all normal files.
config.module.rules.push({
test: /\.ts[x]?$/,
use: [{
loader: '@jsdevtools/coverage-istanbul-loader?esModules=true',
}, {
loader: 'ts-loader',
}],
exclude: [/\.(e2e|spec)\.ts$/],
});
} else {
config.module.rules.push({
test: /\.ts[x]?$/,
use: [{
loader: 'ts-loader',
}],
});
}
if (isProduction) {
config.plugins.push(new plugins.GenerateSW({
swDest: 'service-worker2.js',
// Do not wait for activation
skipWaiting: true,
// Cache until 5MB
maximumFileSizeToCacheInBytes: 5000000000,
}));
}
if (isProduction) {
config.module.rules.push({
test: /\.scss$/,
/*
* Extract the content from a bundle to a file.
*
* See: https://github.com/webpack-contrib/extract-text-webpack-plugin
*/
use: [
plugins.MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
}, {
loader: 'postcss-loader',
}, {
loader: 'sass-loader',
}],
});
} else {
config.module.rules.push({
test: /\.scss$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader',
}, {
loader: 'postcss-loader',
}, {
loader: 'sass-loader',
options: {
sourceMap: true,
},
}],
});
}
if (isAnalyzing) {
config.plugins.push(new plugins.BundleAnalyzerPlugin());
}
return config;
};