Compare commits

..

No commits in common. "gh-pages" and "master" have entirely different histories.

28 changed files with 13899 additions and 46 deletions

9
.eslintrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
"extends": "airbnb-base",
"plugins": [
"import"
],
"env": {
"browser": true,
}
};

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
tags
Session.vim

1
CNAME Normal file
View File

@ -0,0 +1 @@
transport.hallada.net

20
LICENSE.txt Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Tyler Hallada
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# Transport
Work-in-progress procedurally generated train network simulation written in
Typescript with PixiJs.
![Screenshot of simulation](img/screenshot.png)
## Development
Clone the repo and then run:
```bash
npm install
npm start
```
Then visit `http://localhost:8888` in your browser to view the development
build. Edit files in `/src` and see the changes reflected in the browser.
## Test
No tests right now. But, to run lint checks:
```bash
npm run lint
```
## Deploy
Run:
```bash
npm run build
npm run deploy
```

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View File

@ -12,9 +12,9 @@
<!-- <meta property="og:image:height" content="300" /> --> <!-- <meta property="og:image:height" content="300" /> -->
<!-- <meta property="og:image:alt" content="Screenshot of the animation in action" /> --> <!-- <meta property="og:image:alt" content="Screenshot of the animation in action" /> -->
<!-- <meta property="og:description" content="A procedurally generated and interactive animation created with PixiJS" /> --> <!-- <meta property="og:description" content="A procedurally generated and interactive animation created with PixiJS" /> -->
<link href="main.css" rel="stylesheet"></head> </head>
<body> <body>
<script type="text/javascript" src="vendors.min.js"></script><script type="text/javascript" src="main.min.js"></script></body> </body>
<!-- Google Analytics --> <!-- Google Analytics -->
<script> <script>

View File

@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"main.css","sourceRoot":""}

2
main.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13052
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "transport",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --mode development",
"build": "NODE_ENV=production ./node_modules/.bin/webpack --mode production && cp CNAME dist/",
"deploy": "./node_modules/.bin/gh-pages -d dist --remote github",
"lint": "./node_modules/.bin/tslint src/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.hallada.net:pixi.git"
},
"author": "Tyler Hallada",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0-beta.42",
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.42",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.42",
"@babel/preset-env": "^7.0.0-beta.42",
"babel-loader": "^8.0.0-beta.2",
"css-loader": "^0.28.11",
"eslint": "^3.17.0",
"eslint-config-airbnb-base": "^11.1.1",
"eslint-config-standard": "^7.0.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^1.1.11",
"gh-pages": "^1.1.0",
"html-webpack-plugin": "^3.1.0",
"style-loader": "^0.20.3",
"ts-loader": "^4.1.0",
"tslint": "^5.9.1",
"tslint-config-airbnb": "^5.8.0",
"typescript": "^2.8.1",
"uglifyjs-webpack-plugin": "^1.2.4",
"webpack": "^4.4.1",
"webpack-cli": "^2.0.13",
"webpack-dev-server": "^3.1.1"
},
"dependencies": {
"@types/pixi.js": "^4.7.2",
"@types/stats.js": "^0.17.0",
"@types/tinycolor2": "^1.4.0",
"pixi-viewport": "^1.5.0",
"pixi.js": "^4.7.1",
"stats.js": "^0.17.0",
"tinycolor2": "^1.4.1"
}
}

41
src/Direction.ts Normal file
View File

@ -0,0 +1,41 @@
import * as PIXI from 'pixi.js';
import { angleDegrees } from './utils';
enum Direction {
North, // 0
Northeast, // 1
East, // 2
Southeast, // 3
South, // 4
Southwest, // 5
West, // 6
Northwest, // 7
}
export const getPointDirection = (pointA: PIXI.Point, pointB: PIXI.Point): Direction => {
const angle = angleDegrees(pointA, pointB);
let direction = null;
if (angle >= 337.5 || angle < 22.5) {
direction = Direction.North;
} else if (angle >= 22.5 && angle < 67.5) {
direction = Direction.Northeast;
} else if (angle >= 67.5 && angle < 112.5) {
direction = Direction.East;
} else if (angle >= 112.5 && angle < 157.5) {
direction = Direction.Southeast;
} else if (angle >= 157.5 && angle < 202.5) {
direction = Direction.South;
} else if (angle >= 202.5 && angle < 247.5) {
direction = Direction.Southwest;
} else if (angle >= 247.5 && angle < 292.5) {
direction = Direction.West;
} else if (angle >= 292.5 && angle < 337.5) {
direction = Direction.Northwest;
} else {
throw Error('Angle between points is not a valid degree');
}
return direction;
};
export default Direction;

