chore: adding selenium tests with browserstack (#2570)

Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
This commit is contained in:
Bartlomiej Obecny 2021-11-08 23:05:38 +01:00 committed by GitHub
parent 8227258b4a
commit e8d65f22d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 899 additions and 5 deletions

View File

@ -22,8 +22,9 @@ jobs:
run: npm install semver
- name: Check API dependency semantics (stable)
run: lerna exec --ignore propagation-validation-server "node ../../scripts/peer-api-check.js"
working-directory: packages
run: lerna exec --ignore propagation-validation-server --ignore propagation-validation-server --ignore @opentelemetry/selenium-tests "node ../../scripts/peer-api-check.js"
- name: Check API dependency semantics (experimental)
working-directory: experimental
run: lerna exec --ignore propagation-validation-server "node ../../../scripts/peer-api-check.js"
run: lerna exec --ignore propagation-validation-server --ignore @opentelemetry/selenium-tests "node ../../../scripts/peer-api-check.js"

View File

@ -35,7 +35,7 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: |
npm install --ignore-scripts
npx lerna bootstrap --no-ci --hoist --nohoist='zone.js'
npx lerna bootstrap --no-ci --hoist --nohoist='zone.js' --ignore @opentelemetry/selenium-tests
- name: Build 🔧
run: |
@ -111,7 +111,7 @@ jobs:
working-directory: experimental
run: |
npm install --ignore-scripts
npx lerna bootstrap --no-ci --hoist --nohoist='zone.js'
npx lerna bootstrap --no-ci --hoist --nohoist='zone.js' --ignore @opentelemetry/selenium-tests
- name: Build 🔧
working-directory: experimental

View File

@ -5,6 +5,7 @@
"packages": [
"benchmark/*",
"packages/*",
"integration-tests/*"
"integration-tests/*",
"selenium-tests"
]
}

2
selenium-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
tests_output
tmp

54
selenium-tests/README.md Normal file
View File

@ -0,0 +1,54 @@
## Selenium Tests
Selenium tests are to help verify the working of opentelemetry in different browsers. For that the nightwatch is used.
This can be run either locally or in github actions with usage of browserstack.
Browser stack also gives possibility of running tests in browserstack on different browsers using our local environment.
This helps to test and debug things locally using any browser we want.
## Running tests locally using local browser - this is also useful when adding new test
1. run server
```shell
npm run server
```
1. run local test for example for xhr
```shell
npm run local:xhr
```
Please wait a bit it should run selenium tests using our local version of chrome
## Running tests locally using browser stack account - for that you need to have a browser stack account
If you have it please create a file ".env" based on "example.env"
1. run server
```shell
npm run server
```
1. Run local test for example for xhr
```shell
npm run local:bs:xhr
```
## Architecture
1. Folder pages contains all the pages that can be entered after running `npn run server` at <http://localhost:8090/> .
These are fully functioning pages and can be run without running tests.
2. To be able to test it automatically instead of manually we have to create a test. Tests are kept in folder "tests".
For each page there is a corresponding folder with exactly the same name. There are additional 2 files
- helper.js - this file keeps some helpers functions that are included in page automatically and they are available in tests only in section "execute".
This is because this section is being sent to the browser and executed automatically. This is the only way of "sending" and "reading" data back to test
When data is being sent between browser and selenium it needs to be stringified that's why we have one more helper for that called "JSONSafeStringify"
The last helper is the one that helps to verify test has finished successfully "OTELSeleniumDone".
- tracing.js - this file contains the skeleton for tracing and export function "loadOtel" this is a helper function that accepts instrumentations as param.
This way it is easy to add different tests that has different lists of instrumentations.

View File

