Added custom build setup

refs https://github.com/TryGhost/Team/issues/1664

- overrides webpack config to generate single script for publishing
- adds custom scripts for dev and production mode
This commit is contained in:
Rishabh 2022-07-04 15:15:01 +02:00
parent 59865f1e0f
commit cc4eda6478
13 changed files with 6557 additions and 2903 deletions

1
apps/comments-ui/.env Normal file
View File

@ -0,0 +1 @@
REACT_APP_VERSION=$npm_package_version

View File

@ -0,0 +1,12 @@
name: Test
on:
pull_request:
push:
branches:
- main
- 'renovate/*'
jobs:
test:
uses: tryghost/actions/.github/workflows/test.yml@main
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@ -69,3 +69,16 @@ typings/
.DS_Store
# Comments Ui Custom
# dotenv environment variables file
.env.*
# Membersjs build folders
umd/
build/
# Allow .env file
!.env
## We use .env file to define NODE_PATH as recommended test-utils setup pattern to avoid relative imports.
# Refs: https://testing-library.com/docs/react-testing-library/setup#jest-and-create-react-app
# CRA also suggests `.env` files should be checked into source control
# Ref: https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env

View File

@ -5,24 +5,40 @@
"repository": "git@github.com:TryGhost/comments-ui.git",
"author": "Ghost Foundation",
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.0"
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.2",
"@testing-library/user-event": "14.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "4.0.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "BROWSER=none react-scripts start",
"start:combined": "BROWSER=none node ./scripts/start-combined.js",
"start:dev": "node ./scripts/start-mode.js",
"dev": "node ./scripts/dev-mode.js",
"build": "npm run build:combined",
"build:original": "react-scripts build",
"build:combined": "node ./scripts/build-combined.js",
"build:bundle": "webpack --config webpack.config.js",
"test:ui": "react-scripts test",
"test": "yarn test --watchAll=false --coverage",
"eject": "react-scripts eject",
"lint": "eslint src --ext .js --cache",
"preship": "yarn lint",
"ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn publish && git push ${GHOST_UPSTREAM:-upstream} main --follow-tags; fi",
"posttest": "yarn lint",
"analyze": "source-map-explorer 'umd/*.js'",
"prepublishOnly": "yarn build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"plugin:ghost/browser"
],
"plugins": [
"ghost"
]
},
"browserslist": {
@ -36,5 +52,21 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"chalk": "4.1.2",
"chokidar": "3.5.2",
"copy-webpack-plugin": "6.4.1",
"eslint-plugin-ghost": "2.12.0",
"minimist": "1.2.5",
"ora": "5.4.1",
"rewire": "6.0.0",
"serve-handler": "6.1.3",
"source-map-explorer": "2.5.2",
"webpack-cli": "3.3.12"
},
"resolutions": {
"//": "See https://github.com/facebook/create-react-app/issues/11773",
"react-error-overlay": "6.0.9"
}
}

View File

@ -0,0 +1,29 @@
const rewire = require('rewire');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const defaults = rewire('react-scripts/scripts/build.js');
let config = defaults.__get__('config');
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
// JS: Save built file in `/umd`
config.output.filename = '../umd/comments-ui.min.js';
// CSS: Remove MiniCssPlugin from list of plugins
config.plugins = config.plugins.filter(plugin => !(plugin instanceof MiniCssExtractPlugin));
// CSS: replaces all MiniCssExtractPlugin.loader with style-loader to embed CSS in JS
config.module.rules[1].oneOf = config.module.rules[1].oneOf.map((rule) => {
if (!Object.prototype.hasOwnProperty.call(rule, 'use')) {
return rule;
}
return Object.assign({}, rule, {
use: rule.use.map(options => (/mini-css-extract-plugin/.test(options.loader)
? {loader: require.resolve('style-loader'), options: {}}
: options))
});
});

View File