58
src/Line.ts Normal file
View File

@ -0,0 +1,58 @@
import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import Station from './Station';
import { distance, randomInt, randomPoint } from './utils';
const CONNECTION_RADIUS = Math.floor(Math.sqrt(
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
) / 8);
export default class Line {
public name: string;
public color: tinycolorInstance;
constructor(
name: string,
color: tinycolorInstance,
) {
this.name = name;
this.color = color;
}
public connectStations(
currentStation: Station,
stations: Station[],
visitedStations: Station[],
connectionLimit: number,
) {
visitedStations.push(currentStation);
const otherStations = stations.filter(station => station !== currentStation);
const closeStations = Station.stationsWithinRadius(
otherStations,
currentStation.location,
CONNECTION_RADIUS,
);
for (let i = 0; i < connectionLimit; i += 1) {
if (closeStations.length < 1) {
break;
}
const largest = Station.largestStation(closeStations);
currentStation.connections.push(
new LineConnection(largest, this),
);
closeStations.splice(closeStations.indexOf(largest), 1);
}
for (const connectedStation of currentStation.connections) {
if (visitedStations.indexOf(connectedStation.station) === -1) {
this.connectStations(
connectedStation.station,
stations,
visitedStations,
connectionLimit,
);
}
}
}
}

15
src/LineConnection.ts Normal file
View File

@ -0,0 +1,15 @@
import Line from './Line';
import Station from './Station';
export default class LineConnection {
public station: Station;
public line: Line;
constructor(
station: Station,
line: Line,
) {
this.station = station;
this.line = line;
}
}

11
src/Signal.ts Normal file
View File

@ -0,0 +1,11 @@
import * as PIXI from 'pixi.js';
export default class Signal {
public location: PIXI.Point;
constructor(
location: PIXI.Point,
) {
this.location = location;
}
}

101
src/Station.ts Normal file
View File

@ -0,0 +1,101 @@
import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import { distance, randomPoint, weightedRandom } from './utils';
let stationCount = 0;
export default class Station {
// Utility methods for working with arrays of Stations
public static largestStation(stations: Station[]): Station {
let largest: Station = null;
for (const station of stations) {
if (largest === null || station.population > largest.population) {
largest = station;
}
}
return largest;
}
public static stationsWithinRadius(stations: Station[], point: PIXI.Point,
radius: number): Station[] {
return stations.filter(station => distance(point, station.location) <= radius);
}
public static stationsInDirection(stations: Station[], point: PIXI.Point,
direction: Direction): Station[] {
return stations.filter(station => getPointDirection(point, station.location) === direction);
}
public static closestStation(stations: Station[], point: PIXI.Point): Station {
return stations.reduce(
(prev, curr) => distance(point, prev.location) < distance(point, curr.location) ? prev : curr,
);
}
public static randomCloseLargeStation(stations: Station[], point: PIXI.Point,
radius: number): Station {
const closeStations = Station.stationsWithinRadius(stations, point,
radius);
const closeStationWeights = closeStations.map(station => station.population);
return weightedRandom(closeStations, closeStationWeights);
}
public static isPointDistant(point: PIXI.Point, stations: Station[],
minDistance: number): boolean {
for (const station of stations) {
if (distance(point, station.location) < minDistance) {
return false;
}
}
return true;
}
public static randomDistantPoint(stations: Station[], minDistance: number): PIXI.Point | null {
let tries = 100;
while (tries > 0) {
const point = randomPoint();
if (Station.isPointDistant(point, stations, minDistance)) {
return point;
}
tries -= 1;
}
return null;
}
public location: PIXI.Point;
public population: number;
public connections: LineConnection[];
public id: number;
public label: PIXI.Text;
public color: tinycolorInstance;
private textStyle: object;
constructor(
location: PIXI.Point,
population: number,
color: tinycolorInstance,
connections?: LineConnection[],
) {
this.location = location;
this.population = population;
this.color = color;
this.connections = connections || [];
// for debugging
stationCount += 1;
this.id = stationCount;
this.textStyle = {
fill: '#FFA500',
fontFamily: 'monospace',
fontSize: '12px',
};
this.renderLabel();
}
public renderLabel() {
this.label = new PIXI.Text(`${this.id}`, this.textStyle);
}
}