@ -0,0 +1,33 @@
module.exports = function (api) {
api.cache(true);
const presets = [
[
'@babel/preset-env',
{
corejs: {
version: '3',
proposals: true,
},
useBuiltIns: 'entry',
targets: {
// 'edge': 16,
// 'safari': 9,
// 'firefox': 57,
'ie': 11,
// 'ios': 9,
// 'chrome': 49,
},
},
],
];
const plugins = [
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
['@babel/plugin-proposal-class-properties', { 'loose': true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
];
return {
presets,
plugins,
};
};

View File

@ -0,0 +1,2 @@
BROWSERSTACK_USER="YOUR USER"
BROWSERSTACK_KEY="YOUR BROWSER STACK KEY"

View File

@ -0,0 +1,287 @@
// Autogenerated by Nightwatch
// Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/
const Services = {}; loadServices();
module.exports = {
// An array of folders (excluding subfolders) where your tests are located;
// if this is not specified, the test source must be passed as the second argument to the test runner.
src_folders: [],
// See https://nightwatchjs.org/guide/working-with-page-objects/
page_objects_path: '',
// See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands
custom_commands_path: '',
// See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions
custom_assertions_path: '',
// See https://nightwatchjs.org/guide/#external-globals
globals_path : '',
webdriver: {},
test_settings: {
default: {
disable_error_log: false,
launch_url: 'https://nightwatchjs.org',
screenshots: {
enabled: false,
path: 'screens',
on_failure: true
},
desiredCapabilities: {
browserName : 'firefox'
},
webdriver: {
start_process: true,
server_path: (Services.geckodriver ? Services.geckodriver.path : '')
}
},
safari: {
desiredCapabilities : {
browserName : 'safari',
alwaysMatch: {
acceptInsecureCerts: false
}
},
webdriver: {
port: 4445,
start_process: true,
server_path: '/usr/bin/safaridriver'
}
},
firefox: {
desiredCapabilities : {
browserName : 'firefox',
alwaysMatch: {
acceptInsecureCerts: true,
'moz:firefoxOptions': {
args: [
// '-headless',
// '-verbose'
]
}
}
},
webdriver: {
start_process: true,
port: 4444,
server_path: (Services.geckodriver ? Services.geckodriver.path : ''),
cli_args: [
// very verbose geckodriver logs
// '-vv'
]
}
},
chrome: {
desiredCapabilities : {
browserName : 'chrome',
'goog:chromeOptions' : {
// More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/
//
// This tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78)
w3c: false,
args: [
//'--no-sandbox',
//'--ignore-certificate-errors',
//'--allow-insecure-localhost',
//'--headless'
]
}
},
webdriver: {
start_process: true,
port: 9515,
server_path: (Services.chromedriver ? Services.chromedriver.path : ''),
cli_args: [
// --verbose
]
}
},
edge: {
desiredCapabilities : {
browserName : 'MicrosoftEdge',
'ms:edgeOptions' : {
w3c: false,
// More info on EdgeDriver: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options
args: [
//'--headless'
]
}
},
webdriver: {
start_process: true,
// Download msedgedriver from https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/
// and set the location below:
server_path: '',
cli_args: [
// --verbose
]
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using the browserstack.com cloud service |
// |
// Please set the username and access key by setting the environment variables: |
// - BROWSERSTACK_USER |
// - BROWSERSTACK_KEY |
// .env files are supported |
//////////////////////////////////////////////////////////////////////////////////
browserstack: {
selenium: {
host: 'hub-cloud.browserstack.com',
port: 443
},
// More info on configuring capabilities can be found on:
// https://www.browserstack.com/automate/capabilities?tag=selenium-4
desiredCapabilities: {
'bstack:options' : {
userName: '${BROWSERSTACK_USER}',
accessKey: '${BROWSERSTACK_KEY}',
}
},
disable_error_log: true,
webdriver: {
timeout_options: {
timeout: 15000,
retry_attempts: 3
},
keep_alive: true,
start_process: false
}
},
'browserstack.local': {
extends: 'browserstack',
desiredCapabilities: {
'browserstack.local': true,
'browserstack.console': 'errors'
}
},
'browserstack.chrome': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'chrome',
chromeOptions : {
w3c: false
}
}
},
'browserstack.firefox': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'firefox'
}
},
'browserstack.ie': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'internet explorer',
browserVersion: '11.0'
}
},
'browserstack.safari': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'safari'
}
},
'browserstack.local_chrome': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'chrome'
}
},
'browserstack.local_firefox': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'firefox'
}
},
'browserstack.local_ie': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'internet explorer',
browserVersion: '11.0'
}
},
'browserstack.local_safari': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'safari',
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using the Selenium service, either locally or remote, |
// like Selenium Grid |
//////////////////////////////////////////////////////////////////////////////////
selenium_server: {
// Selenium Server is running locally and is managed by Nightwatch
selenium: {
start_process: true,
port: 4444,
server_path: (Services.seleniumServer ? Services.seleniumServer.path : ''),
cli_args: {
'webdriver.gecko.driver': (Services.geckodriver ? Services.geckodriver.path : ''),
'webdriver.chrome.driver': (Services.chromedriver ? Services.chromedriver.path : '')
}
}
},
'selenium.chrome': {
extends: 'selenium_server',
desiredCapabilities: {
browserName: 'chrome',
chromeOptions : {
w3c: false,
}
}
},
'selenium.firefox': {
extends: 'selenium_server',
desiredCapabilities: {
browserName: 'firefox',
'moz:firefoxOptions': {
args: [
// '-headless',
// '-verbose'
]
}
}
}
}
};
function loadServices() {
try {
Services.seleniumServer = require('selenium-server');
} catch (err) {}
try {
Services.chromedriver = require('chromedriver');
} catch (err) {}
try {
Services.geckodriver = require('geckodriver');
} catch (err) {}
}