@ -0,0 +1,220 @@
const handler = require('serve-handler');
const http = require('http');
const chokidar = require('chokidar');
const chalk = require('chalk');
const {spawn} = require('child_process');
const minimist = require('minimist');
const ora = require('ora');
/* eslint-disable no-console */
const log = console.log;
/* eslint-enable no-console */
let buildProcess;
let fileChanges = [];
let spinner;
let stdOutChunks = [];
let stdErrChunks = [];
const {v, verbose, port = 5368, basic, b} = minimist(process.argv.slice(2));
const showVerbose = !!(v || verbose);
const showBasic = !!(b || basic);
function clearConsole({withHistory = true} = {}) {
if (!withHistory) {
process.stdout.write('\x1Bc');
return;
}
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
}
function maybePluralize(count, noun, suffix = 's') {
return `${count} ${noun}${count !== 1 ? suffix : ''}`;
}
function printFileChanges() {
if (fileChanges.length > 0) {
const prefix = maybePluralize(fileChanges.length, 'file');
log(chalk.bold.hex('#ffa300').underline(`${prefix} changed`));
const message = fileChanges.map((path) => {
return chalk.hex('#ffa300').dim(`${path}`);
}).join('\n');
log(message);
log();
}
}
function printBuildSuccessDetails() {
if (showBasic) {
return;
}
if ((stdOutChunks && stdOutChunks.length > 0)) {
const detail = Buffer.concat(stdOutChunks.slice(4,7)).toString();
log();
log(chalk.dim(detail));
}
}
function printBuildErrorDetails() {
if ((stdOutChunks && stdOutChunks.length > 0)) {
const failDetails = Buffer.concat(stdOutChunks.slice(4, stdOutChunks.length - 1)).toString().replace(/^(?=\n)$|\s*$|\n\n+/gm, '');
log(chalk(failDetails));
}
if (stdErrChunks && stdErrChunks.length > 0) {
const stderrContent = Buffer.concat(stdErrChunks).toString();
log(chalk.dim(stderrContent));
}
}
function printBuildComplete(code) {
if (code === 0) {
if (!showVerbose) {
spinner && spinner.succeed(chalk.greenBright.bold('Build finished'));
printBuildSuccessDetails();
} else {
log();
log(chalk.bold.greenBright.bgBlackBright(`${'-'.repeat(25)}Build Success${'-'.repeat(25)}`));
}
} else {
if (!showVerbose) {
spinner && spinner.fail(chalk.redBright.bold('Build failed'));
printBuildErrorDetails();
} else {
log(chalk.bold.redBright.bgBlackBright(`${'-'.repeat(25)}Build finished: Failed${'-'.repeat(25)}`));
}
}
log();
}
function printConfigInstruction() {
const data = {
portal: {
url: `http://localhost:${port}/portal`
}
};
const stringifedData = JSON.stringify(data, null, 2);
const splitData = stringifedData.split('\n');
log();
splitData.forEach((_data, idx, arr) => {
if (idx === 0 || idx === arr.length - 1) {
log(chalk.grey(_data));
} else {
log(chalk.bold.whiteBright(_data));
}
});
log();
}
function printInstructions() {
log();
log(chalk.yellowBright.underline(`Add portal to your local Ghost config`));
printConfigInstruction();
log(chalk.cyanBright('='.repeat(50)));
log();
}
function printBuildStart() {
if (showVerbose) {
log(chalk.bold.greenBright.bgBlackBright(`${'-'.repeat(32)}Building${'-'.repeat(32)}`));
log();
} else {
spinner = ora(chalk.magentaBright.bold('Bundling files, hang on...')).start();
}
}
function onBuildComplete(code) {
buildProcess = null;
printBuildComplete(code);
stdErrChunks = [];
stdOutChunks = [];
if (fileChanges.length > 0) {
buildPortal();
} else {
log(chalk.yellowBright.bold.underline(`Watching file changes...\n`));
}
}
function getBuildOptions() {
process.env.FORCE_COLOR = 'true';
const options = {
shell: true,
env: process.env
};
if (showVerbose) {
options.stdio = 'inherit';
}
return options;
}
function buildPortal() {
if (buildProcess) {
return;
}
printFileChanges();
printBuildStart();
fileChanges = [];
const options = getBuildOptions();
buildProcess = spawn('yarn build', options);
buildProcess.on('close', onBuildComplete);
if (!showVerbose) {
buildProcess.stdout.on('data', (data) => {
stdOutChunks.push(data);
});
buildProcess.stderr.on('data', (data) => {
stdErrChunks.push(data);
});
}
}
function watchFiles() {
const watcher = chokidar.watch('.', {
ignored: /build|node_modules|.git|public|umd|scripts|(^|[\/\\])\../
});
watcher.on('ready', () => {
buildPortal();
}).on('change', (path) => {
if (!fileChanges.includes(path)) {
fileChanges.push(path);
}
if (!buildProcess) {
buildPortal();
}
});
}
function startDevServer() {
const server = http.createServer((request, response) => {
return handler(request, response, {
rewrites: [
{source: '/portal', destination: 'umd/portal.min.js'},
{source: '/portal.min.js.map', destination: 'umd/portal.min.js.map'}
],
headers: [
{
source: '**',
headers: [{
key: 'Cache-Control',
value: 'no-cache'
},{
key: 'Access-Control-Allow-Origin',
value: '*'
}]
}
]
});
});
server.listen(port, () => {
log(chalk.whiteBright(`Portal dev server is running on http://localhost:${port}`));
printInstructions();
watchFiles();
});
}
clearConsole({withHistory: false});
startDevServer();