57
src/Train.ts Normal file
View File

@ -0,0 +1,57 @@
import * as tinycolor from 'tinycolor2';
import Station from './Station';
let trainCount = 0;
export default class Train {
public location: PIXI.Point;
public speed: number;
public origin: Station;
public destination: Station;
public passengers: number;
public id: number;
public label: PIXI.Text;
public color: tinycolorInstance;
public sprite: PIXI.Sprite;
private textStyle: object;
constructor(
location: PIXI.Point,
speed: number,
passengers: number,
origin: Station,
destination: Station,
color: tinycolorInstance,
) {
this.location = location;
this.speed = speed;
this.origin = origin;
this.destination = destination;
this.passengers = passengers;
this.color = color;
this.sprite = new PIXI.Sprite(PIXI.loader.resources.nodeImg.texture);
this.sprite.visible = false;
// for debugging
trainCount += 1;
this.id = trainCount;
this.textStyle = {
fill: '#AEAEAE',
fontFamily: 'monospace',
fontSize: '12px',
};
this.renderLabel();
}
public boardPassengers() {
if (this.location === this.origin.location) { // about to leave a station
}
}
public renderLabel() {
this.label = new PIXI.Text(`${this.id}`, this.textStyle);
}
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -18,5 +18,3 @@ canvas {
bottom: 0 !important; bottom: 0 !important;
top: 0 !important; top: 0 !important;
} }
/*# sourceMappingURL=main.css.map*/

269
src/transport.ts Normal file
View File