View File

@ -0,0 +1,70 @@
{
"name": "@opentelemetry/selenium-tests",
"version": "0.0.1",
"description": "OpenTelemetry Selenium Tests",
"main": "index.js",
"repository": "open-telemetry/opentelemetry-js",
"scripts": {
"all:bs": "nightwatch ./tests --parallel --env browserstack.chrome,browserstack.safari,browserstack.firefox",
"all:local": "nightwatch ./tests --parallel --env selenium.chrome",
"local:bs:fetch": "node scripts/local.runner.js --test ./tests/fetch/fetch.js --parallel --env browserstack.local_chrome,browserstack.local_firefox,browserstack.local_safari",
"local:bs:xhr": "node scripts/local.runner.js --test ./tests/xhr/xhr.js --parallel --env browserstack.local_chrome,browserstack.local_firefox,browserstack.local_ie,browserstack.local_safari",
"local:fetch": "nightwatch ./tests/fetch/fetch.js --env selenium.chrome",
"local:xhr": "nightwatch ./tests/xhr/xhr.js --env selenium.chrome",
"server": "webpack serve --progress --port 8090 --config webpack.dev.js --hot --host 0.0.0.0"
},
"keywords": [
"opentelemetry",
"web",
"tracing",
"profiling",
"metrics",
"stats"
],
"author": "OpenTelemetry Authors",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0.0"
},
"publishConfig": {
"access": "private"
},
"devDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.15.8",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/preset-env": "^7.15.0",
"@opentelemetry/api": "^1.0.3",
"babel-loader": "^8.2.3",
"babel-polyfill": "^6.26.0",
"browserstack-local": "^1.4.8",
"chromedriver": "^95.0.0",
"dotenv": "^10.0.0",
"fast-safe-stringify": "^2.1.1",
"geckodriver": "^2.0.4",
"nightwatch": "^1.7.11",
"selenium-server": "^3.141.59",
"terser-webpack-plugin": "^5.2.4",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.3.1",
"webpack-merge": "^5.8.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.3"
},
"dependencies": {
"@opentelemetry/context-zone-peer-dep": "^1.0.0",
"@opentelemetry/core": "^1.0.0",
"@opentelemetry/exporter-otlp-http": "^0.26.0",
"@opentelemetry/exporter-zipkin": "^1.0.0",
"@opentelemetry/instrumentation": "^0.26.0",
"@opentelemetry/instrumentation-fetch": "^0.26.0",
"@opentelemetry/instrumentation-xml-http-request": "^0.26.0",
"@opentelemetry/sdk-metrics-base": "^0.26.0",
"@opentelemetry/sdk-trace-base": "^1.0.0",
"@opentelemetry/sdk-trace-web": "^1.0.0",
"zone.js": "^0.10.3"
}
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fetch Plugin</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
Example of using Web Tracer with Fetch plugin and console exporter
<script type="text/javascript" src="fetch.js"></script>
<br/>
<button id="button1">Test</button>
</body>
</html>

View File