View File

@ -0,0 +1,8 @@
/** Script to load Portal bundle for local development */
function loadScript(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
}
loadScript('http://localhost:3000/static/js/bundle.js');

View File

@ -0,0 +1,14 @@
const rewire = require('rewire');
const defaults = rewire('react-scripts/scripts/start.js');
let configFactory = defaults.__get__('configFactory');
defaults.__set__('configFactory', (env) => {
const config = configFactory(env);
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
return config;
});

View File

@ -0,0 +1,151 @@
const handler = require('serve-handler');
const http = require('http');
const chalk = require('chalk');
const {spawn} = require('child_process');
const minimist = require('minimist');
/* eslint-disable no-console */
const log = console.log;
/* eslint-enable no-console */
let yarnStartProcess;
let stdOutChunks = [];
let stdErrChunks = [];
let startYarnOutput = false;
const {v, verbose, port = 5368} = minimist(process.argv.slice(2));
const showVerbose = !!(v || verbose);
function clearConsole({withHistory = true} = {}) {
if (!withHistory) {
process.stdout.write('\x1Bc');
return;
}
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
}
function printConfigInstruction() {
const data = {
portal: {
url: `http://localhost:${port}/portal`
}
};
const stringifedData = JSON.stringify(data, null, 2);
const splitData = stringifedData.split('\n');
log();
splitData.forEach((data, idx, arr) => {
if (idx === 0 || idx === arr.length - 1) {
log(chalk.grey(data));
} else {
log(chalk.bold.whiteBright(data));
}
});
log();
}
function printInstructions() {
log();
log(chalk.yellowBright.underline(`Add portal to your local Ghost config`));
printConfigInstruction();
log(chalk.cyanBright('='.repeat(50)));
log();
}
function onProcessClose(code) {
yarnStartProcess = null;
stdErrChunks = [];
stdOutChunks = [];
log(chalk.redBright.bold.underline(`Please restart the script...\n`));
}
function getBuildOptions() {
process.env.FORCE_COLOR = 'true';
const options = {
shell: true,
env: process.env
};
if (showVerbose) {
options.stdio = 'inherit';
}
return options;
}
function doYarnStart() {
if (yarnStartProcess) {
return;
}
const options = getBuildOptions();
yarnStartProcess = spawn('yarn start:combined', options);
['SIGINT', 'SIGTERM'].forEach(function (sig) {
yarnStartProcess.on(sig, function () {
yarnStartProcess && yarnStartProcess.exit();
});
});
yarnStartProcess.on('close', onProcessClose);
if (!showVerbose) {
yarnStartProcess.stdout.on('data', (data) => {
stdOutChunks.push(data);
printYarnProcessOutput(data);
});
yarnStartProcess.stderr.on('data', (data) => {
log(Buffer.from(data).toString());
stdErrChunks.push(data);
});
}
}
function printYarnProcessOutput(data) {
const dataStr = Buffer.from(data).toString();
const dataArr = dataStr.split('\n').filter((d) => {
return /\S/.test(d.trim());
});
if (dataArr.find(d => d.includes('Starting the development'))) {
startYarnOutput = true;
log(chalk.yellowBright('Starting the development server...\n'));
return;
}
dataArr.forEach((dataOut) => {
if (startYarnOutput) {
log(dataOut);
}
});
if (startYarnOutput) {
log();
}
}
function startDevServer() {
const server = http.createServer((request, response) => {
return handler(request, response, {
rewrites: [
{source: '/portal', destination: 'scripts/load-portal.js'}
],
headers: [
{
source: '**',
headers: [{
key: 'Cache-Control',
value: 'no-cache'
},{
key: 'Access-Control-Allow-Origin',
value: '*'
}]
}
]
});
});
server.listen(port, () => {
log(chalk.whiteBright(`Portal dev server is running on http://localhost:${port}`));
printInstructions();
doYarnStart();
});
}
clearConsole({withHistory: false});
startDevServer();