@ -0,0 +1,269 @@
import * as Viewport from 'pixi-viewport';
import * as PIXI from 'pixi.js';
import * as Stats from 'stats.js';
import * as tinycolor from 'tinycolor2';
import Direction from './Direction';
import Line from './Line';
import Station from './Station';
import Train from './Train';
import { distance, pointsAlmostEqual, pointsEqual, randomInt, randomPoint,
rangeMap, weightedRandom } from './utils';
import * as imgNode from './node.png';
import './style.css';
const NODE_RES = 100;
const MAX_SPEED = 10.0;
const ACCELERATION = 0.025;
const APPROACH_DISTANCE = 3.0;
const TRAIN_CAPACITY = 50;
const LINE_CONNECTION_LIMIT = 5;
const WORLD_WIDTH = 1000;
const WORLD_HEIGHT = 1000;
const ZOOM_MIN_WIDTH = 100;
const ZOOM_MIN_HEIGHT = 100;
const ZOOM_MAX_WIDTH = 4000;
const ZOOM_MAX_HEIGHT = 4000;
const trainTexts: PIXI.Text[] = [];
const initStations = (numStations: number): Station[] => {
const stations: Station[] = [];
for (let i = 0; i < numStations; i += 1) {
stations.push(new Station(
Station.randomDistantPoint(stations, 30),
randomInt(300, 2000),
tinycolor.random()));
}
return stations;
};
const initTrains = (numTrains: number, stations: Station[]): Train[] => {
const trains = [];
const stationsWithConnections = stations.filter(station => station.connections.length > 0);
for (let i = 0; i < numTrains; i += 1) {
const originStation = stationsWithConnections[
Math.floor(Math.random() * stationsWithConnections.length)
];
trains.push(new Train(
new PIXI.Point(originStation.location.x, originStation.location.y),
0, 0, originStation, undefined, tinycolor('grey')),
);
}
return trains;
};
const initLines = (numLines: number, stations: Station[]): Line[] => {
const lines = [];
for (let i = 0; i < numLines; i += 1) {
let color = tinycolor.random();
while (color.isDark()) {
color = tinycolor.random();
}
const stationsWithoutConnections = stations.filter(station =>
station.connections.length === 0,
);
let centralHub: Station;
if (stationsWithoutConnections.length > 0) {
centralHub = Station.largestStation(stationsWithoutConnections);
} else {
centralHub = stations[randomInt(0, stations.length - 1)];
}
const line = new Line(`line-${i}`, tinycolor.random());
const stationsLeft = stations.slice(0);
line.connectStations(centralHub, stationsLeft, [], LINE_CONNECTION_LIMIT);
lines.push(line);
}
return lines;
};
const moveTrains = (trains: Train[], stations: Station[]) => {
for (const train of trains) {
if (train.origin.connections.length === 0) {
// train is stuck at an orphaned station
continue;
}
// choose a destination randomly with a bias towards larger stations
if (train.destination === undefined) {
const otherStations = train.origin.connections.map(conn => conn.station);
const closeStationWeights = otherStations.map(station => station.population);
train.destination = weightedRandom(otherStations, closeStationWeights);
// board passengers
const boardingPassengers = randomInt(0, Math.min(TRAIN_CAPACITY - train.passengers,
train.origin.population));
// set or mix train color with the color of new passenger origin
if (train.passengers === 0) {
train.color = train.origin.color;
} else {
train.color = tinycolor.mix(
train.color,
train.origin.color,
Math.round((boardingPassengers / train.passengers) * 100),
);
}
train.passengers += boardingPassengers;
train.origin.population -= boardingPassengers;
}
// train reached destination, stop moving and let passengers off
if (pointsAlmostEqual(train.location, train.destination.location)) {
train.speed = 0;
// average destination color with passenger color weighted by ratio
// (a simulation of culture mixing)
train.destination.color = tinycolor.mix(
train.destination.color,
train.origin.color,
Math.round((train.passengers / train.destination.population) * 100),
);
// transfer passengers to destination
const disembarkingPassengers = randomInt(0, train.passengers);
train.destination.population += disembarkingPassengers;
train.passengers -= disembarkingPassengers;
// prepare for next journey
train.origin = train.destination;
train.destination = undefined;
continue;
}
const journeyLeft = distance(train.location, train.destination.location);
if ((train.speed / ACCELERATION) >= ((journeyLeft / train.speed) - APPROACH_DISTANCE) &&
train.speed !== ACCELERATION) {
// slowing down
train.speed -= ACCELERATION;
} else if (train.speed < MAX_SPEED) {
// speeding up
train.speed += ACCELERATION;
}
// advance train
const progress = train.speed / journeyLeft;
train.location.x += ((train.destination.location.x - train.location.x) * progress);
train.location.y += ((train.destination.location.y - train.location.y) * progress);
}
};
const drawStations = (stations: Station[], graphics: PIXI.Graphics) => {
for (const station of stations) {
const radius = station.population / 150;
graphics.beginFill(parseInt(station.color.toHex(), 16), 0.5);
graphics.drawCircle(station.location.x, station.location.y, radius);
graphics.endFill();
station.label.x = station.location.x + radius + 1;
station.label.y = station.location.y + radius + 1;
}
};
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
for (const train of trains) {
const trainSize = rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5);
const scale = trainSize / NODE_RES;
train.sprite.visible = true;
train.sprite.x = train.location.x;
train.sprite.y = train.location.y;
train.sprite.scale.x = scale;
train.sprite.scale.y = scale;
train.sprite.tint = parseInt(train.color.toHex(), 16);
train.label.x = train.location.x + scale + 1;
train.label.y = train.location.y + scale + 1;
}
};
const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
for (const station of stations) {
for (const connection of station.connections) {
let twoWay = false;
for (const conn of connection.station.connections) {
if (conn.station === station) {
twoWay = true;
}
}
graphics.lineStyle(twoWay ? 2 : 1, parseInt(connection.line.color.toHex(), 16), 1);
graphics.moveTo(station.location.x, station.location.y);
graphics.lineTo(connection.station.location.x, connection.station.location.y);
}
}
};
const run = () => {
const app = new PIXI.Application({
antialias: true,
height: window.innerHeight,
width: window.innerWidth,
});
const viewport = new Viewport({
screenHeight: window.innerHeight,
screenWidth: window.innerWidth,
worldHeight: WORLD_HEIGHT,
worldWidth: WORLD_WIDTH,
});
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
const ticker = new PIXI.ticker.Ticker();
const graphics = new PIXI.Graphics();
let stations: Station[] = [];
let lines: Line[] = [];
let stationsWithConnections: Station[] = [];
while (stationsWithConnections.length === 0) {
// If all stations are too far away to connect, try generating again
stations = initStations(30);
lines = initLines(4, stations);
stationsWithConnections = stations.filter(station => station.connections.length > 0);
}
const trains = initTrains(50, stations);
ticker.stop();
ticker.add((deltaTime) => {
stats.begin();
moveTrains(trains, stations);
graphics.clear();
graphics.lineStyle(1, 0xFFA500, 1);
drawStations(stations, graphics);
graphics.lineStyle(1, 0xAEAEAE, 1);
drawTrains(trains, graphics);
drawLines(stations, graphics);
stats.end();
});
ticker.start();
viewport.addChild(graphics);
// add train sprites
for (const train of trains) {
viewport.addChild(train.sprite);
}
// Add debug labels
for (const train of trains) {
viewport.addChild(train.label);
}
for (const station of stations) {
viewport.addChild(station.label);
}
document.body.appendChild(app.view);
app.stage.addChild(viewport);
viewport.drag().pinch().wheel().clampZoom({
maxHeight: ZOOM_MAX_HEIGHT,
maxWidth: ZOOM_MAX_WIDTH,
minHeight: ZOOM_MIN_HEIGHT,
minWidth: ZOOM_MIN_WIDTH,
}).decelerate();
window.addEventListener('resize', () => {
app.renderer.resize(window.innerWidth, window.innerHeight);
});
};
PIXI.loader.add('nodeImg', imgNode).load(run);