@ -0,0 +1,51 @@
'use strict';
import '../helper';
import { context, trace } from '@opentelemetry/api';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { loadOtel } from '../tracing';
const provider = loadOtel([
new FetchInstrumentation({
ignoreUrls: [/localhost:8090\/sockjs-node/],
propagateTraceHeaderCorsUrls: [
'https://cors-test.appspot.com/test',
'https://httpbin.org/get',
],
clearTimingResources: true,
}),
]);
const webTracerWithZone = provider.getTracer('example-tracer-web');
const getData = (url) => fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
// example of keeping track of context between async operations
const prepareClickEvent = () => {
const url = 'https://httpbin.org/get';
const element = document.getElementById('button1');
const onClick = () => {
const singleSpan = webTracerWithZone.startSpan(`files-series-info`);
context.with(trace.setSpan(context.active(), singleSpan), () => {
getData(url).then((_data) => {
trace.getSpan(context.active()).addEvent('fetching-single-span-completed');
singleSpan.end();
}).finally(()=> {
otel.OTELSeleniumDone();
console.log(otel.memoryExporter.getFinishedSpans());
});
});
};
element.addEventListener('click', onClick);
};
window.addEventListener('load', prepareClickEvent);

View File

@ -0,0 +1,46 @@
/**
* These are all needed to make otel to work correctly for IE
*/
window.__Zone_enable_cross_context_check = true;
import 'babel-polyfill';
import 'zone.js';
const safeStringify = require('fast-safe-stringify');
/**
* Function to stringify data between browser and selenium
* It works fine for circular dependency
* @param obj
* @return {*|string}
* @constructor
*/
function JSONSafeStringify(obj) {
return safeStringify(
obj,
function replacer(key, value) {
// Remove the circular structure
if (value === '[Circular]') {
return;
}
return value;
},
2,
);
}
/**
* Function helper to mark action on page as finished. This way selenium understands that test is finished
*/
function OTELSeleniumDone() {
const element = document.createElement('div');
element.setAttribute('id', 'otelSeleniumDone');
element.innerHTML = 'OTELSeleniumDone';
window.setTimeout(() => {
document.body.appendChild(element);
}, 1000);
}
window.otel = Object.assign({}, window.otel, {
JSONSafeStringify: JSONSafeStringify,
OTELSeleniumDone: OTELSeleniumDone,
});

View File

