0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Migrated Portal from CRA to Vite

refs https://github.com/TryGhost/Ghost/issues/15502

- this commit migrates Portal from CRA to Vite, as it brings the
  package more inline with the direction we're going in terms of tooling
  for builds
- the bulk of the changes here are just config related to get things
  working with Vite, and then cleaning up all the CRA boilerplate
This commit is contained in:
Daniel Lockyer 2023-03-14 15:45:27 +01:00 committed by Daniel Lockyer
parent cdcb3dcd6f
commit 327fef0ad8
17 changed files with 1143 additions and 3255 deletions

View file

@ -7,7 +7,6 @@
"url": "git://github.com/TryGhost/Ghost.git"
},
"author": "Ghost Foundation",
"unpkg": "umd/portal.min.js",
"files": [
"umd/",
"LICENSE",
@ -17,35 +16,18 @@
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@sentry/react": "7.42.0",
"@sentry/tracing": "7.42.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.4.3",
"@tryghost/i18n": "0.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "5.0.1"
},
"scripts": {
"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",
"test": "react-scripts test",
"test:ci": "yarn test --watchAll=false --coverage",
"dev": "concurrently \"vite\" \"yarn build:watch\"",
"build": "vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"test": "vitest run",
"test:ci": "yarn test --coverage",
"test:unit": "yarn test:ci",
"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 version; fi",
"postship": "git push ${GHOST_UPSTREAM:-origin} --follow-tags && yarn publish . --tag $npm_package_version",
"posttest": "yarn lint",
"analyze": "source-map-explorer 'umd/*.js'",
"prepublishOnly": "yarn build"
},
"eslintConfig": {
@ -55,10 +37,7 @@
],
"plugins": [
"ghost"
],
"rules": {
"import/no-webpack-loader-syntax": "off"
}
]
},
"browserslist": {
"production": [
@ -77,24 +56,28 @@
"cobertura",
"text-summary",
"html"
],
"moduleNameMapper": {
"^!!raw-loader!.*": "jest-raw-loader"
}
]
},
"devDependencies": {
"chalk": "4.1.2",
"chokidar": "3.5.3",
"jest-raw-loader": "1.0.1",
"minimist": "1.2.8",
"ora": "5.4.1",
"raw-loader": "4.0.2",
"rewire": "6.0.0",
"serve-handler": "6.1.5",
"source-map-explorer": "2.5.3"
},
"resolutions": {
"//": "See https://github.com/facebook/create-react-app/issues/11773",
"react-error-overlay": "6.0.11"
"@babel/eslint-parser": "7.21.3",
"@sentry/react": "7.42.0",
"@sentry/tracing": "7.42.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@tryghost/i18n": "0.0.0",
"@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-c8": "0.29.3",
"@vitest/ui": "0.29.3",
"concurrently": "7.6.0",
"cross-fetch": "3.1.5",
"eslint": "8.36.0",
"eslint-config-react-app": "7.0.1",
"jsdom": "21.1.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"vite": "4.1.4",
"vite-plugin-css-injected-by-js": "3.1.0",
"vite-plugin-svgr": "2.4.0",
"vitest": "0.29.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -1,29 +0,0 @@
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/portal.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

@ -1,200 +0,0 @@
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 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 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}`));
watchFiles();
});
}
startDevServer();

View file

@ -1,8 +0,0 @@
/** 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

@ -1,14 +0,0 @@
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

@ -1,151 +0,0 @@
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

@ -14,6 +14,8 @@ import NotificationParser from './utils/notifications';
import {allowCompMemberUpgrade, createPopupNotification, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnlySite, isPaidMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {handleDataAttributes} from './data-attributes';
import i18nLib from '@tryghost/i18n';
const DEV_MODE_DATA = {
showPopup: true,
site: Fixtures.site,
@ -155,7 +157,7 @@ export default class App extends React.Component {
try {
// Fetch data from API, links, preview, dev sources
const {site, member, page, showPopup, popupNotification, lastPage, pageQuery, pageData} = await this.fetchData();
const i18n = require('@tryghost/i18n')(/*site.locale || */ 'en', 'portal'); // TODO: uncomment when you want to enable i18n translations
const i18n = i18nLib(/*site.locale || */ 'en', 'portal'); // TODO: uncomment when you want to enable i18n translations
const state = {
site,
member,
@ -507,7 +509,8 @@ export default class App extends React.Component {
return null;
}
const {portal_sentry: portalSentry, portal_version: portalVersion, version: ghostVersion} = site;
const appVersion = process.env.REACT_APP_VERSION || portalVersion;
// eslint-disable-next-line no-undef
const appVersion = REACT_APP_VERSION || portalVersion;
const releaseTag = `portal@${appVersion}|ghost@${ghostVersion}`;
if (portalSentry && portalSentry.dsn) {
Sentry.init({

View file

@ -7,7 +7,7 @@ import {GlobalStyles} from './Global.styles';
import {ActionButtonStyles} from './common/ActionButton';
import {BackButtonStyles} from './common/BackButton';
import {SwitchStyles} from './common/Switch';
import AccountHomePageStyles from '!!raw-loader!./pages/AccountHomePage/AccountHomePage.css';
import AccountHomePageStyles from './pages/AccountHomePage/AccountHomePage.css';
import {AccountPlanPageStyles} from './pages/AccountPlanPage';
import {InputFieldStyles} from './common/InputField';
import {SignupPageStyles} from './pages/SignupPage';
@ -17,9 +17,9 @@ import {MagicLinkStyles} from './pages/MagicLinkPage';
import {PopupNotificationStyles} from './common/PopupNotification';
import {OfferPageStyles} from './pages/OfferPage';
import {FeedbackPageStyles} from './pages/FeedbackPage';
import EmailSuppressedPage from '!!raw-loader!./pages/EmailSuppressedPage.css';
import EmailSuppressionFAQ from '!!raw-loader!./pages/EmailSuppressionFAQ.css';
import EmailReceivingFAQ from '!!raw-loader!./pages/EmailReceivingFAQ.css';
import EmailSuppressedPage from './pages/EmailSuppressedPage.css';
import EmailSuppressionFAQ from './pages/EmailSuppressionFAQ.css';
import EmailReceivingFAQ from './pages/EmailReceivingFAQ.css';
// Global styles
const FrameStyles = `