52
src/utils.ts Normal file
View File

@ -0,0 +1,52 @@
import * as PIXI from 'pixi.js';
const EPSILON = 1.0;
export const randomInt = (min: number, max: number): number => (
// inclusive of min and max
Math.floor(Math.random() * (max - (min + 1))) + min
);
export const weightedRandom = (choices: any[], weights: number[]): any => {
const totalWeight = weights.reduce((a, b) => a + b, 0);
const rand = randomInt(0, totalWeight);
let cumulWeight = 0;
for (let i = 0; i < weights.length; i += 1) {
cumulWeight += weights[i];
if (rand < cumulWeight) {
return choices[i];
}
}
};
export const randomPoint = () => (
new PIXI.Point(randomInt(0, window.innerWidth), randomInt(0, window.innerHeight))
);
export const pointsEqual = (pointA: PIXI.Point, pointB: PIXI.Point): boolean => (
(pointA.x === pointB.x && pointA.y === pointB.y)
);
export const pointsAlmostEqual = (pointA: PIXI.Point, pointB: PIXI.Point): boolean => (
Math.abs(pointA.x - pointB.x) < EPSILON &&
Math.abs(pointA.y - pointB.y) < EPSILON
);
export const distance = (pointA: PIXI.Point, pointB: PIXI.Point): number => {
const distX = pointA.x - pointB.x;
const distY = pointA.y - pointB.y;
return Math.sqrt((distX * distX) + (distY * distY));
};
export const rangeMap = (num: number, inMin: number, inMax: number,
outMin: number, outMax: number): number => (
(num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
);
export const angleRadians = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
Math.atan2(-(pointB.x - pointA.x), pointB.y - pointA.y)
);
export const angleDegrees = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
180 + angleRadians(pointA, pointB) * (180 / Math.PI)
);

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "es6",
"target": "es6",
"jsx": "react",
"allowJs": true
},
"include": [
"typings"
]
}

11
tslint.json Normal file
View File

@ -0,0 +1,11 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-config-airbnb",
"tslint-eslint-rules"
],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}

1
typings/custom.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '*.png';

37
vendors.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

91
webpack.config.js Normal file
View File

@ -0,0 +1,91 @@
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const env = process.env.NODE_ENV;
const prodPlugins = env === 'production' ? [
new UglifyJsPlugin({
sourceMap: true,
}),
] : [];
module.exports = {
entry: './src/transport.ts',
output: {
filename: env === 'production' ? '[name].min.js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
},
mode: env === 'production' ? 'production' : 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
],
plugins: [
'@babel/proposal-class-properties',
'@babel/proposal-object-rest-spread',
],
cacheDirectory: env === 'development',
},
},
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: env === 'production'
? ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader'],
})
: ['style-loader', 'css-loader'],
},
{
test: /\.png$/,
use: 'file-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.html'),
}),
new ExtractTextPlugin({
disable: env === 'development',
filename: '[name].css',
}),
...prodPlugins,
],
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map',
devServer: {
contentBase: path.join(__dirname, 'dist'),
host: '0.0.0.0',
port: 8888,
disableHostCheck: true,
},
};