View File

@ -1,23 +1,9 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
Hello world
</div>
);
}

View File

@ -1,17 +1,54 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
const ROOT_DIV_ID = 'ghost-comments-root';
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
function addRootDiv() {
const elem = document.createElement('div');
elem.id = ROOT_DIV_ID;
document.body.appendChild(elem);
}
function getSiteData() {
/**
* @type {HTMLElement}
*/
const scriptTag = document.querySelector('script[data-ghost]');
if (scriptTag) {
const siteUrl = scriptTag.dataset.ghost;
const apiKey = scriptTag.dataset.key;
const apiUrl = scriptTag.dataset.api;
return {siteUrl, apiKey, apiUrl};
}
return {};
}
function handleTokenUrl() {
const url = new URL(window.location.href);
if (url.searchParams.get('token')) {
url.searchParams.delete('token');
window.history.replaceState({}, document.title, url.href);
}
}
function setup({siteUrl}) {
addRootDiv();
handleTokenUrl();
}
function init() {
// const customSiteUrl = getSiteUrl();
const {siteUrl: customSiteUrl} = getSiteData();
const siteUrl = customSiteUrl || window.location.origin;
setup({siteUrl});
ReactDOM.render(
<React.StrictMode>
Hello World
{/* <App siteUrl={siteUrl} customSiteUrl={customSiteUrl} apiKey={apiKey} apiUrl={apiUrl} /> */}
</React.StrictMode>,
document.getElementById(ROOT_DIV_ID)
);
}
init();

View File

@ -0,0 +1,34 @@
const path = require('path');
const glob = require('glob');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
'bundle.js': glob.sync('build/static/?(js|css)/main.*.?(js|css)').map(f => path.resolve(__dirname, f))
},
output: {
filename: 'comments-ui.min.js',
path: __dirname + '/umd'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new CopyPlugin({
patterns: [
{from: './build/static/js/main.js.map', to: './umd/comments-ui.min.js.map'}
]
})
],
performance: {
hints: false,
maxEntrypointSize: 560,
maxAssetSize: 5600
}
};

File diff suppressed because it is too large Load Diff