View file

@ -1,5 +1,20 @@
import {afterEach, expect} from 'vitest';
import {cleanup} from '@testing-library/react';
import {fetch} from 'cross-fetch';
import matchers from '@testing-library/jest-dom/matchers';
// TODO: remove this once we're switched `jest` to `vi` in code
// eslint-disable-next-line no-undef
globalThis.jest = vi;
// eslint-disable-next-line no-undef
globalThis.fetch = fetch;
// Add the cleanup function for React testing library
afterEach(cleanup);
// 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/extend-expect';
expect.extend(matchers);

View file

@ -0,0 +1,80 @@
import {resolve} from 'path';
import fs from 'fs/promises';
import {defineConfig} from 'vitest/config';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import reactPlugin from '@vitejs/plugin-react';
import svgrPlugin from 'vite-plugin-svgr';
import pkg from './package.json';
import {SUPPORTED_LOCALES} from '@tryghost/i18n';
export default defineConfig((config) => {
const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name;
return {
clearScreen: false,
define: {
'process.env.NODE_ENV': JSON.stringify(config.mode),
REACT_APP_VERSION: JSON.stringify(process.env.npm_package_version),
},
server: {
port: 5368,
},
plugins: [
cssInjectedByJsPlugin(),
reactPlugin(),
svgrPlugin(),
],
esbuild: {
loader: "jsx",
include: /src\/.*\.jsx?$/,
exclude: [],
},
optimizeDeps: {
esbuildOptions: {
plugins: [
{
name: "load-js-files-as-jsx",
setup(build) {
build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => ({
loader: "jsx",
contents: await fs.readFile(args.path, "utf8"),
}));
},
},
],
},
},
build: {
outDir: resolve(__dirname, 'umd'),
emptyOutDir: true,
minify: true,
sourcemap: true,
cssCodeSplit: false,
lib: {
entry: resolve(__dirname, 'src/index.js'),
formats: ['umd'],
name: pkg.name,
fileName: (format) => `${outputFileName}.min.js`,
},
rollupOptions: {
output: {
manualChunks: false,
}
},
commonjsOptions: {
include: [/ghost/, /node_modules/],
dynamicRequireRoot: '../',
dynamicRequireTargets: SUPPORTED_LOCALES.map((locale) => `../i18n/locales/${locale}/portal.json`),
}
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
testTimeout: 10000
}
};
});

3740
yarn.lock

File diff suppressed because it is too large Load diff