Merge pull request #81 from AOEpeople/useCreateReactAppToSimplify
Use create react app to simplify the application
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
REACT_APP_RADAR_NAME=AOE Technology Radar
|
||||
PUBLIC_URL=/techradar
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,9 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.idea
|
||||
.DS_Store
|
||||
dist
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
aoe_technology_radar.iml
|
||||
build
|
||||
# bin
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn ts:check && yarn lint-staged
|
||||
5
.lintstagedrc
Normal file
5
.lintstagedrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"*.{json, md, yml, js, ts, tsx}": [
|
||||
"prettier --write",
|
||||
]
|
||||
}
|
||||
58
README.md
58
README.md
@@ -1,32 +1,33 @@
|
||||
# AOE Technology Radar
|
||||
|
||||
A static site generator for AOE Technology Radar
|
||||
|
||||
## Looking for the AOE Tech Radar content?
|
||||
|
||||
The repository is now found here: https://github.com/AOEpeople/techradar
|
||||
|
||||
The AOE Tech radar is deployed here: https://www.aoe.com/techradar/index.html
|
||||
|
||||
## Usage for your own radar?
|
||||
|
||||
The generator is free to use under Open Source License - in fact there are already some other Radars published based on our Radar and there are also Contributions back.
|
||||
(There is a list of planned features below in case someone wants to contribute :-)
|
||||
|
||||
However please be aware:
|
||||
However, please be aware:
|
||||
* It would be nice to mention in radar that the generator is based on this repository.
|
||||
* Also when you want to reuse the CSS and Styling: Change the font (it is a licensed font) and the colors (It using AOE CI)
|
||||
* Also, when you want to reuse the CSS and Styling: Change the font (it is a licensed font) and the colors (It using AOE CI)
|
||||
|
||||
## Use and build
|
||||
|
||||
Add the tech radar as a dependency
|
||||
```
|
||||
yarn add https://github.com/aoepeople/aoe_technology_radar.git
|
||||
```
|
||||
|
||||
Generate json file based on md files
|
||||
```
|
||||
yarn aoe_technology_radar-generateJson
|
||||
```
|
||||
|
||||
Build the radar
|
||||
```
|
||||
yarn aoe_technology_radar
|
||||
yarn aoe_technology_radar-buildRadar
|
||||
```
|
||||
|
||||
Serve
|
||||
@@ -34,30 +35,49 @@ Serve
|
||||
cd build
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
|
||||
Then open here: http://localhost:8080
|
||||
|
||||
## Run a prepared static version
|
||||
|
||||
In most cases you have the techradar available at `/techradar`, and for reasons want all correct pages to be accessable.
|
||||
In most cases you have the tech radar available at `/techradar`, and for reasons want all correct pages to be accessible.
|
||||
|
||||
Until this setup improves, you can use the following way to generate the correct tech radar:
|
||||
|
||||
```
|
||||
PUBLIC_URL=techradar/ yarn aoe_technology_radar
|
||||
yarn aoe_technology_radar-static
|
||||
yarn aoe_technology_radar-generateJson
|
||||
PUBLIC_URL=/techradar yarn aoe_technology_radar-buildRadar
|
||||
yarn aoe_technology_radar-createStaticFiles
|
||||
cp -r build techradar
|
||||
```
|
||||
|
||||
(This is rather workaroundish for now, but does the job.)
|
||||
|
||||
## Usage
|
||||
## Customize the tech radar
|
||||
You can customize the following parts of the tech radar.
|
||||
|
||||
### Change title, description and headline
|
||||
Set the environment variable `REACT_APP_RADAR_NAME`. The default is "AOE Technology Radar".
|
||||
|
||||
### Host the application under a sub path
|
||||
To host the application under a sub path, set the environment variable `PUBLIC_URL`, e.g. "/techradar".
|
||||
|
||||
### Change the favicon
|
||||
To change the favicon, create a public folder in your application and put your favicon.ico in it.
|
||||
|
||||
### Change the logo
|
||||
To change the logo, create a public folder in your application and put your logo.svg in it.
|
||||
For reference have a look at [public/logo.svg](./public/logo.svg).
|
||||
|
||||
### Change the index.html
|
||||
To change the index.html, create a public folder in your application and put your index.html in it.
|
||||
For reference have a look at [public/index.html](./public/index.html).
|
||||
|
||||
## Usage
|
||||
For a new Technology Radar release, create a folder of the release date
|
||||
(YYYY-MM-DD) under `/radar`. In each release folder create a folder for every
|
||||
quadrant and place the items there.
|
||||
|
||||
### Maintaining items
|
||||
|
||||
The items are written in Markdown format (.md)
|
||||
|
||||
Each file has a [front-matter](https://github.com/jxson/front-matter) header
|
||||
@@ -88,4 +108,14 @@ The name of the .md file acts as item identifier and may overwrite items with
|
||||
the same name from older releases.
|
||||
|
||||
If an item is overwritten in a new release, the attributes from the new item are
|
||||
merged with the old ones and a new history entry is created for that item.
|
||||
merged with the old ones, and a new history entry is created for that item.
|
||||
|
||||
You can integrate images in your markdown. Put the image files in your public folder and reference them
|
||||
```
|
||||

|
||||
```
|
||||
|
||||
## Development
|
||||
### Change scripts
|
||||
If you change one of the scripts in the scripts' folder, you have to compile them to JavaScript.
|
||||
Therefore, run `yarn build:scripts` and commit the results in dist_scripts.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.assetUrl = exports.isMobileViewport = exports.translate = exports.showEmptyRings = exports.getItemPageNames = exports.rings = exports.quadrants = exports.radarNameShort = exports.radarName = void 0;
|
||||
exports.radarName = process.env.RADAR_NAME || 'AOE Technology Radar';
|
||||
exports.radarNameShort = exports.radarName;
|
||||
exports.quadrants = [
|
||||
'languages-and-frameworks',
|
||||
'methods-and-patterns',
|
||||
'platforms-and-aoe-services',
|
||||
'tools',
|
||||
];
|
||||
exports.rings = [
|
||||
'all',
|
||||
'adopt',
|
||||
'trial',
|
||||
'assess',
|
||||
'hold'
|
||||
];
|
||||
exports.getItemPageNames = (items) => items.map(item => `${item.quadrant}/${item.name}`);
|
||||
exports.showEmptyRings = false;
|
||||
const messages = {
|
||||
'languages-and-frameworks': 'Languages & Frameworks',
|
||||
'methods-and-patterns': 'Methods & Patterns',
|
||||
'platforms-and-aoe-services': 'Platforms and Operations',
|
||||
'tools': 'Tools',
|
||||
};
|
||||
exports.translate = (key) => (messages[key] || '-');
|
||||
function isMobileViewport() {
|
||||
// return false for server side rendering
|
||||
if (typeof window == 'undefined')
|
||||
return false;
|
||||
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
return width < 1200;
|
||||
}
|
||||
exports.isMobileViewport = isMobileViewport;
|
||||
function assetUrl(file) {
|
||||
return process.env.PUBLIC_URL + '/' + file;
|
||||
// return `/techradar/assets/${file}`
|
||||
}
|
||||
exports.assetUrl = assetUrl;
|
||||
@@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.featuredOnly = void 0;
|
||||
exports.featuredOnly = (items) => items.filter(item => item.featured);
|
||||
exports.groupByQuadrants = (items) => items.reduce((quadrants, item) => (Object.assign(Object.assign({}, quadrants), { [item.quadrant]: addItemToQuadrant(quadrants[item.quadrant], item) })), {});
|
||||
exports.groupByFirstLetter = (items) => {
|
||||
const index = items.reduce((letterIndex, item) => (Object.assign(Object.assign({}, letterIndex), { [exports.getFirstLetter(item)]: addItemToList(letterIndex[exports.getFirstLetter(item)], item) })), {});
|
||||
return Object.keys(index)
|
||||
.sort()
|
||||
.map(letter => ({
|
||||
letter,
|
||||
items: index[letter],
|
||||
}));
|
||||
};
|
||||
const addItemToQuadrant = (quadrant = {}, item) => (Object.assign(Object.assign({}, quadrant), { [item.ring]: addItemToRing(quadrant[item.ring], item) }));
|
||||
const addItemToList = (list = [], item) => [...list, item];
|
||||
const addItemToRing = (ring = [], item) => [...ring, item];
|
||||
exports.getFirstLetter = (item) => item.title.substr(0, 1).toUpperCase();
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const radar_1 = require("./radar");
|
||||
const fs_1 = require("fs");
|
||||
const config_1 = require("../src/config");
|
||||
(() => __awaiter(void 0, void 0, void 0, function* () {
|
||||
try {
|
||||
console.log('starting static');
|
||||
const radar = yield radar_1.createRadar();
|
||||
fs_1.copyFileSync('build/index.html', 'build/overview.html');
|
||||
fs_1.copyFileSync('build/index.html', 'build/help-and-about-tech-radar.html');
|
||||
config_1.quadrants.forEach(quadrant => {
|
||||
fs_1.copyFileSync('build/index.html', 'build/' + quadrant + '.html');
|
||||
fs_1.mkdirSync('build/' + quadrant);
|
||||
});
|
||||
radar.items.forEach(item => {
|
||||
fs_1.copyFileSync('build/index.html', 'build/' + item.quadrant + '/' + item.name + '.html');
|
||||
});
|
||||
console.log('created static');
|
||||
}
|
||||
catch (e) {
|
||||
console.error('error:', e);
|
||||
}
|
||||
}))();
|
||||
@@ -1,43 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.save = exports.getAllMarkdownFiles = exports.distPath = exports.jsPath = exports.faviconPath = exports.assetsPath = exports.stylesPath = exports.radarPath = exports.relativePath = void 0;
|
||||
const fs_extra_1 = require("fs-extra");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const walk_1 = require("walk");
|
||||
exports.relativePath = (...relativePath) => (
|
||||
// path.resolve(__dirname, '..', ...relativePath)
|
||||
path_1.default.resolve(...relativePath));
|
||||
exports.radarPath = (...pathInSrc) => (exports.relativePath('radar', ...pathInSrc));
|
||||
exports.stylesPath = (...pathInSrc) => (exports.relativePath('styles', ...pathInSrc));
|
||||
exports.assetsPath = (...pathInSrc) => (exports.relativePath('assets', ...pathInSrc));
|
||||
exports.faviconPath = (...pathInSrc) => (exports.relativePath('assets/favicon.ico', ...pathInSrc));
|
||||
exports.jsPath = (...pathInSrc) => (exports.relativePath('js', ...pathInSrc));
|
||||
exports.distPath = (...pathInDist) => (exports.relativePath('src', ...pathInDist));
|
||||
exports.getAllMarkdownFiles = (folder) => (getAllFiles(folder, isMarkdownFile));
|
||||
const getAllFiles = (folder, predicate) => (new Promise((resolve, reject) => {
|
||||
const walker = walk_1.walk(folder, { followLinks: false });
|
||||
const files = [];
|
||||
walker.on("file", (root, fileStat, next) => {
|
||||
if (predicate(fileStat.name)) {
|
||||
files.push(path_1.default.resolve(root, fileStat.name));
|
||||
}
|
||||
next();
|
||||
});
|
||||
walker.on("errors", (root, nodeStatsArray, next) => {
|
||||
nodeStatsArray.forEach(function (n) {
|
||||
console.error("[ERROR] " + n.name);
|
||||
if (n.error) {
|
||||
console.error(n.error.message || (n.error.code + ": " + n.error.path));
|
||||
}
|
||||
});
|
||||
next();
|
||||
});
|
||||
walker.on("end", () => {
|
||||
resolve(files.sort());
|
||||
});
|
||||
}));
|
||||
const isMarkdownFile = (name) => name.match(/\.md$/) !== null;
|
||||
exports.save = (data, fileName) => fs_extra_1.outputFile(exports.distPath(fileName), data);
|
||||
@@ -1,122 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createRadar = void 0;
|
||||
const fs_extra_1 = require("fs-extra");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const front_matter_1 = __importDefault(require("front-matter"));
|
||||
const marked_1 = __importDefault(require("marked"));
|
||||
const highlight_js_1 = __importDefault(require("highlight.js"));
|
||||
const config_1 = require("../src/config");
|
||||
const file_1 = require("./file");
|
||||
marked_1.default.setOptions({
|
||||
highlight: code => highlight_js_1.default.highlightAuto(code).value,
|
||||
});
|
||||
exports.createRadar = () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const fileNames = yield file_1.getAllMarkdownFiles(file_1.radarPath());
|
||||
const revisions = yield createRevisionsFromFiles(fileNames);
|
||||
const allReleases = getAllReleases(revisions);
|
||||
const items = createItems(revisions);
|
||||
const flaggedItems = flagItem(items, allReleases);
|
||||
return {
|
||||
items: flaggedItems,
|
||||
releases: allReleases,
|
||||
};
|
||||
});
|
||||
const checkAttributes = (fileName, attributes) => {
|
||||
if (attributes.ring && !config_1.rings.includes(attributes.ring)) {
|
||||
throw new Error(`Error: ${fileName} has an illegal value for 'ring' - must be one of ${config_1.rings}`);
|
||||
}
|
||||
if (attributes.quadrant && !config_1.quadrants.includes(attributes.quadrant)) {
|
||||
throw new Error(`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${config_1.quadrants}`);
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
const createRevisionsFromFiles = (fileNames) => Promise.all(fileNames.map(fileName => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs_extra_1.readFile(fileName, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
const fm = front_matter_1.default(data);
|
||||
// add target attribute to external links
|
||||
// todo: check path
|
||||
let html = marked_1.default(fm.body.replace(/\]\(\//g, '](/techradar/'));
|
||||
html = html.replace(/a href="http/g, 'a target="_blank" rel="noopener noreferrer" href="http');
|
||||
resolve(Object.assign(Object.assign(Object.assign({}, itemInfoFromFilename(fileName)), checkAttributes(fileName, fm.attributes)), { fileName, body: html }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
const itemInfoFromFilename = (fileName) => {
|
||||
const [release, name] = fileName.split(path_1.default.sep).slice(-2);
|
||||
return {
|
||||
name: path_1.default.basename(name, '.md'),
|
||||
release,
|
||||
};
|
||||
};
|
||||
const getAllReleases = (revisions) => revisions
|
||||
.reduce((allReleases, { release }) => {
|
||||
if (!allReleases.includes(release)) {
|
||||
return [...allReleases, release];
|
||||
}
|
||||
return allReleases;
|
||||
}, [])
|
||||
.sort();
|
||||
const createItems = (revisions) => {
|
||||
const itemMap = revisions.reduce((items, revision) => {
|
||||
return Object.assign(Object.assign({}, items), { [revision.name]: addRevisionToItem(items[revision.name], revision) });
|
||||
}, {});
|
||||
return Object.values(itemMap).map(item => (Object.assign(Object.assign({}, item), { ['title']: item.title || item.name }))).sort((x, y) => (x.name > y.name ? 1 : -1));
|
||||
};
|
||||
const ignoreEmptyRevisionBody = (revision, item) => {
|
||||
if (!revision.body || revision.body.trim() === '') {
|
||||
return item.body;
|
||||
}
|
||||
return revision.body;
|
||||
};
|
||||
const addRevisionToItem = (item = {
|
||||
flag: 'default',
|
||||
featured: true,
|
||||
revisions: [],
|
||||
name: '',
|
||||
title: '',
|
||||
ring: 'trial',
|
||||
quadrant: '',
|
||||
body: '',
|
||||
info: '',
|
||||
}, revision) => {
|
||||
let newItem = Object.assign(Object.assign(Object.assign({}, item), revision), { body: ignoreEmptyRevisionBody(revision, item) });
|
||||
if (revisionCreatesNewHistoryEntry(revision)) {
|
||||
newItem = Object.assign(Object.assign({}, newItem), { revisions: [revision, ...newItem.revisions] });
|
||||
}
|
||||
return newItem;
|
||||
};
|
||||
const revisionCreatesNewHistoryEntry = (revision) => {
|
||||
return revision.body.trim() !== '' || typeof revision.ring !== 'undefined';
|
||||
};
|
||||
const flagItem = (items, allReleases) => items.map(item => (Object.assign(Object.assign({}, item), { flag: getItemFlag(item, allReleases) })), []);
|
||||
const isInLastRelease = (item, allReleases) => item.revisions[0].release === allReleases[allReleases.length - 1];
|
||||
const isNewItem = (item, allReleases) => item.revisions.length === 1 && isInLastRelease(item, allReleases);
|
||||
const hasItemChanged = (item, allReleases) => item.revisions.length > 1 && isInLastRelease(item, allReleases);
|
||||
const getItemFlag = (item, allReleases) => {
|
||||
if (isNewItem(item, allReleases)) {
|
||||
return 'new';
|
||||
}
|
||||
if (hasItemChanged(item, allReleases)) {
|
||||
return 'changed';
|
||||
}
|
||||
return 'default';
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.radarJsonGenerator = void 0;
|
||||
const radar_1 = require("./radar");
|
||||
const file_1 = require("./file");
|
||||
exports.radarJsonGenerator = (() => __awaiter(void 0, void 0, void 0, function* () {
|
||||
try {
|
||||
console.log('start');
|
||||
const radar = yield radar_1.createRadar();
|
||||
// console.log(radar);
|
||||
file_1.save(JSON.stringify(radar), 'rd.json');
|
||||
file_1.save(`import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from 'aoe_technology_radar/src/components/App';
|
||||
import 'aoe_technology_radar/src/index.scss';
|
||||
import {Item} from "aoe_technology_radar/src/model";
|
||||
import radardata from './rd.json';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App items={radardata.items as Item[]} releases={radardata.releases as string[]} />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
`, 'index.tsx');
|
||||
console.log('Built radar');
|
||||
}
|
||||
catch (e) {
|
||||
console.error('error:', e);
|
||||
}
|
||||
}));
|
||||
104
config/env.js
104
config/env.js
@@ -1,104 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Make sure that including paths.js after env.js will read .env variables.
|
||||
delete require.cache[require.resolve('./paths')];
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
'The NODE_ENV environment variable is required but was not specified.'
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
const dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
// Don't include `.env.local` for `test` environment
|
||||
// since normally you expect tests to produce the same
|
||||
// results for everyone
|
||||
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
|
||||
paths.dotenv,
|
||||
].filter(Boolean);
|
||||
|
||||
// Load environment variables from .env* files. Suppress warnings using silent
|
||||
// if this file is missing. dotenv will never modify any environment variables
|
||||
// that have already been set. Variable expansion is supported in .env files.
|
||||
// https://github.com/motdotla/dotenv
|
||||
// https://github.com/motdotla/dotenv-expand
|
||||
dotenvFiles.forEach(dotenvFile => {
|
||||
if (fs.existsSync(dotenvFile)) {
|
||||
require('dotenv-expand')(
|
||||
require('dotenv').config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We support resolving modules according to `NODE_PATH`.
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
// https://github.com/facebook/create-react-app/issues/253.
|
||||
// It works similar to `NODE_PATH` in Node itself:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
|
||||
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||
// We also resolve them to make sure all tools using them work consistently.
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||
.split(path.delimiter)
|
||||
.filter(folder => folder && !path.isAbsolute(folder))
|
||||
.map(folder => path.resolve(appDirectory, folder))
|
||||
.join(path.delimiter);
|
||||
|
||||
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in webpack configuration.
|
||||
const REACT_APP = /^REACT_APP_/i;
|
||||
|
||||
function getClientEnvironment(publicUrl) {
|
||||
const raw = Object.keys(process.env)
|
||||
.filter(key => REACT_APP.test(key))
|
||||
.reduce(
|
||||
(env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
},
|
||||
{
|
||||
// Useful for determining whether we’re running in production mode.
|
||||
// Most importantly, it switches React into the correct mode.
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
// Useful for resolving the correct path to static assets in `public`.
|
||||
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
|
||||
RADAR_NAME: process.env.RADAR_NAME || 'AOE Technology Radar',
|
||||
|
||||
// We support configuring the sockjs pathname during development.
|
||||
// These settings let a developer run multiple simultaneous projects.
|
||||
// They are used as the connection `hostname`, `pathname` and `port`
|
||||
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
|
||||
// and `sockPort` options in webpack-dev-server.
|
||||
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
|
||||
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
|
||||
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
|
||||
}
|
||||
);
|
||||
// Stringify all values so we can feed into webpack DefinePlugin
|
||||
const stringified = {
|
||||
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return { raw, stringified };
|
||||
}
|
||||
|
||||
module.exports = getClientEnvironment;
|
||||
@@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Ensure the certificate and key provided are valid and if not
|
||||
// throw an easy to debug error
|
||||
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
|
||||
let encrypted;
|
||||
try {
|
||||
// publicEncrypt will throw an error with an invalid cert
|
||||
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// privateDecrypt will throw an error with an invalid key
|
||||
crypto.privateDecrypt(key, encrypted);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
|
||||
err.message
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Read file and throw an error if it doesn't exist
|
||||
function readEnvFile(file, type) {
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(
|
||||
`You specified ${chalk.cyan(
|
||||
type
|
||||
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
|
||||
);
|
||||
}
|
||||
return fs.readFileSync(file);
|
||||
}
|
||||
|
||||
// Get the https config
|
||||
// Return cert files if provided in env, otherwise just true or false
|
||||
function getHttpsConfig() {
|
||||
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
|
||||
const isHttps = HTTPS === 'true';
|
||||
|
||||
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
|
||||
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
|
||||
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
|
||||
const config = {
|
||||
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
|
||||
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
|
||||
};
|
||||
|
||||
validateKeyAndCerts({ ...config, keyFile, crtFile });
|
||||
return config;
|
||||
}
|
||||
return isHttps;
|
||||
}
|
||||
|
||||
module.exports = getHttpsConfig;
|
||||
@@ -1,14 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return 'cssTransform';
|
||||
},
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const camelcase = require('camelcase');
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
const assetFilename = JSON.stringify(path.basename(filename));
|
||||
|
||||
if (filename.match(/\.svg$/)) {
|
||||
// Based on how SVGR generates a component name:
|
||||
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
||||
const pascalCaseFilename = camelcase(path.parse(filename).name, {
|
||||
pascalCase: true,
|
||||
});
|
||||
const componentName = `Svg${pascalCaseFilename}`;
|
||||
return `const React = require('react');
|
||||
module.exports = {
|
||||
__esModule: true,
|
||||
default: ${assetFilename},
|
||||
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
||||
return {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'svg',
|
||||
ref: ref,
|
||||
key: null,
|
||||
props: Object.assign({}, props, {
|
||||
children: ${assetFilename}
|
||||
})
|
||||
};
|
||||
}),
|
||||
};`;
|
||||
}
|
||||
|
||||
return `module.exports = ${assetFilename};`;
|
||||
},
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const paths = require('./paths');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const resolve = require('resolve');
|
||||
|
||||
/**
|
||||
* Get additional module paths based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function getAdditionalModulePaths(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
// We need to explicitly check for null and undefined (and not a falsy value) because
|
||||
// TypeScript treats an empty string as `.`.
|
||||
if (baseUrl == null) {
|
||||
// If there's no baseUrl set we respect NODE_PATH
|
||||
// Note that NODE_PATH is deprecated and will be removed
|
||||
// in the next major release of create-react-app.
|
||||
|
||||
const nodePath = process.env.NODE_PATH || '';
|
||||
return nodePath.split(path.delimiter).filter(Boolean);
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
|
||||
// the default behavior.
|
||||
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow the user set the `baseUrl` to `appSrc`.
|
||||
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
|
||||
return [paths.appSrc];
|
||||
}
|
||||
|
||||
// If the path is equal to the root directory we ignore it here.
|
||||
// We don't want to allow importing from the root directly as source files are
|
||||
// not transpiled outside of `src`. We do allow importing them with the
|
||||
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
|
||||
// an alias.
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise, throw an error.
|
||||
throw new Error(
|
||||
chalk.red.bold(
|
||||
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
|
||||
' Create React App does not support other values at this time.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webpack aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getWebpackAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return {
|
||||
src: paths.appSrc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jest aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getJestAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return {
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getModules() {
|
||||
// Check if TypeScript is setup
|
||||
const hasTsConfig = fs.existsSync(paths.appTsConfig);
|
||||
const hasJsConfig = fs.existsSync(paths.appJsConfig);
|
||||
|
||||
if (hasTsConfig && hasJsConfig) {
|
||||
throw new Error(
|
||||
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
|
||||
);
|
||||
}
|
||||
|
||||
let config;
|
||||
|
||||
// If there's a tsconfig.json we assume it's a
|
||||
// TypeScript project and set up the config
|
||||
// based on tsconfig.json
|
||||
if (hasTsConfig) {
|
||||
const ts = require(resolve.sync('typescript', {
|
||||
basedir: paths.appNodeModules,
|
||||
}));
|
||||
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
|
||||
// Otherwise we'll check if there is jsconfig.json
|
||||
// for non TS projects.
|
||||
} else if (hasJsConfig) {
|
||||
config = require(paths.appJsConfig);
|
||||
}
|
||||
|
||||
config = config || {};
|
||||
const options = config.compilerOptions || {};
|
||||
|
||||
const additionalModulePaths = getAdditionalModulePaths(options);
|
||||
|
||||
return {
|
||||
additionalModulePaths: additionalModulePaths,
|
||||
webpackAliases: getWebpackAliases(options),
|
||||
jestAliases: getJestAliases(options),
|
||||
hasTsConfig,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getModules();
|
||||
@@ -1,75 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
|
||||
|
||||
// Make sure any symlinks in the project folder are resolved:
|
||||
// https://github.com/facebook/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
const templateDirectory = fs.realpathSync(__dirname);
|
||||
const resolveTemplate = relativePath => path.resolve(templateDirectory, '..', relativePath);
|
||||
|
||||
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||
// "public path" at which the app is served.
|
||||
// webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||
// We can't use a relative path in HTML because we don't want to load something
|
||||
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||
const publicUrlOrPath = getPublicUrlOrPath(
|
||||
process.env.NODE_ENV === 'development',
|
||||
require(resolveApp('package.json')).homepage,
|
||||
process.env.PUBLIC_URL
|
||||
);
|
||||
|
||||
const moduleFileExtensions = [
|
||||
'web.mjs',
|
||||
'mjs',
|
||||
'web.js',
|
||||
'js',
|
||||
'web.ts',
|
||||
'ts',
|
||||
'web.tsx',
|
||||
'tsx',
|
||||
'json',
|
||||
'web.jsx',
|
||||
'jsx',
|
||||
];
|
||||
|
||||
// Resolve file paths in the same order as webpack
|
||||
const resolveModule = (resolveFn, filePath) => {
|
||||
const extension = moduleFileExtensions.find(extension =>
|
||||
fs.existsSync(resolveFn(`${filePath}.${extension}`))
|
||||
);
|
||||
|
||||
if (extension) {
|
||||
return resolveFn(`${filePath}.${extension}`);
|
||||
}
|
||||
|
||||
return resolveFn(`${filePath}.js`);
|
||||
};
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp('.env'),
|
||||
appPath: resolveApp('.'),
|
||||
appBuild: resolveApp('build'),
|
||||
appPublic: resolveTemplate('public'),
|
||||
appHtml: resolveTemplate('public/index.html'),
|
||||
appIndexJs: resolveModule(resolveApp, 'src/index'),
|
||||
appPackageJson: resolveApp('package.json'),
|
||||
appSrc: resolveApp('src'),
|
||||
templateSrc: resolveTemplate('src'),
|
||||
appTsConfig: resolveTemplate('tsconfig.json'),
|
||||
appJsConfig: resolveTemplate('jsconfig.json'),
|
||||
yarnLockFile: resolveApp('yarn.lock'),
|
||||
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
|
||||
proxySetup: resolveTemplate('src/setupProxy.js'),
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
publicUrlOrPath,
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { resolveModuleName } = require('ts-pnp');
|
||||
|
||||
exports.resolveModuleName = (
|
||||
typescript,
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveModuleName
|
||||
);
|
||||
};
|
||||
|
||||
exports.resolveTypeReferenceDirective = (
|
||||
typescript,
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveTypeReferenceDirective
|
||||
);
|
||||
};
|
||||
@@ -1,670 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const resolve = require('resolve');
|
||||
const PnpWebpackPlugin = require('pnp-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const safePostCssParser = require('postcss-safe-parser');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
|
||||
const paths = require('./paths');
|
||||
const modules = require('./modules');
|
||||
const getClientEnvironment = require('./env');
|
||||
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
|
||||
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
|
||||
|
||||
const postcssNormalize = require('postcss-normalize');
|
||||
|
||||
const appPackageJson = require(paths.appPackageJson);
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||
// makes for a smoother build process.
|
||||
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||
|
||||
const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';
|
||||
|
||||
const imageInlineSizeLimit = parseInt(
|
||||
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
|
||||
);
|
||||
|
||||
// Check if TypeScript is setup
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
|
||||
// style files regexes
|
||||
const cssRegex = /\.css$/;
|
||||
const cssModuleRegex = /\.module\.css$/;
|
||||
const sassRegex = /\.(scss|sass)$/;
|
||||
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||
|
||||
// This is the production and development configuration.
|
||||
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||
module.exports = function(webpackEnv) {
|
||||
const isEnvDevelopment = webpackEnv === 'development';
|
||||
const isEnvProduction = webpackEnv === 'production';
|
||||
|
||||
// Variable used for enabling profiling in Production
|
||||
// passed into alias object. Uses a flag if passed into the build command
|
||||
const isEnvProductionProfile =
|
||||
isEnvProduction && process.argv.includes('--profile');
|
||||
|
||||
// We will provide `paths.publicUrlOrPath` to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
|
||||
// common function to get style loaders
|
||||
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||
const loaders = [
|
||||
isEnvDevelopment && require.resolve('style-loader'),
|
||||
isEnvProduction && {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
// css is located in `static/css`, use '../../' to locate index.html folder
|
||||
// in production `paths.publicUrlOrPath` can be a relative path
|
||||
options: paths.publicUrlOrPath.startsWith('.')
|
||||
? { publicPath: '../../' }
|
||||
: {},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: cssOptions,
|
||||
},
|
||||
{
|
||||
// Options for PostCSS as we reference these options twice
|
||||
// Adds vendor prefixing based on your specified browser support in
|
||||
// package.json
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebook/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-preset-env')({
|
||||
autoprefixer: {
|
||||
flexbox: 'no-2009',
|
||||
},
|
||||
stage: 3,
|
||||
}),
|
||||
// Adds PostCSS Normalize as the reset css with default options,
|
||||
// so that it honors browserslist config in package.json
|
||||
// which in turn let's users customize the target behavior as per their needs.
|
||||
postcssNormalize(),
|
||||
],
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
if (preProcessor) {
|
||||
loaders.push(
|
||||
{
|
||||
loader: require.resolve('resolve-url-loader'),
|
||||
options: {
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve(preProcessor),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return loaders;
|
||||
};
|
||||
|
||||
return {
|
||||
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
|
||||
// Stop compilation early in production
|
||||
bail: isEnvProduction,
|
||||
devtool: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
? 'source-map'
|
||||
: false
|
||||
: isEnvDevelopment && 'cheap-module-source-map',
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
entry: [
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
isEnvDevelopment &&
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
// Finally, this is your app's code:
|
||||
paths.appIndexJs,
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
].filter(Boolean),
|
||||
output: {
|
||||
// The build folder.
|
||||
path: isEnvProduction ? paths.appBuild : undefined,
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: isEnvDevelopment,
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// In development, it does not produce real files.
|
||||
filename: isEnvProduction
|
||||
? 'static/js/[name].[contenthash:8].js'
|
||||
: isEnvDevelopment && 'static/js/bundle.js',
|
||||
// TODO: remove this when upgrading to webpack 5
|
||||
futureEmitAssets: true,
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: isEnvProduction
|
||||
? 'static/js/[name].[contenthash:8].chunk.js'
|
||||
: isEnvDevelopment && 'static/js/[name].chunk.js',
|
||||
// webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: isEnvProduction
|
||||
? info =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, '/')
|
||||
: isEnvDevelopment &&
|
||||
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
|
||||
// Prevents conflicts when multiple webpack runtimes (from different apps)
|
||||
// are used on the same page.
|
||||
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
|
||||
// this defaults to 'window', but by setting it to 'this' then
|
||||
// module chunks which are built will work in web workers as well.
|
||||
globalObject: 'this',
|
||||
},
|
||||
optimization: {
|
||||
minimize: isEnvProduction,
|
||||
minimizer: [
|
||||
// This is only used in production mode
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
parse: {
|
||||
// We want terser to parse ecma 8 code. However, we don't want it
|
||||
// to apply any minification steps that turns valid ecma 5 code
|
||||
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||
// sections only apply transformations that are ecma 5 safe
|
||||
// https://github.com/facebook/create-react-app/pull/4234
|
||||
ecma: 8,
|
||||
},
|
||||
compress: {
|
||||
ecma: 5,
|
||||
warnings: false,
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
// Disabled because of an issue with Terser breaking valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/5250
|
||||
// Pending further investigation:
|
||||
// https://github.com/terser-js/terser/issues/120
|
||||
inline: 2,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
// Added for profiling in devtools
|
||||
keep_classnames: isEnvProductionProfile,
|
||||
keep_fnames: isEnvProductionProfile,
|
||||
output: {
|
||||
ecma: 5,
|
||||
comments: false,
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebook/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
// This is only used in production mode
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
parser: safePostCssParser,
|
||||
map: shouldUseSourceMap
|
||||
? {
|
||||
// `inline: false` forces the sourcemap to be output into a
|
||||
// separate file
|
||||
inline: false,
|
||||
// `annotation: true` appends the sourceMappingURL to the end of
|
||||
// the css file, helping the browser find the sourcemap
|
||||
annotation: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Automatically split vendor and commons
|
||||
// https://twitter.com/wSokra/status/969633336732905474
|
||||
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
name: false,
|
||||
},
|
||||
// Keep the runtime chunk separated to enable long term caching
|
||||
// https://twitter.com/wSokra/status/969679223278505985
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
runtimeChunk: {
|
||||
name: entrypoint => `runtime-${entrypoint.name}`,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// This allows you to set a fallback for where webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebook/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
modules.additionalModulePaths || []
|
||||
),
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebook/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: paths.moduleFileExtensions
|
||||
.map(ext => `.${ext}`)
|
||||
.filter(ext => useTypeScript || !ext.includes('ts')),
|
||||
alias: {
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
// Allows for better profiling with ReactDevTools
|
||||
...(isEnvProductionProfile && {
|
||||
'react-dom$': 'react-dom/profiling',
|
||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||
}),
|
||||
...(modules.webpackAliases || {}),
|
||||
},
|
||||
plugins: [
|
||||
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||
// guards against forgotten dependencies and such.
|
||||
PnpWebpackPlugin,
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
plugins: [
|
||||
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
|
||||
// from the current package.
|
||||
PnpWebpackPlugin.moduleLoader(module),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
// {
|
||||
// test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||
// enforce: 'pre',
|
||||
// use: [
|
||||
// {
|
||||
// options: {
|
||||
// cache: true,
|
||||
// formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||
// eslintPath: require.resolve('eslint'),
|
||||
// resolvePluginsRelativeTo: __dirname,
|
||||
// },
|
||||
// loader: require.resolve('eslint-loader'),
|
||||
// },
|
||||
// ],
|
||||
// include: paths.appSrc,
|
||||
// },
|
||||
{
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: imageInlineSizeLimit,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// Process application JS with Babel.
|
||||
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||
{
|
||||
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||
include: [paths.appSrc, paths.templateSrc],
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
customize: require.resolve(
|
||||
'babel-preset-react-app/webpack-overrides'
|
||||
),
|
||||
|
||||
presets:['react-app'],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
require.resolve('babel-plugin-named-asset-import'),
|
||||
{
|
||||
loaderMap: {
|
||||
svg: {
|
||||
ReactComponent:
|
||||
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
compact: isEnvProduction,
|
||||
},
|
||||
},
|
||||
// Process any JS outside of the app with Babel.
|
||||
// Unlike the application JS, we only compile the standard ES features.
|
||||
{
|
||||
test: /\.(js|mjs)$/,
|
||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
compact: false,
|
||||
presets: [
|
||||
[
|
||||
require.resolve('babel-preset-react-app/dependencies'),
|
||||
{ helpers: true },
|
||||
],
|
||||
],
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
|
||||
// Babel sourcemaps are needed for debugging into node_modules
|
||||
// code. Without the options below, debuggers like VSCode
|
||||
// show incorrect code and set breakpoints on the wrong lines.
|
||||
sourceMaps: shouldUseSourceMap,
|
||||
inputSourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||
// to a file, but in development "style" loader enables hot editing
|
||||
// of CSS.
|
||||
// By default we support CSS Modules with the extension .module.css
|
||||
{
|
||||
test: cssRegex,
|
||||
exclude: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
}),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||
// using the extension .module.css
|
||||
{
|
||||
test: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
}),
|
||||
},
|
||||
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||
// By default we support SASS Modules with the
|
||||
// extensions .module.scss or .module.sass
|
||||
{
|
||||
test: sassRegex,
|
||||
exclude: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules, but using SASS
|
||||
// using the extension .module.scss or .module.sass
|
||||
{
|
||||
test: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its runtime that would otherwise be processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
},
|
||||
isEnvProduction
|
||||
? {
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
),
|
||||
// Inlines the webpack runtime script. This script is too small to warrant
|
||||
// a network request.
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
isEnvProduction &&
|
||||
shouldInlineRuntimeChunk &&
|
||||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// It will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||
// This gives some necessary context to module not found errors, such as
|
||||
// the requesting resource.
|
||||
new ModuleNotFoundPlugin(paths.appPath),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV is set to production
|
||||
// during a production build.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// This is necessary to emit hot updates (currently CSS only):
|
||||
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebook/create-react-app/issues/240
|
||||
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebook/create-react-app/issues/186
|
||||
isEnvDevelopment &&
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
isEnvProduction &&
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: 'static/css/[name].[contenthash:8].css',
|
||||
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
||||
}),
|
||||
// Generate an asset manifest file with the following content:
|
||||
// - "files" key: Mapping of all asset filenames to their corresponding
|
||||
// output file so that tools can pick it up without having to parse
|
||||
// `index.html`
|
||||
// - "entrypoints" key: Array of files which are included in `index.html`,
|
||||
// can be used to reconstruct the HTML if necessary
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json',
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
generate: (seed, files, entrypoints) => {
|
||||
const manifestFiles = files.reduce((manifest, file) => {
|
||||
manifest[file.name] = file.path;
|
||||
return manifest;
|
||||
}, seed);
|
||||
const entrypointFiles = entrypoints.main.filter(
|
||||
fileName => !fileName.endsWith('.map')
|
||||
);
|
||||
|
||||
return {
|
||||
files: manifestFiles,
|
||||
entrypoints: entrypointFiles,
|
||||
};
|
||||
},
|
||||
}),
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the webpack build.
|
||||
isEnvProduction &&
|
||||
new WorkboxWebpackPlugin.GenerateSW({
|
||||
clientsClaim: true,
|
||||
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||
importWorkboxFrom: 'cdn',
|
||||
navigateFallback: paths.publicUrlOrPath + 'index.html',
|
||||
navigateFallbackBlacklist: [
|
||||
// Exclude URLs starting with /_, as they're likely an API call
|
||||
new RegExp('^/_'),
|
||||
// Exclude any URLs whose last part seems to be a file extension
|
||||
// as they're likely a resource and not a SPA route.
|
||||
// URLs containing a "?" character won't be blacklisted as they're likely
|
||||
// a route with query params (e.g. auth callbacks).
|
||||
new RegExp('/[^/?]+\\.[^/]+$'),
|
||||
],
|
||||
}),
|
||||
// TypeScript type checking
|
||||
useTypeScript &&
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: resolve.sync('typescript', {
|
||||
basedir: paths.appNodeModules,
|
||||
}),
|
||||
async: isEnvDevelopment,
|
||||
useTypescriptIncrementalApi: true,
|
||||
checkSyntacticErrors: true,
|
||||
resolveModuleNameModule: process.versions.pnp
|
||||
? `${__dirname}/pnpTs.js`
|
||||
: undefined,
|
||||
resolveTypeReferenceDirectiveModule: process.versions.pnp
|
||||
? `${__dirname}/pnpTs.js`
|
||||
: undefined,
|
||||
tsconfig: paths.appTsConfig,
|
||||
reportFiles: [
|
||||
'**',
|
||||
'!**/__tests__/**',
|
||||
'!**/?(*.)(spec|test).*',
|
||||
'!**/src/setupProxy.*',
|
||||
'!**/src/setupTests.*',
|
||||
],
|
||||
silent: true,
|
||||
// The formatter is invoked directly in WebpackDevServerUtils during development
|
||||
formatter: isEnvProduction ? typescriptFormatter : undefined,
|
||||
}),
|
||||
].filter(Boolean),
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
module: 'empty',
|
||||
dgram: 'empty',
|
||||
dns: 'mock',
|
||||
fs: 'empty',
|
||||
http2: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
// Turn off performance processing because we utilize
|
||||
// our own hints via the FileSizeReporter
|
||||
performance: false,
|
||||
};
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
|
||||
const paths = require('./paths');
|
||||
const getHttpsConfig = require('./getHttpsConfig');
|
||||
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const sockHost = process.env.WDS_SOCKET_HOST;
|
||||
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
|
||||
const sockPort = process.env.WDS_SOCKET_PORT;
|
||||
|
||||
module.exports = function(proxy, allowedHost) {
|
||||
return {
|
||||
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||
// websites from potentially accessing local content through DNS rebinding:
|
||||
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// However, it made several existing use cases such as development in cloud
|
||||
// environment or subdomains in development significantly more complicated:
|
||||
// https://github.com/facebook/create-react-app/issues/2271
|
||||
// https://github.com/facebook/create-react-app/issues/2233
|
||||
// While we're investigating better solutions, for now we will take a
|
||||
// compromise. Since our WDS configuration only serves files in the `public`
|
||||
// folder we won't consider accessing them a vulnerability. However, if you
|
||||
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||
// So we will disable the host check normally, but enable it if you have
|
||||
// specified the `proxy` setting. Finally, we let you override it if you
|
||||
// really know what you're doing with a special environment variable.
|
||||
disableHostCheck:
|
||||
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||
// It will still show compile warnings and errors with this setting.
|
||||
clientLogLevel: 'none',
|
||||
// By default WebpackDevServer serves physical files from current directory
|
||||
// in addition to all the virtual build products that it serves from memory.
|
||||
// This is confusing because those files won’t automatically be available in
|
||||
// production build folder unless we copy them. However, copying the whole
|
||||
// project directory is dangerous because we may expose sensitive files.
|
||||
// Instead, we establish a convention that only files in `public` directory
|
||||
// get served. Our build script will copy `public` into the `build` folder.
|
||||
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||
// Note that we only recommend to use `public` folder as an escape hatch
|
||||
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||
// for some reason broken when imported through webpack. If you just want to
|
||||
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||
contentBase: paths.appPublic,
|
||||
contentBasePublicPath: paths.publicUrlOrPath,
|
||||
// By default files from `contentBase` will not trigger a page reload.
|
||||
watchContentBase: true,
|
||||
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
|
||||
// for the WebpackDevServer client so it can learn when the files were
|
||||
// updated. The WebpackDevServer client is included as an entry point
|
||||
// in the webpack development configuration. Note that only changes
|
||||
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||
hot: true,
|
||||
// Use 'ws' instead of 'sockjs-node' on server since we're using native
|
||||
// websockets in `webpackHotDevClient`.
|
||||
transportMode: 'ws',
|
||||
// Prevent a WS client from getting injected as we're already including
|
||||
// `webpackHotDevClient`.
|
||||
injectClient: false,
|
||||
// Enable custom sockjs pathname for websocket connection to hot reloading server.
|
||||
// Enable custom sockjs hostname, pathname and port for websocket connection
|
||||
// to hot reloading server.
|
||||
sockHost,
|
||||
sockPath,
|
||||
sockPort,
|
||||
// It is important to tell WebpackDevServer to use the same "publicPath" path as
|
||||
// we specified in the webpack config. When homepage is '.', default to serving
|
||||
// from the root.
|
||||
// remove last slash so user can land on `/test` instead of `/test/`
|
||||
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
||||
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||
quiet: true,
|
||||
// Reportedly, this avoids CPU overload on some systems.
|
||||
// https://github.com/facebook/create-react-app/issues/293
|
||||
// src/node_modules is not ignored to support absolute imports
|
||||
// https://github.com/facebook/create-react-app/issues/1065
|
||||
watchOptions: {
|
||||
ignored: ignoredFiles(paths.appSrc),
|
||||
},
|
||||
https: getHttpsConfig(),
|
||||
host,
|
||||
overlay: false,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebook/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
index: paths.publicUrlOrPath,
|
||||
},
|
||||
public: allowedHost,
|
||||
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
|
||||
proxy,
|
||||
before(app, server) {
|
||||
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
|
||||
// middlewares before `redirectServedPath` otherwise will not have any effect
|
||||
// This lets us fetch source contents from webpack for the error overlay
|
||||
app.use(evalSourceMapMiddleware(server));
|
||||
// This lets us open files from the runtime error overlay.
|
||||
app.use(errorOverlayMiddleware());
|
||||
|
||||
if (fs.existsSync(paths.proxySetup)) {
|
||||
// This registers user provided middleware for proxy reasons
|
||||
require(paths.proxySetup)(app);
|
||||
}
|
||||
},
|
||||
after(app) {
|
||||
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
|
||||
app.use(redirectServedPath(paths.publicUrlOrPath));
|
||||
|
||||
// This service worker file is effectively a 'no-op' that will reset any
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
|
||||
},
|
||||
};
|
||||
};
|
||||
75
dist_scripts/scripts/buildRadar.js
Normal file
75
dist_scripts/scripts/buildRadar.js
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var fs = __importStar(require("fs-extra"));
|
||||
var child_process_1 = require("child_process");
|
||||
var paths = __importStar(require("./paths"));
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", function (err) {
|
||||
throw err;
|
||||
});
|
||||
fs.removeSync(paths.templateNodeModules);
|
||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
||||
var runCommand = function (command) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var executedCommand = child_process_1.spawn(command, {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
executedCommand.on("error", function (error) {
|
||||
reject(error);
|
||||
});
|
||||
executedCommand.on("exit", function (code) {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
}
|
||||
else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
var buildTemplate = function () {
|
||||
var packageManager = fs.existsSync(paths.appYarnLock) ? "yarn" : "npx";
|
||||
fs.emptyDirSync(paths.templateBuild);
|
||||
process.chdir(paths.template);
|
||||
return runCommand(packageManager + " build");
|
||||
};
|
||||
if (fs.existsSync(paths.appRdJson)) {
|
||||
buildTemplate().then(function () {
|
||||
fs.copySync(paths.templateBuild, paths.appBuild);
|
||||
fs.copySync(paths.appPublic, paths.appBuild);
|
||||
console.log(paths.appBuild + " was created and can be deployed.");
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.error(paths.appRdJson + " does not exist. You have to generate it first.");
|
||||
process.exit(1);
|
||||
}
|
||||
83
dist_scripts/scripts/createStaticFiles.js
Normal file
83
dist_scripts/scripts/createStaticFiles.js
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var fs_1 = require("fs");
|
||||
var radar_1 = require("./generateJson/radar");
|
||||
var config_1 = require("../src/config");
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", function (err) {
|
||||
throw err;
|
||||
});
|
||||
(function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var radar, e_1;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 2, , 3]);
|
||||
console.log("starting static");
|
||||
return [4 /*yield*/, radar_1.createRadar()];
|
||||
case 1:
|
||||
radar = _a.sent();
|
||||
fs_1.copyFileSync("build/index.html", "build/overview.html");
|
||||
fs_1.copyFileSync("build/index.html", "build/help-and-about-tech-radar.html");
|
||||
config_1.quadrants.forEach(function (quadrant) {
|
||||
var destFolder = "build/" + quadrant;
|
||||
fs_1.copyFileSync("build/index.html", destFolder + ".html");
|
||||
if (!fs_1.existsSync(destFolder)) {
|
||||
fs_1.mkdirSync(destFolder);
|
||||
}
|
||||
});
|
||||
radar.items.forEach(function (item) {
|
||||
fs_1.copyFileSync("build/index.html", "build/" + item.quadrant + "/" + item.name + ".html");
|
||||
});
|
||||
console.log("created static files.");
|
||||
return [3 /*break*/, 3];
|
||||
case 2:
|
||||
e_1 = _a.sent();
|
||||
console.error("error:", e_1);
|
||||
return [3 /*break*/, 3];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); })();
|
||||
106
dist_scripts/scripts/generateJson.js
Normal file
106
dist_scripts/scripts/generateJson.js
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var fs = __importStar(require("fs-extra"));
|
||||
var paths = __importStar(require("./paths"));
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", function (err) {
|
||||
throw err;
|
||||
});
|
||||
fs.removeSync(paths.templateNodeModules);
|
||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
||||
var generateJson = function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var createRadar, save, radar, e_1;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
createRadar = require("./generateJson/radar").createRadar;
|
||||
save = require("./generateJson/file").save;
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 4, , 5]);
|
||||
return [4 /*yield*/, createRadar()];
|
||||
case 2:
|
||||
radar = _a.sent();
|
||||
return [4 /*yield*/, save(JSON.stringify(radar), paths.radarJson)];
|
||||
case 3:
|
||||
_a.sent();
|
||||
return [3 /*break*/, 5];
|
||||
case 4:
|
||||
e_1 = _a.sent();
|
||||
console.error("error:", e_1);
|
||||
return [3 /*break*/, 5];
|
||||
case 5: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
generateJson()
|
||||
.then(function () {
|
||||
console.log(paths.appRdJson + " created.");
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err && err.message) {
|
||||
console.error(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
111
dist_scripts/scripts/generateJson/file.js
Normal file
111
dist_scripts/scripts/generateJson/file.js
Normal file
@@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
|
||||
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
|
||||
to[j] = from[i];
|
||||
return to;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.save = exports.getAllMarkdownFiles = exports.buildPath = exports.jsPath = exports.faviconPath = exports.stylesPath = exports.radarPath = exports.relativePath = void 0;
|
||||
var fs_extra_1 = require("fs-extra");
|
||||
var path = __importStar(require("path"));
|
||||
var walk_1 = require("walk");
|
||||
var relativePath = function () {
|
||||
var relativePath = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
relativePath[_i] = arguments[_i];
|
||||
}
|
||||
return path.resolve.apply(path, relativePath);
|
||||
};
|
||||
exports.relativePath = relativePath;
|
||||
var radarPath = function () {
|
||||
var pathInSrc = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
pathInSrc[_i] = arguments[_i];
|
||||
}
|
||||
return exports.relativePath.apply(void 0, __spreadArray(["radar"], pathInSrc));
|
||||
};
|
||||
exports.radarPath = radarPath;
|
||||
var stylesPath = function () {
|
||||
var pathInSrc = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
pathInSrc[_i] = arguments[_i];
|
||||
}
|
||||
return exports.relativePath.apply(void 0, __spreadArray(["styles"], pathInSrc));
|
||||
};
|
||||
exports.stylesPath = stylesPath;
|
||||
var faviconPath = function () {
|
||||
var pathInSrc = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
pathInSrc[_i] = arguments[_i];
|
||||
}
|
||||
return exports.relativePath.apply(void 0, __spreadArray(["assets/favicon.ico"], pathInSrc));
|
||||
};
|
||||
exports.faviconPath = faviconPath;
|
||||
var jsPath = function () {
|
||||
var pathInSrc = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
pathInSrc[_i] = arguments[_i];
|
||||
}
|
||||
return exports.relativePath.apply(void 0, __spreadArray(["js"], pathInSrc));
|
||||
};
|
||||
exports.jsPath = jsPath;
|
||||
var buildPath = function () {
|
||||
var pathInDist = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
pathInDist[_i] = arguments[_i];
|
||||
}
|
||||
return exports.relativePath.apply(void 0, __spreadArray(["build"], pathInDist));
|
||||
};
|
||||
exports.buildPath = buildPath;
|
||||
var getAllMarkdownFiles = function (folder) {
|
||||
return getAllFiles(folder, isMarkdownFile);
|
||||
};
|
||||
exports.getAllMarkdownFiles = getAllMarkdownFiles;
|
||||
var getAllFiles = function (folder, predicate) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var walker = walk_1.walk(folder, { followLinks: false });
|
||||
var files = [];
|
||||
walker.on("file", function (root, fileStat, next) {
|
||||
if (predicate(fileStat.name)) {
|
||||
files.push(path.resolve(root, fileStat.name));
|
||||
}
|
||||
next();
|
||||
});
|
||||
walker.on("errors", function (root, nodeStatsArray, next) {
|
||||
nodeStatsArray.forEach(function (n) {
|
||||
console.error("[ERROR] " + n.name);
|
||||
if (n.error) {
|
||||
console.error(n.error.message || n.error.code + ": " + n.error.path);
|
||||
}
|
||||
});
|
||||
next();
|
||||
});
|
||||
walker.on("end", function () {
|
||||
resolve(files.sort());
|
||||
});
|
||||
});
|
||||
};
|
||||
var isMarkdownFile = function (name) { return name.match(/\.md$/) !== null; };
|
||||
var save = function (data, fileName) {
|
||||
return fs_extra_1.outputFile(exports.buildPath(fileName), data);
|
||||
};
|
||||
exports.save = save;
|
||||
217
dist_scripts/scripts/generateJson/radar.js
Normal file
217
dist_scripts/scripts/generateJson/radar.js
Normal file
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
|
||||
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
|
||||
to[j] = from[i];
|
||||
return to;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createRadar = void 0;
|
||||
var fs_extra_1 = require("fs-extra");
|
||||
var path = __importStar(require("path"));
|
||||
var front_matter_1 = __importDefault(require("front-matter"));
|
||||
// @ts-ignore esModuleInterop is activated in tsconfig.scripts.json, but IDE typescript uses default typescript config
|
||||
var marked_1 = __importDefault(require("marked"));
|
||||
var highlight_js_1 = __importDefault(require("highlight.js"));
|
||||
var file_1 = require("./file");
|
||||
var config_1 = require("../../src/config");
|
||||
marked_1.default.setOptions({
|
||||
highlight: function (code) { return highlight_js_1.default.highlightAuto(code).value; },
|
||||
});
|
||||
var createRadar = function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var fileNames, revisions, allReleases, items, flaggedItems;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, file_1.getAllMarkdownFiles(file_1.radarPath())];
|
||||
case 1:
|
||||
fileNames = _a.sent();
|
||||
return [4 /*yield*/, createRevisionsFromFiles(fileNames)];
|
||||
case 2:
|
||||
revisions = _a.sent();
|
||||
allReleases = getAllReleases(revisions);
|
||||
items = createItems(revisions);
|
||||
flaggedItems = flagItem(items, allReleases);
|
||||
return [2 /*return*/, {
|
||||
items: flaggedItems,
|
||||
releases: allReleases,
|
||||
}];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
exports.createRadar = createRadar;
|
||||
var checkAttributes = function (fileName, attributes) {
|
||||
if (attributes.ring && !config_1.rings.includes(attributes.ring)) {
|
||||
throw new Error("Error: " + fileName + " has an illegal value for 'ring' - must be one of " + config_1.rings);
|
||||
}
|
||||
if (attributes.quadrant && !config_1.quadrants.includes(attributes.quadrant)) {
|
||||
throw new Error("Error: " + fileName + " has an illegal value for 'quadrant' - must be one of " + config_1.quadrants);
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
var createRevisionsFromFiles = function (fileNames) {
|
||||
return Promise.all(fileNames.map(function (fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs_extra_1.readFile(fileName, "utf8", function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
var fm = front_matter_1.default(data);
|
||||
// add target attribute to external links
|
||||
// todo: check path
|
||||
var html = marked_1.default(fm.body.replace(/\]\(\//g, "](/techradar/"));
|
||||
html = html.replace(/a href="http/g, 'a target="_blank" rel="noopener noreferrer" href="http');
|
||||
resolve(__assign(__assign(__assign({}, itemInfoFromFilename(fileName)), checkAttributes(fileName, fm.attributes)), { fileName: fileName, body: html }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
};
|
||||
var itemInfoFromFilename = function (fileName) {
|
||||
var _a = fileName.split(path.sep).slice(-2), release = _a[0], name = _a[1];
|
||||
return {
|
||||
name: path.basename(name, ".md"),
|
||||
release: release,
|
||||
};
|
||||
};
|
||||
var getAllReleases = function (revisions) {
|
||||
return revisions
|
||||
.reduce(function (allReleases, _a) {
|
||||
var release = _a.release;
|
||||
if (!allReleases.includes(release)) {
|
||||
return __spreadArray(__spreadArray([], allReleases), [release]);
|
||||
}
|
||||
return allReleases;
|
||||
}, [])
|
||||
.sort();
|
||||
};
|
||||
var createItems = function (revisions) {
|
||||
var itemMap = revisions.reduce(function (items, revision) {
|
||||
var _a;
|
||||
return __assign(__assign({}, items), (_a = {}, _a[revision.name] = addRevisionToItem(items[revision.name], revision), _a));
|
||||
}, {});
|
||||
return Object.values(itemMap)
|
||||
.map(function (item) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, item), (_a = {}, _a["title"] = item.title || item.name, _a)));
|
||||
})
|
||||
.sort(function (x, y) { return (x.name > y.name ? 1 : -1); });
|
||||
};
|
||||
var ignoreEmptyRevisionBody = function (revision, item) {
|
||||
if (!revision.body || revision.body.trim() === "") {
|
||||
return item.body;
|
||||
}
|
||||
return revision.body;
|
||||
};
|
||||
var addRevisionToItem = function (item, revision) {
|
||||
if (item === void 0) { item = {
|
||||
flag: "default",
|
||||
featured: true,
|
||||
revisions: [],
|
||||
name: "",
|
||||
title: "",
|
||||
ring: "trial",
|
||||
quadrant: "",
|
||||
body: "",
|
||||
info: "",
|
||||
}; }
|
||||
var newItem = __assign(__assign(__assign({}, item), revision), { body: ignoreEmptyRevisionBody(revision, item) });
|
||||
if (revisionCreatesNewHistoryEntry(revision)) {
|
||||
newItem = __assign(__assign({}, newItem), { revisions: __spreadArray([revision], newItem.revisions) });
|
||||
}
|
||||
return newItem;
|
||||
};
|
||||
var revisionCreatesNewHistoryEntry = function (revision) {
|
||||
return revision.body.trim() !== "" || typeof revision.ring !== "undefined";
|
||||
};
|
||||
var flagItem = function (items, allReleases) {
|
||||
return items.map(function (item) {
|
||||
return (__assign(__assign({}, item), { flag: getItemFlag(item, allReleases) }));
|
||||
}, []);
|
||||
};
|
||||
var isInLastRelease = function (item, allReleases) {
|
||||
return item.revisions[0].release === allReleases[allReleases.length - 1];
|
||||
};
|
||||
var isNewItem = function (item, allReleases) {
|
||||
return item.revisions.length === 1 && isInLastRelease(item, allReleases);
|
||||
};
|
||||
var hasItemChanged = function (item, allReleases) {
|
||||
return item.revisions.length > 1 && isInLastRelease(item, allReleases);
|
||||
};
|
||||
var getItemFlag = function (item, allReleases) {
|
||||
if (isNewItem(item, allReleases)) {
|
||||
return "new";
|
||||
}
|
||||
if (hasItemChanged(item, allReleases)) {
|
||||
return "changed";
|
||||
}
|
||||
return "default";
|
||||
};
|
||||
24
dist_scripts/scripts/paths.js
Normal file
24
dist_scripts/scripts/paths.js
Normal file
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.appNodeModules = exports.appYarnLock = exports.appPublic = exports.appBuild = exports.appRdJson = exports.templateNodeModules = exports.templateBuild = exports.template = exports.radarJson = void 0;
|
||||
var path_1 = require("path");
|
||||
var fs_1 = require("fs");
|
||||
exports.radarJson = "rd.json";
|
||||
var appDirectory = fs_1.realpathSync(process.cwd());
|
||||
var resolveApp = function (relativePath) {
|
||||
if (relativePath === void 0) { relativePath = ""; }
|
||||
return path_1.resolve(appDirectory, relativePath);
|
||||
};
|
||||
var templateDirectory = fs_1.realpathSync(__dirname);
|
||||
var resolveTemplate = function (relativePath) {
|
||||
if (relativePath === void 0) { relativePath = ""; }
|
||||
return path_1.resolve(templateDirectory, "../..", relativePath);
|
||||
};
|
||||
exports.template = resolveTemplate();
|
||||
exports.templateBuild = resolveTemplate("build");
|
||||
exports.templateNodeModules = resolveTemplate("node_modules");
|
||||
exports.appRdJson = resolveApp("build/" + exports.radarJson);
|
||||
exports.appBuild = resolveApp("build");
|
||||
exports.appPublic = resolveApp("public");
|
||||
exports.appYarnLock = resolveApp("yarn.lock");
|
||||
exports.appNodeModules = resolveApp("node_modules");
|
||||
39
dist_scripts/src/config.js
Normal file
39
dist_scripts/src/config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.assetUrl = exports.isMobileViewport = exports.translate = exports.showEmptyRings = exports.getItemPageNames = exports.rings = exports.quadrants = exports.radarNameShort = exports.radarName = void 0;
|
||||
exports.radarName = process.env.REACT_APP_RADAR_NAME || "AOE Technology Radar";
|
||||
exports.radarNameShort = exports.radarName;
|
||||
exports.quadrants = [
|
||||
"languages-and-frameworks",
|
||||
"methods-and-patterns",
|
||||
"platforms-and-aoe-services",
|
||||
"tools",
|
||||
];
|
||||
exports.rings = ["all", "adopt", "trial", "assess", "hold"];
|
||||
var getItemPageNames = function (items) {
|
||||
return items.map(function (item) { return item.quadrant + "/" + item.name; });
|
||||
};
|
||||
exports.getItemPageNames = getItemPageNames;
|
||||
exports.showEmptyRings = false;
|
||||
var messages = {
|
||||
"languages-and-frameworks": "Languages & Frameworks",
|
||||
"methods-and-patterns": "Methods & Patterns",
|
||||
"platforms-and-aoe-services": "Platforms and Operations",
|
||||
tools: "Tools",
|
||||
};
|
||||
var translate = function (key) { return messages[key] || "-"; };
|
||||
exports.translate = translate;
|
||||
function isMobileViewport() {
|
||||
// return false for server side rendering
|
||||
if (typeof window == "undefined")
|
||||
return false;
|
||||
var width = window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
document.body.clientWidth;
|
||||
return width < 1200;
|
||||
}
|
||||
exports.isMobileViewport = isMobileViewport;
|
||||
function assetUrl(file) {
|
||||
return process.env.PUBLIC_URL + "/" + file;
|
||||
}
|
||||
exports.assetUrl = assetUrl;
|
||||
56
dist_scripts/src/model.js
Normal file
56
dist_scripts/src/model.js
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
|
||||
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
|
||||
to[j] = from[i];
|
||||
return to;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.featuredOnly = void 0;
|
||||
var featuredOnly = function (items) { return items.filter(function (item) { return item.featured; }); };
|
||||
exports.featuredOnly = featuredOnly;
|
||||
var groupByQuadrants = function (items) {
|
||||
return items.reduce(function (quadrants, item) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, quadrants), (_a = {}, _a[item.quadrant] = addItemToQuadrant(quadrants[item.quadrant], item), _a)));
|
||||
}, {});
|
||||
};
|
||||
exports.groupByQuadrants = groupByQuadrants;
|
||||
var groupByFirstLetter = function (items) {
|
||||
var index = items.reduce(function (letterIndex, item) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, letterIndex), (_a = {}, _a[exports.getFirstLetter(item)] = addItemToList(letterIndex[exports.getFirstLetter(item)], item), _a)));
|
||||
}, {});
|
||||
return Object.keys(index)
|
||||
.sort()
|
||||
.map(function (letter) { return ({
|
||||
letter: letter,
|
||||
items: index[letter],
|
||||
}); });
|
||||
};
|
||||
exports.groupByFirstLetter = groupByFirstLetter;
|
||||
var addItemToQuadrant = function (quadrant, item) {
|
||||
var _a;
|
||||
if (quadrant === void 0) { quadrant = {}; }
|
||||
return (__assign(__assign({}, quadrant), (_a = {}, _a[item.ring] = addItemToRing(quadrant[item.ring], item), _a)));
|
||||
};
|
||||
var addItemToList = function (list, item) {
|
||||
if (list === void 0) { list = []; }
|
||||
return __spreadArray(__spreadArray([], list), [item]);
|
||||
};
|
||||
var addItemToRing = function (ring, item) {
|
||||
if (ring === void 0) { ring = []; }
|
||||
return __spreadArray(__spreadArray([], ring), [item]);
|
||||
};
|
||||
var getFirstLetter = function (item) { return item.title.substr(0, 1).toUpperCase(); };
|
||||
exports.getFirstLetter = getFirstLetter;
|
||||
120
package.json
120
package.json
@@ -3,12 +3,63 @@
|
||||
"version": "2.0.0",
|
||||
"description": "AOE Technology Radar",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
"author": "AOE GmbH <contact-de@aoe.com> (http://www.aoe.com)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AOEpeople/aoe_technology_radar.git"
|
||||
},
|
||||
"bin": {
|
||||
"aoe_technology_radar": "scripts/build.js",
|
||||
"aoe_technology_radar-static": "bin/tasks/create-static.js"
|
||||
"aoe_technology_radar-generateJson": "dist_scripts/scripts/generateJson.js",
|
||||
"aoe_technology_radar-buildRadar": "dist_scripts/scripts/buildRadar.js",
|
||||
"aoe_technology_radar-createStaticFiles": "dist_scripts/scripts/createStaticFiles.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install && yarn build:scripts",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"build:scripts": "tsc --project tsconfig.scripts.json",
|
||||
"test": "react-scripts test",
|
||||
"ts:check": "tsc --noEmit",
|
||||
"lint": "yarn ts:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/marked": "^2.0.3",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/walk": "^2.3.0",
|
||||
"classnames": "^2.3.1",
|
||||
"front-matter": "^4.0.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"highlight.js": "^11.0.0",
|
||||
"marked": "^2.0.7",
|
||||
"moment": "^2.29.1",
|
||||
"query-string": "^7.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"sass": "^1.34.0",
|
||||
"typescript": "^4.1.2",
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.15",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -21,66 +72,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"author": "AOE GmbH <contact-de@aoe.com> (http://www.aoe.com)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/highlight.js": "^9.12.4",
|
||||
"@types/marked": "^1.1.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/walk": "^2.3.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-named-asset-import": "^0.3.6",
|
||||
"babel-preset-react-app": "^9.1.2",
|
||||
"camelcase": "^5.3.1",
|
||||
"case-sensitive-paths-webpack-plugin": "2.3.0",
|
||||
"classnames": "^2.2.6",
|
||||
"css-loader": "3.4.2",
|
||||
"dotenv": "8.2.0",
|
||||
"dotenv-expand": "5.1.0",
|
||||
"file-loader": "4.3.0",
|
||||
"front-matter": "2.3.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "10.5.0",
|
||||
"html-webpack-plugin": "4.0.0-beta.11",
|
||||
"jest": "^26.6.3",
|
||||
"marked": "0.3.18",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"moment": "2.22.1",
|
||||
"node-sass": "4",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-flexbugs-fixes": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "8.0.1",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"postcss-safe-parser": "4.0.1",
|
||||
"query-string": "^6.13.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"resolve": "1.15.0",
|
||||
"resolve-url-loader": "3.1.2",
|
||||
"sass-loader": "8.0.2",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "2.3.8",
|
||||
"ts-pnp": "1.1.6",
|
||||
"typescript": "^3.9.6",
|
||||
"url-loader": "2.3.0",
|
||||
"walk": "2.3.9",
|
||||
"webpack": "4.42.0",
|
||||
"webpack-dev-server": "3.11.1",
|
||||
"webpack-manifest-plugin": "2.2.0",
|
||||
"workbox-webpack-plugin": "4.3.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AOEpeople/aoe_technology_radar.git"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 239 KiB |
@@ -4,15 +4,15 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="%RADAR_NAME%" />
|
||||
<meta name="description" content="%REACT_APP_RADAR_NAME%" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
|
||||
<meta property="og:title" content="%RADAR_NAME%" />
|
||||
<meta property="og:title" content="%REACT_APP_RADAR_NAME%" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/logo.svg" />
|
||||
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="viewport" content="width=device-width, maximum-scale=1.0, initial-scale=1.0, user-scalable=0" />
|
||||
|
||||
<title>%RADAR_NAME%</title>
|
||||
<title>%REACT_APP_RADAR_NAME%</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
215
scripts/build.js
215
scripts/build.js
@@ -1,215 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const {checkBrowsers} = require('react-dev-utils/browsersHelper');
|
||||
require('../bin/tasks/radarjson').radarJsonGenerator()
|
||||
.then(checkBrowsers(paths.appPath, isInteractive))
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({stats, previousFileSizes, warnings}) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
'\nSearch for the ' +
|
||||
chalk.underline(chalk.yellow('keywords')) +
|
||||
' to learn more about each warning.'
|
||||
);
|
||||
console.log(
|
||||
'To ignore, add ' +
|
||||
chalk.cyan('// eslint-disable-next-line') +
|
||||
' to the line before.\n'
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrlOrPath;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
if (tscCompileOnError) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
||||
)
|
||||
);
|
||||
printBuildError(err);
|
||||
} else {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let errMessage = err.message;
|
||||
|
||||
// Add additional information for postcss errors
|
||||
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||
errMessage +=
|
||||
'\nCompileError: Begins at CSS selector ' +
|
||||
err['postcssNode'].selector;
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [errMessage],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({all: false, warnings: true, errors: true})
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml,
|
||||
});
|
||||
}
|
||||
|
||||
64
scripts/buildRadar.ts
Normal file
64
scripts/buildRadar.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs-extra";
|
||||
import { spawn } from "child_process";
|
||||
import * as paths from "./paths";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
fs.removeSync(paths.templateNodeModules);
|
||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
||||
|
||||
const runCommand = (command: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const executedCommand = spawn(command, {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
executedCommand.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
executedCommand.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const buildTemplate = () => {
|
||||
const packageManager = fs.existsSync(paths.appYarnLock) ? "yarn" : "npx";
|
||||
|
||||
fs.emptyDirSync(paths.templateBuild);
|
||||
process.chdir(paths.template);
|
||||
|
||||
return runCommand(`${packageManager} build`);
|
||||
};
|
||||
|
||||
if (fs.existsSync(paths.appRdJson)) {
|
||||
buildTemplate().then(() => {
|
||||
fs.copySync(paths.templateBuild, paths.appBuild);
|
||||
fs.copySync(paths.appPublic, paths.appBuild);
|
||||
console.log(`${paths.appBuild} was created and can be deployed.`);
|
||||
});
|
||||
} else {
|
||||
console.error(
|
||||
`${paths.appRdJson} does not exist. You have to generate it first.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
44
scripts/createStaticFiles.ts
Normal file
44
scripts/createStaticFiles.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { copyFileSync, mkdirSync, existsSync } from "fs";
|
||||
import { createRadar } from "./generateJson/radar";
|
||||
import { quadrants } from "../src/config";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log("starting static");
|
||||
const radar = await createRadar();
|
||||
|
||||
copyFileSync("build/index.html", "build/overview.html");
|
||||
copyFileSync("build/index.html", "build/help-and-about-tech-radar.html");
|
||||
|
||||
quadrants.forEach((quadrant) => {
|
||||
const destFolder = `build/${quadrant}`;
|
||||
copyFileSync("build/index.html", `${destFolder}.html`);
|
||||
if (!existsSync(destFolder)) {
|
||||
mkdirSync(destFolder);
|
||||
}
|
||||
});
|
||||
radar.items.forEach((item) => {
|
||||
copyFileSync(
|
||||
"build/index.html",
|
||||
`build/${item.quadrant}/${item.name}.html`
|
||||
);
|
||||
});
|
||||
|
||||
console.log("created static files.");
|
||||
} catch (e) {
|
||||
console.error("error:", e);
|
||||
}
|
||||
})();
|
||||
42
scripts/generateJson.ts
Normal file
42
scripts/generateJson.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs-extra";
|
||||
import * as paths from "./paths";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
fs.removeSync(paths.templateNodeModules);
|
||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
||||
|
||||
const generateJson = async () => {
|
||||
const { createRadar } = require("./generateJson/radar");
|
||||
const { save } = require("./generateJson/file");
|
||||
|
||||
try {
|
||||
const radar = await createRadar();
|
||||
|
||||
await save(JSON.stringify(radar), paths.radarJson);
|
||||
} catch (e) {
|
||||
console.error("error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
generateJson()
|
||||
.then(() => {
|
||||
console.log(`${paths.appRdJson} created.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message) {
|
||||
console.error(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
59
scripts/generateJson/file.ts
Normal file
59
scripts/generateJson/file.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { outputFile } from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { walk } from "walk";
|
||||
|
||||
export const relativePath = (...relativePath: string[]): string =>
|
||||
path.resolve(...relativePath);
|
||||
|
||||
export const radarPath = (...pathInSrc: string[]) =>
|
||||
relativePath("radar", ...pathInSrc);
|
||||
|
||||
export const stylesPath = (...pathInSrc: string[]) =>
|
||||
relativePath("styles", ...pathInSrc);
|
||||
|
||||
export const faviconPath = (...pathInSrc: string[]) =>
|
||||
relativePath("assets/favicon.ico", ...pathInSrc);
|
||||
|
||||
export const jsPath = (...pathInSrc: string[]) =>
|
||||
relativePath("js", ...pathInSrc);
|
||||
|
||||
export const buildPath = (...pathInDist: string[]) =>
|
||||
relativePath("build", ...pathInDist);
|
||||
|
||||
export const getAllMarkdownFiles = (folder: string) =>
|
||||
getAllFiles(folder, isMarkdownFile);
|
||||
|
||||
const getAllFiles = (
|
||||
folder: string,
|
||||
predicate: (s: string) => boolean
|
||||
): Promise<string[]> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const walker = walk(folder, { followLinks: false });
|
||||
const files: string[] = [];
|
||||
|
||||
walker.on("file", (root, fileStat, next) => {
|
||||
if (predicate(fileStat.name)) {
|
||||
files.push(path.resolve(root, fileStat.name));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
walker.on("errors", (root, nodeStatsArray, next) => {
|
||||
nodeStatsArray.forEach(function (n) {
|
||||
console.error("[ERROR] " + n.name);
|
||||
if (n.error) {
|
||||
console.error(n.error.message || n.error.code + ": " + n.error.path);
|
||||
}
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
walker.on("end", () => {
|
||||
resolve(files.sort());
|
||||
});
|
||||
});
|
||||
|
||||
const isMarkdownFile = (name: string) => name.match(/\.md$/) !== null;
|
||||
|
||||
export const save = (data: string | Buffer | DataView, fileName: string) =>
|
||||
outputFile(buildPath(fileName), data);
|
||||
@@ -1,16 +1,18 @@
|
||||
import { readFile } from 'fs-extra';
|
||||
import path from 'path';
|
||||
import frontmatter from 'front-matter';
|
||||
import marked from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import { quadrants, rings } from '../src/config';
|
||||
import { radarPath, getAllMarkdownFiles } from './file';
|
||||
import { Item, Revision, ItemAttributes, Radar } from '../src/model';
|
||||
import { readFile } from "fs-extra";
|
||||
import * as path from "path";
|
||||
import frontMatter from "front-matter";
|
||||
// @ts-ignore esModuleInterop is activated in tsconfig.scripts.json, but IDE typescript uses default typescript config
|
||||
import marked from "marked";
|
||||
import highlight from "highlight.js";
|
||||
|
||||
type FMAttributes = ItemAttributes
|
||||
import { radarPath, getAllMarkdownFiles } from "./file";
|
||||
import { quadrants, rings } from "../../src/config";
|
||||
import { Item, Revision, ItemAttributes, Radar } from "../../src/model";
|
||||
|
||||
type FMAttributes = ItemAttributes;
|
||||
|
||||
marked.setOptions({
|
||||
highlight: code => hljs.highlightAuto(code).value,
|
||||
highlight: (code) => highlight.highlightAuto(code).value,
|
||||
});
|
||||
|
||||
export const createRadar = async (): Promise<Radar> => {
|
||||
@@ -28,31 +30,35 @@ export const createRadar = async (): Promise<Radar> => {
|
||||
|
||||
const checkAttributes = (fileName: string, attributes: FMAttributes) => {
|
||||
if (attributes.ring && !rings.includes(attributes.ring)) {
|
||||
throw new Error(`Error: ${fileName} has an illegal value for 'ring' - must be one of ${rings}`);
|
||||
throw new Error(
|
||||
`Error: ${fileName} has an illegal value for 'ring' - must be one of ${rings}`
|
||||
);
|
||||
}
|
||||
|
||||
if (attributes.quadrant && !quadrants.includes(attributes.quadrant)) {
|
||||
throw new Error(`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${quadrants}`);
|
||||
throw new Error(
|
||||
`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${quadrants}`
|
||||
);
|
||||
}
|
||||
|
||||
return attributes
|
||||
return attributes;
|
||||
};
|
||||
|
||||
const createRevisionsFromFiles = (fileNames: string[]) =>
|
||||
Promise.all(
|
||||
fileNames.map(fileName => {
|
||||
fileNames.map((fileName) => {
|
||||
return new Promise<Revision>((resolve, reject) => {
|
||||
readFile(fileName, 'utf8', (err, data) => {
|
||||
readFile(fileName, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const fm = frontmatter<FMAttributes>(data);
|
||||
const fm = frontMatter<FMAttributes>(data);
|
||||
// add target attribute to external links
|
||||
// todo: check path
|
||||
let html = marked(fm.body.replace(/\]\(\//g, '](/techradar/'));
|
||||
let html = marked(fm.body.replace(/\]\(\//g, "](/techradar/"));
|
||||
html = html.replace(
|
||||
/a href="http/g,
|
||||
'a target="_blank" rel="noopener noreferrer" href="http',
|
||||
'a target="_blank" rel="noopener noreferrer" href="http'
|
||||
);
|
||||
|
||||
resolve({
|
||||
@@ -64,13 +70,13 @@ const createRevisionsFromFiles = (fileNames: string[]) =>
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const itemInfoFromFilename = (fileName: string) => {
|
||||
const [release, name] = fileName.split(path.sep).slice(-2);
|
||||
return {
|
||||
name: path.basename(name, '.md'),
|
||||
name: path.basename(name, ".md"),
|
||||
release,
|
||||
};
|
||||
};
|
||||
@@ -86,18 +92,23 @@ const getAllReleases = (revisions: Revision[]) =>
|
||||
.sort();
|
||||
|
||||
const createItems = (revisions: Revision[]) => {
|
||||
const itemMap = revisions.reduce<{[name: string]: Item}>((items, revision) => {
|
||||
const itemMap = revisions.reduce<{ [name: string]: Item }>(
|
||||
(items, revision) => {
|
||||
return {
|
||||
...items,
|
||||
[revision.name]: addRevisionToItem(items[revision.name], revision),
|
||||
};
|
||||
}, {});
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return Object.values(itemMap).map(item => ({...item, ['title']: item.title || item.name})).sort((x, y) => (x.name > y.name ? 1 : -1));
|
||||
return Object.values(itemMap)
|
||||
.map((item) => ({ ...item, ["title"]: item.title || item.name }))
|
||||
.sort((x, y) => (x.name > y.name ? 1 : -1));
|
||||
};
|
||||
|
||||
const ignoreEmptyRevisionBody = (revision: Revision, item: Item) => {
|
||||
if (!revision.body || revision.body.trim() === '') {
|
||||
if (!revision.body || revision.body.trim() === "") {
|
||||
return item.body;
|
||||
}
|
||||
return revision.body;
|
||||
@@ -105,17 +116,17 @@ const ignoreEmptyRevisionBody = (revision: Revision, item: Item) => {
|
||||
|
||||
const addRevisionToItem = (
|
||||
item: Item = {
|
||||
flag: 'default',
|
||||
flag: "default",
|
||||
featured: true,
|
||||
revisions: [],
|
||||
name: '',
|
||||
title: '',
|
||||
ring: 'trial',
|
||||
quadrant: '',
|
||||
body: '',
|
||||
info: '',
|
||||
name: "",
|
||||
title: "",
|
||||
ring: "trial",
|
||||
quadrant: "",
|
||||
body: "",
|
||||
info: "",
|
||||
},
|
||||
revision: Revision,
|
||||
revision: Revision
|
||||
): Item => {
|
||||
let newItem: Item = {
|
||||
...item,
|
||||
@@ -134,16 +145,17 @@ const addRevisionToItem = (
|
||||
};
|
||||
|
||||
const revisionCreatesNewHistoryEntry = (revision: Revision) => {
|
||||
return revision.body.trim() !== '' || typeof revision.ring !== 'undefined';
|
||||
return revision.body.trim() !== "" || typeof revision.ring !== "undefined";
|
||||
};
|
||||
|
||||
const flagItem = (items: Item[], allReleases: string[]) =>
|
||||
items.map(
|
||||
item => ({
|
||||
(item) =>
|
||||
({
|
||||
...item,
|
||||
flag: getItemFlag(item, allReleases),
|
||||
} as Item),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const isInLastRelease = (item: Item, allReleases: string[]) =>
|
||||
@@ -157,10 +169,10 @@ const hasItemChanged = (item: Item, allReleases: string[]) =>
|
||||
|
||||
const getItemFlag = (item: Item, allReleases: string[]): string => {
|
||||
if (isNewItem(item, allReleases)) {
|
||||
return 'new';
|
||||
return "new";
|
||||
}
|
||||
if (hasItemChanged(item, allReleases)) {
|
||||
return 'changed';
|
||||
return "changed";
|
||||
}
|
||||
return 'default';
|
||||
return "default";
|
||||
};
|
||||
18
scripts/paths.ts
Normal file
18
scripts/paths.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { resolve } from "path";
|
||||
import { realpathSync } from "fs";
|
||||
|
||||
export const radarJson = "rd.json";
|
||||
const appDirectory = realpathSync(process.cwd());
|
||||
const resolveApp = (relativePath = "") => resolve(appDirectory, relativePath);
|
||||
const templateDirectory = realpathSync(__dirname);
|
||||
const resolveTemplate = (relativePath = "") =>
|
||||
resolve(templateDirectory, "../..", relativePath);
|
||||
|
||||
export const template = resolveTemplate();
|
||||
export const templateBuild = resolveTemplate("build");
|
||||
export const templateNodeModules = resolveTemplate("node_modules");
|
||||
export const appRdJson = resolveApp(`build/${radarJson}`);
|
||||
export const appBuild = resolveApp("build");
|
||||
export const appPublic = resolveApp("public");
|
||||
export const appYarnLock = resolveApp("yarn.lock");
|
||||
export const appNodeModules = resolveApp("node_modules");
|
||||
168
scripts/start.js
168
scripts/start.js
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const paths = require('../config/paths');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
const urls = prepareUrls(
|
||||
protocol,
|
||||
HOST,
|
||||
port,
|
||||
paths.publicUrlOrPath.slice(0, -1)
|
||||
);
|
||||
const devSocket = {
|
||||
warnings: warnings =>
|
||||
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
|
||||
errors: errors =>
|
||||
devServer.sockWrite(devServer.sockets, 'errors', errors),
|
||||
};
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
tscCompileOnError,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(
|
||||
proxySetting,
|
||||
paths.appPublic,
|
||||
paths.publicUrlOrPath
|
||||
);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(function(sig) {
|
||||
process.on(sig, function() {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
||||
if (isInteractive || process.env.CI !== 'true') {
|
||||
// Gracefully exit when stdin ends
|
||||
process.stdin.on('end', function() {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
process.stdin.resume();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PUBLIC_URL = '';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const jest = require('jest');
|
||||
const execSync = require('child_process').execSync;
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
function isInGitRepository() {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInMercurialRepository() {
|
||||
try {
|
||||
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch unless on CI or explicitly running all tests
|
||||
if (
|
||||
!process.env.CI &&
|
||||
argv.indexOf('--watchAll') === -1 &&
|
||||
argv.indexOf('--watchAll=false') === -1
|
||||
) {
|
||||
// https://github.com/facebook/create-react-app/issues/5210
|
||||
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
||||
@@ -1,12 +0,0 @@
|
||||
// Add Google Analytics
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-839059-1', 'auto');
|
||||
|
||||
export function track() {
|
||||
ga('set', 'location', document.location.href);
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
@@ -1,57 +1,103 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Header from './Header/Header';
|
||||
import Footer from './Footer/Footer';
|
||||
import Router from './Router';
|
||||
import {BrowserRouter, Switch, Route, Redirect, useParams, useLocation} from 'react-router-dom';
|
||||
import {Item} from '../model';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import Header from "./Header/Header";
|
||||
import Footer from "./Footer/Footer";
|
||||
import Router from "./Router";
|
||||
import {
|
||||
BrowserRouter,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useParams,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { Item } from "../model";
|
||||
|
||||
interface Params {
|
||||
page: string;
|
||||
}
|
||||
|
||||
const useQuery = () => {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
};
|
||||
|
||||
const RouterWithPageParam = ({items, releases}: { items: Item[], releases: string[] }) => {
|
||||
const {page} = useParams();
|
||||
const RouterWithPageParam = ({
|
||||
items,
|
||||
releases,
|
||||
}: {
|
||||
items: Item[];
|
||||
releases: string[];
|
||||
}) => {
|
||||
const { page } = useParams<Params>();
|
||||
const query = useQuery();
|
||||
|
||||
return <Router pageName={page} search={query.get('search') || ''} items={items} releases={releases}/>;
|
||||
return (
|
||||
<Router
|
||||
pageName={page}
|
||||
search={query.get("search") || ""}
|
||||
items={items}
|
||||
releases={releases}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderWithPageParam = () => {
|
||||
const {page} = useParams();
|
||||
const { page } = useParams<Params>();
|
||||
|
||||
return <Header pageName={page}/>
|
||||
return <Header pageName={page} />;
|
||||
};
|
||||
|
||||
const FooterWithPageParam = ({ items }: { items: Item[] }) => {
|
||||
const {page} = useParams();
|
||||
const { page } = useParams<Params>();
|
||||
|
||||
return <Footer pageName={page} items={items}/>
|
||||
return <Footer pageName={page} items={items} />;
|
||||
};
|
||||
|
||||
export default function App({items, releases}: { items: Item[], releases: string[] }) {
|
||||
interface Data {
|
||||
items: Item[];
|
||||
releases: string[];
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = React.useState<Data>();
|
||||
React.useEffect(() => {
|
||||
fetch(`${process.env.PUBLIC_URL}/rd.json`)
|
||||
.then((response) => response.json())
|
||||
.then((data: Data) => {
|
||||
setData(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("fetch data", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (data) {
|
||||
const { items, releases } = data;
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<BrowserRouter basename={`${process.env.PUBLIC_URL}`}>
|
||||
<Switch>
|
||||
<Route path={'/techradar/:page(.+).html'}>
|
||||
<Route path={"/:page(.+).html"}>
|
||||
<div>
|
||||
<div className='page'>
|
||||
<div className='page__header'>
|
||||
<div className="page">
|
||||
<div className="page__header">
|
||||
<HeaderWithPageParam />
|
||||
</div>
|
||||
<div className={classNames('page__content')}>
|
||||
<div className={classNames("page__content")}>
|
||||
<RouterWithPageParam items={items} releases={releases} />
|
||||
</div>
|
||||
<div className='page__footer'>
|
||||
<div className="page__footer">
|
||||
<FooterWithPageParam items={items} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Route>
|
||||
<Route path={'/'}>
|
||||
<Redirect to={'/techradar/index.html'}/>
|
||||
<Route path={"/"}>
|
||||
<Redirect to={"/index.html"} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import './flag.scss';
|
||||
import {FlagType} from "../../model";
|
||||
import React from "react";
|
||||
import "./flag.scss";
|
||||
|
||||
export type FlagType = "new" | "changed" | "default";
|
||||
|
||||
interface ItemFlag {
|
||||
flag: FlagType;
|
||||
}
|
||||
|
||||
export default function Flag({ item, short = false }: { item: ItemFlag; short?: boolean }) {
|
||||
export default function Flag({
|
||||
item,
|
||||
short = false,
|
||||
}: {
|
||||
item: ItemFlag;
|
||||
short?: boolean;
|
||||
}) {
|
||||
const ucFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
if (item.flag !== 'default') {
|
||||
if (item.flag !== "default") {
|
||||
let name = item.flag.toUpperCase();
|
||||
let title = ucFirst(item.flag);
|
||||
if (short === true) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Branding from '../Branding/Branding';
|
||||
import Link from '../Link/Link';
|
||||
import LogoLink from '../LogoLink/LogoLink';
|
||||
import Search from '../Search/Search';
|
||||
import { radarNameShort } from '../../config';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import React, { useState, useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import Branding from "../Branding/Branding";
|
||||
import Link from "../Link/Link";
|
||||
import LogoLink from "../LogoLink/LogoLink";
|
||||
import Search from "../Search/Search";
|
||||
import { radarNameShort } from "../../config";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import qs from "query-string";
|
||||
|
||||
export default function Header({ pageName }: { pageName: string }) {
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [search, setSearch] = useState("");
|
||||
const history = useHistory();
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -23,17 +23,17 @@ export default function Header({ pageName }: { pageName: string }) {
|
||||
};
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearch(value)
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSearchSubmit = () => {
|
||||
history.push({
|
||||
pathname: '/techradar/overview.html',
|
||||
pathname: "/overview.html",
|
||||
search: qs.stringify({ search: search }),
|
||||
})
|
||||
});
|
||||
|
||||
setSearchOpen(false);
|
||||
setSearch('');
|
||||
setSearch("");
|
||||
};
|
||||
|
||||
const handleOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -44,27 +44,37 @@ export default function Header({ pageName }: { pageName: string }) {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const smallLogo = pageName !== 'index';
|
||||
const smallLogo = pageName !== "index";
|
||||
|
||||
return (
|
||||
<Branding logoContent={<LogoLink small={smallLogo} />}>
|
||||
<div className='nav'>
|
||||
<div className='nav__item'>
|
||||
<Link pageName='help-and-about-tech-radar' className='icon-link'>
|
||||
<span className='icon icon--question icon-link__icon'/>How to Use {radarNameShort}?
|
||||
<div className="nav">
|
||||
<div className="nav__item">
|
||||
<Link pageName="help-and-about-tech-radar" className="icon-link">
|
||||
<span className="icon icon--question icon-link__icon" />
|
||||
How to Use {radarNameShort}?
|
||||
</Link>
|
||||
</div>
|
||||
<div className='nav__item'>
|
||||
<Link pageName='overview' className='icon-link'>
|
||||
<span className='icon icon--overview icon-link__icon'/>Technologies Overview
|
||||
<div className="nav__item">
|
||||
<Link pageName="overview" className="icon-link">
|
||||
<span className="icon icon--overview icon-link__icon" />
|
||||
Technologies Overview
|
||||
</Link>
|
||||
</div>
|
||||
<div className='nav__item'>
|
||||
<button className='icon-link' onClick={handleOpenClick}>
|
||||
<span className='icon icon--search icon-link__icon'/>Search
|
||||
<div className="nav__item">
|
||||
<button className="icon-link" onClick={handleOpenClick}>
|
||||
<span className="icon icon--search icon-link__icon" />
|
||||
Search
|
||||
</button>
|
||||
<div className={classNames('nav__search', { 'is-open': searchOpen })}>
|
||||
<Search value={search} onClose={closeSearch} onSubmit={handleSearchSubmit} onChange={handleSearchChange} open={searchOpen} ref={searchRef} />
|
||||
<div className={classNames("nav__search", { "is-open": searchOpen })}>
|
||||
<Search
|
||||
value={search}
|
||||
onClose={closeSearch}
|
||||
onSubmit={handleSearchSubmit}
|
||||
onChange={handleSearchChange}
|
||||
open={searchOpen}
|
||||
ref={searchRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Link as RLink } from 'react-router-dom';
|
||||
import './link.scss';
|
||||
import React from "react";
|
||||
import { Link as RLink } from "react-router-dom";
|
||||
import "./link.scss";
|
||||
type LinkProps = {
|
||||
pageName: string;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Link({ pageName, children, className, style = {} }: React.PropsWithChildren<LinkProps>) {
|
||||
function Link({
|
||||
pageName,
|
||||
children,
|
||||
className,
|
||||
style = {},
|
||||
}: React.PropsWithChildren<LinkProps>) {
|
||||
return (
|
||||
<RLink to={`/techradar/${pageName}.html`} style={style} {...{ className }}>
|
||||
<RLink to={`/${pageName}.html`} style={style} {...{ className }}>
|
||||
{children}
|
||||
</RLink>
|
||||
);
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
import {Item} from './model';
|
||||
import { Item } from "./model";
|
||||
|
||||
export const radarName = process.env.RADAR_NAME || 'AOE Technology Radar'
|
||||
export const radarName =
|
||||
process.env.REACT_APP_RADAR_NAME || "AOE Technology Radar";
|
||||
export const radarNameShort = radarName;
|
||||
|
||||
export const quadrants = [
|
||||
'languages-and-frameworks',
|
||||
'methods-and-patterns',
|
||||
'platforms-and-aoe-services',
|
||||
'tools',
|
||||
"languages-and-frameworks",
|
||||
"methods-and-patterns",
|
||||
"platforms-and-aoe-services",
|
||||
"tools",
|
||||
];
|
||||
|
||||
export const rings = [
|
||||
'all',
|
||||
'adopt',
|
||||
'trial',
|
||||
'assess',
|
||||
'hold'
|
||||
] as const;
|
||||
export const rings = ["all", "adopt", "trial", "assess", "hold"] as const;
|
||||
|
||||
export type Ring = typeof rings[number]
|
||||
export type Ring = typeof rings[number];
|
||||
|
||||
export const getItemPageNames = (items: Item[]) => items.map(item => `${item.quadrant}/${item.name}`);
|
||||
export const getItemPageNames = (items: Item[]) =>
|
||||
items.map((item) => `${item.quadrant}/${item.name}`);
|
||||
|
||||
export const showEmptyRings = false;
|
||||
|
||||
const messages: { [k: string]: string } = {
|
||||
'languages-and-frameworks': 'Languages & Frameworks',
|
||||
'methods-and-patterns': 'Methods & Patterns',
|
||||
'platforms-and-aoe-services': 'Platforms and Operations',
|
||||
'tools': 'Tools',
|
||||
"languages-and-frameworks": "Languages & Frameworks",
|
||||
"methods-and-patterns": "Methods & Patterns",
|
||||
"platforms-and-aoe-services": "Platforms and Operations",
|
||||
tools: "Tools",
|
||||
};
|
||||
|
||||
export const translate = (key: string) => (messages[key] || '-');
|
||||
export const translate = (key: string) => messages[key] || "-";
|
||||
|
||||
export function isMobileViewport() {
|
||||
// return false for server side rendering
|
||||
if (typeof window == 'undefined') return false;
|
||||
if (typeof window == "undefined") return false;
|
||||
|
||||
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
const width =
|
||||
window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
document.body.clientWidth;
|
||||
return width < 1200;
|
||||
}
|
||||
|
||||
export function assetUrl(file: string) {
|
||||
return process.env.PUBLIC_URL + '/' + file;
|
||||
// return `/techradar/assets/${file}`
|
||||
return process.env.PUBLIC_URL + "/" + file;
|
||||
}
|
||||
|
||||
@@ -58,4 +58,4 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@import './styles/main.scss'
|
||||
@import './styles/main.scss';
|
||||
11
src/index.tsx
Normal file
11
src/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./components/App";
|
||||
import "./index.scss";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
49
src/model.js
Normal file
49
src/model.js
Normal file
@@ -0,0 +1,49 @@
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
|
||||
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
|
||||
to[j] = from[i];
|
||||
return to;
|
||||
};
|
||||
export var featuredOnly = function (items) { return items.filter(function (item) { return item.featured; }); };
|
||||
export var groupByQuadrants = function (items) {
|
||||
return items.reduce(function (quadrants, item) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, quadrants), (_a = {}, _a[item.quadrant] = addItemToQuadrant(quadrants[item.quadrant], item), _a)));
|
||||
}, {});
|
||||
};
|
||||
export var groupByFirstLetter = function (items) {
|
||||
var index = items.reduce(function (letterIndex, item) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, letterIndex), (_a = {}, _a[getFirstLetter(item)] = addItemToList(letterIndex[getFirstLetter(item)], item), _a)));
|
||||
}, {});
|
||||
return Object.keys(index)
|
||||
.sort()
|
||||
.map(function (letter) { return ({
|
||||
letter: letter,
|
||||
items: index[letter],
|
||||
}); });
|
||||
};
|
||||
var addItemToQuadrant = function (quadrant, item) {
|
||||
var _a;
|
||||
if (quadrant === void 0) { quadrant = {}; }
|
||||
return (__assign(__assign({}, quadrant), (_a = {}, _a[item.ring] = addItemToRing(quadrant[item.ring], item), _a)));
|
||||
};
|
||||
var addItemToList = function (list, item) {
|
||||
if (list === void 0) { list = []; }
|
||||
return __spreadArray(__spreadArray([], list), [item]);
|
||||
};
|
||||
var addItemToRing = function (ring, item) {
|
||||
if (ring === void 0) { ring = []; }
|
||||
return __spreadArray(__spreadArray([], ring), [item]);
|
||||
};
|
||||
export var getFirstLetter = function (item) { return item.title.substr(0, 1).toUpperCase(); };
|
||||
67
src/react-app-env.d.ts
vendored
67
src/react-app-env.d.ts
vendored
@@ -1,66 +1 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly NODE_ENV: 'development' | 'production' | 'test';
|
||||
readonly PUBLIC_URL: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.bmp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
import * as React from 'react';
|
||||
|
||||
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||
SVGSVGElement
|
||||
> & { title?: string }>;
|
||||
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.sass' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
/// <reference types="react-scripts" />
|
||||
|
||||
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {createRadar} from "./radar";
|
||||
import {save} from "./file";
|
||||
import {copyFileSync, mkdir, mkdirSync} from "fs";
|
||||
import {quadrants} from "../src/config";
|
||||
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('starting static')
|
||||
const radar = await createRadar();
|
||||
|
||||
copyFileSync('build/index.html', 'build/overview.html')
|
||||
copyFileSync('build/index.html', 'build/help-and-about-tech-radar.html')
|
||||
|
||||
quadrants.forEach(quadrant => {
|
||||
copyFileSync('build/index.html', 'build/' + quadrant + '.html')
|
||||
mkdirSync('build/' + quadrant)
|
||||
})
|
||||
radar.items.forEach(item => {
|
||||
copyFileSync('build/index.html', 'build/' + item.quadrant + '/' + item.name + '.html')
|
||||
})
|
||||
|
||||
console.log('created static');
|
||||
} catch (e) {
|
||||
console.error('error:', e);
|
||||
}
|
||||
})()
|
||||
@@ -1,68 +0,0 @@
|
||||
import { outputFile } from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { walk } from 'walk';
|
||||
|
||||
export const relativePath = (...relativePath: string[]): string => (
|
||||
// path.resolve(__dirname, '..', ...relativePath)
|
||||
path.resolve(...relativePath)
|
||||
);
|
||||
|
||||
export const radarPath = (...pathInSrc: string[]) => (
|
||||
relativePath('radar', ...pathInSrc)
|
||||
);
|
||||
|
||||
export const stylesPath = (...pathInSrc: string[]) => (
|
||||
relativePath('styles', ...pathInSrc)
|
||||
);
|
||||
|
||||
export const assetsPath = (...pathInSrc: string[]) => (
|
||||
relativePath('assets', ...pathInSrc)
|
||||
);
|
||||
|
||||
export const faviconPath = (...pathInSrc: string[]) => (
|
||||
relativePath('assets/favicon.ico', ...pathInSrc)
|
||||
);
|
||||
|
||||
export const jsPath = (...pathInSrc: string[]) => (
|
||||
relativePath('js', ...pathInSrc)
|
||||
);
|
||||
|
||||
export const distPath = (...pathInDist: string[]) => (
|
||||
relativePath('src', ...pathInDist)
|
||||
);
|
||||
|
||||
export const getAllMarkdownFiles = (folder: string) => (
|
||||
getAllFiles(folder, isMarkdownFile)
|
||||
);
|
||||
|
||||
const getAllFiles = (folder: string, predicate: (s: string) => boolean): Promise<string[]> => (
|
||||
new Promise((resolve, reject) => {
|
||||
const walker = walk(folder, { followLinks: false });
|
||||
const files: string[] = [];
|
||||
|
||||
walker.on("file", (root, fileStat, next) => {
|
||||
if (predicate(fileStat.name)) {
|
||||
files.push(path.resolve(root, fileStat.name));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
walker.on("errors", (root, nodeStatsArray, next) => {
|
||||
nodeStatsArray.forEach(function (n) {
|
||||
console.error("[ERROR] " + n.name)
|
||||
if (n.error) {
|
||||
console.error(n.error.message || (n.error.code + ": " + n.error.path));
|
||||
}
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
walker.on("end", () => {
|
||||
resolve(files.sort());
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const isMarkdownFile = (name: string) => name.match(/\.md$/) !== null;
|
||||
|
||||
export const save = (data: string | Buffer | DataView, fileName: string) => outputFile(distPath(fileName), data);
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {createRadar} from "./radar";
|
||||
import {save} from "./file";
|
||||
import {copyFileSync, mkdir, mkdirSync} from "fs";
|
||||
import {quadrants} from "../src/config";
|
||||
|
||||
|
||||
export const radarJsonGenerator = (async () => {
|
||||
try {
|
||||
console.log('start')
|
||||
const radar = await createRadar();
|
||||
|
||||
// console.log(radar);
|
||||
|
||||
save(JSON.stringify(radar), 'rd.json')
|
||||
save(`import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from 'aoe_technology_radar/src/components/App';
|
||||
import 'aoe_technology_radar/src/index.scss';
|
||||
import {Item} from "aoe_technology_radar/src/model";
|
||||
import radardata from './rd.json';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App items={radardata.items as Item[]} releases={radardata.releases as string[]} />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
`, 'index.tsx')
|
||||
|
||||
console.log('Built radar');
|
||||
} catch (e) {
|
||||
console.error('error:', e);
|
||||
}
|
||||
})
|
||||
@@ -1,26 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "CommonJS",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"outDir": "bin",
|
||||
"resolveJsonModule": true
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"files": [
|
||||
"tasks/radarjson.ts",
|
||||
"tasks/create-static.ts"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
21
tsconfig.scripts.json
Normal file
21
tsconfig.scripts.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "dist_scripts",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["scripts"]
|
||||
}
|
||||
Reference in New Issue
Block a user