@ -0,0 +1,32 @@
'use strict';
import { ConsoleSpanExporter, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { ZoneContextManager } from '@opentelemetry/context-zone-peer-dep';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
/**
* Function helper to load the tracing with predefined instrumentations
* @param instrumentations
* @return {WebTracerProvider}
*/
export function loadOtel(instrumentations) {
const provider = new WebTracerProvider();
const memoryExporter = new InMemorySpanExporter();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register({
contextManager: new ZoneContextManager(),
});
registerInstrumentations({
instrumentations: [
instrumentations,
],
});
window.otel = Object.assign({}, window.otel, {
provider,
memoryExporter,
});
return provider;
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>XHR Plugin</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
Example of using Web Tracer with XHR plugin and console exporter
<script type="text/javascript" src="xhr.js"></script>
<br/>
<button id="button1">Test</button>
</body>
</html>

View File

@ -0,0 +1,56 @@
'use strict';
import '../helper';
import { context, trace } from '@opentelemetry/api';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { loadOtel } from '../tracing';
const provider = loadOtel([
new XMLHttpRequestInstrumentation({
ignoreUrls: [/localhost:8090\/sockjs-node/],
propagateTraceHeaderCorsUrls: [
'https://httpbin.org/get',
],
clearTimingResources: true,
}),
]);
const webTracerWithZone = provider.getTracer('example-tracer-web');
const getData = (url) => new Promise((resolve, reject) => {
// eslint-disable-next-line no-undef
const req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('Content-Type', 'application/json');
req.setRequestHeader('Accept', 'application/json');
req.onload = () => {
resolve();
};
req.onerror = () => {
reject();
};
req.send();
});
// example of keeping track of context between async operations
const prepareClickEvent = () => {
const url = 'https://httpbin.org/get';
const element = document.getElementById('button1');
const onClick = () => {
const singleSpan = webTracerWithZone.startSpan(`files-series-info`);
context.with(trace.setSpan(context.active(), singleSpan), () => {
getData(url).then((_data) => {
trace.getSpan(context.active()).addEvent('fetching-single-span-completed');
singleSpan.end();
}).finally(() => {
otel.OTELSeleniumDone();
});
});
};
element.addEventListener('click', onClick);
};
window.addEventListener('load', prepareClickEvent);

View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
const Nightwatch = require('nightwatch');
const browserstack = require('browserstack-local');
require('dotenv').config();
let bs_local;
try {
require.main.filename = './node_modules/.bin/nightwatch';
// Code to start browserstack local before start of test
console.log('Connecting local');
Nightwatch.bs_local = bs_local = new browserstack.Local();
bs_local.start({ key: process.env.BROWSERSTACK_KEY }, function (error) {
if (error) {
bs_local.stop(function () {});
throw error;
}
console.log('Connected. Now testing...');
Nightwatch.cli(function (argv) {
Nightwatch.CliRunner(argv)
.setup()
.runTests()
.catch((err) => {
throw err;
})
.finally(() => {
// Code to stop browserstack local after end of single test
bs_local.stop(function () {});
});
});
});
} catch (ex) {
console.log('There was an error while starting the test runner:\n\n');
process.stderr.write(ex.stack + '\n');
process.exit(2);
}

View File

@ -0,0 +1,5 @@
const TIMEOUT_ELEMENT_MS = 5000;
module.exports = {
TIMEOUT_ELEMENT_MS,
};

View File

@ -0,0 +1,27 @@
const { TIMEOUT_ELEMENT_MS } = require('../../tests-helpers/constants');
module.exports = {
'Fetch instrumentation': function (browser) {
browser
.url('http://localhost:8090/fetch')
.waitForElementVisible('body', TIMEOUT_ELEMENT_MS)
.waitForElementVisible('#button1', TIMEOUT_ELEMENT_MS)
.click('#button1')
.waitForElementVisible('#otelSeleniumDone', TIMEOUT_ELEMENT_MS)
.execute(
() => {
const spans = otel.memoryExporter.getFinishedSpans();
return otel.JSONSafeStringify(spans);
},
[],
(result) => {
const spans = JSON.parse(result.value);
browser.assert.equal(spans.length, 2, 'Should export 2 spans');
},
)
browser.end(() => {
console.log('end');
});
},
};

View File

@ -0,0 +1,26 @@
const { TIMEOUT_ELEMENT_MS } = require('../../tests-helpers/constants');
module.exports = {
'XHR instrumentation': function (browser) {
browser
.url('http://localhost:8090/xhr')
.waitForElementVisible('body', TIMEOUT_ELEMENT_MS)
.waitForElementVisible('#button1', TIMEOUT_ELEMENT_MS)
.click('#button1')
.waitForElementVisible('#otelSeleniumDone', TIMEOUT_ELEMENT_MS)
.execute(
function() {
const spans = otel.memoryExporter.getFinishedSpans();
return otel.JSONSafeStringify(spans);
},
[],
function(result) {
const spans = JSON.parse(result.value);
browser.assert.equal(spans.length, 2, 'Should export 2 spans');
},
)
browser.end(() => {
console.log('end');
});
},
};

View File

@ -0,0 +1,38 @@
const path = require('path');
const directory = path.resolve(__dirname);
module.exports = {
entry: {
fetch: 'pages/fetch/index.js',
xhr: 'pages/xhr/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
sourceMapFilename: '[file].map',
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.js[x]?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: path.resolve(__dirname, 'babel.config.js'),
presets: ['@babel/preset-env'],
},
},
},
],
},
resolve: {
modules: [
path.resolve(directory),
'node_modules',
],
extensions: ['.ts', '.js', '.jsx', '.json'],
},
};

View File

@ -0,0 +1,57 @@
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const path = require('path');
const directory = path.resolve(__dirname);
const common = {
mode: 'development',
entry: {
fetch: 'pages/fetch/index.js',
// xhr: 'pages/xhr/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
sourceMapFilename: '[file].map',
},
target: 'web',
module: {
rules: [
{
test: /\.js[x]?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: path.resolve(__dirname, 'babel.config.js'),
presets: ['@babel/preset-env'],
},
},
include: [
path.resolve(__dirname, 'pages/helper.js'),
],
},
],
},
resolve: {
modules: [
path.resolve(directory),
'node_modules',
],
extensions: ['.js', '.jsx', '.json'],
},
};
module.exports = webpackMerge(common, {
devtool: 'eval-source-map',
devServer: {
static: path.resolve(__dirname),
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
],
});

View File

@ -0,0 +1,11 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
module.exports = merge(common, {
mode: 'development',
// devtool: 'inline-source-map',
devServer: {
static: path.resolve(__dirname, 'pages'),
},
});

View File

@ -0,0 +1,17 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
extractComments: false,
terserOptions: {
ie8: true,
safari10: true,
},
})],
},
});