Converted CRA packages to Vite

refs https://github.com/TryGhost/DevOps/issues/50

- we're moving away from CRA in favor of Vite, so this switches the last
  packages over
This commit is contained in:
Daniel Lockyer 2023-07-27 09:09:01 +02:00 committed by Daniel Lockyer
parent 145d0b84f5
commit 5875962392
42 changed files with 557 additions and 3883 deletions

View File

@ -108,7 +108,7 @@ if (DASH_DASH_ARGS.includes('announcement-bar') || DASH_DASH_ARGS.includes('anno
prefixColor: '#DC9D00',
env: {}
});
COMMAND_GHOST.env['announcementBar__url'] = 'http://localhost:5371/announcement-bar';
COMMAND_GHOST.env['announcementBar__url'] = 'http://localhost:4177/announcement-bar.min.js';
}
if (DASH_DASH_ARGS.includes('search') || DASH_DASH_ARGS.includes('all')) {
@ -119,8 +119,8 @@ if (DASH_DASH_ARGS.includes('search') || DASH_DASH_ARGS.includes('all')) {
prefixColor: '#23de43',
env: {}
});
COMMAND_GHOST.env['sodoSearch__url'] = 'http://localhost:5370/umd/sodo-search.min.js';
COMMAND_GHOST.env['sodoSearch__styles'] = 'http://localhost:5370/umd/main.css';
COMMAND_GHOST.env['sodoSearch__url'] = 'http://localhost:4178/sodo-search.min.js';
COMMAND_GHOST.env['sodoSearch__styles'] = 'http://localhost:4178/main.css';
}
if (DASH_DASH_ARGS.includes('lexical')) {

View File

@ -70,7 +70,6 @@
"@vitejs/plugin-react": "4.0.3",
"autoprefixer": "10.4.14",
"concurrently": "8.2.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"eslint-plugin-tailwindcss": "3.13.0",

View File

@ -1,82 +1,15 @@
# Announcement Bar
# Getting Started with Create React App
## Development
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
### Pre-requisites
## Basic Setup
- Run `yarn` in Ghost monorepo root
- Run `yarn` in this directory
This section is mostly relevant for core team only for active development.
Run `yarn dev` with `--announcement-bar` to start Ghost with a development version of Announcement Bar
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
### Running via Ghost `yarn dev` in root folder
You can automatically start the announcement-bar dev server when developing Ghost by running Ghost (in root folder) via `yarn dev --announcementbar`.
# Copyright & License

View File

@ -17,32 +17,22 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.4.3",
"@tryghost/content-api": "1.11.7",
"flexsearch": "0.7.31",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "5.0.1"
"react-dom": "17.0.2"
},
"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",
"build:bundle": "webpack --config webpack.config.js",
"test:ui": "react-scripts test",
"test:unit": "yarn test",
"test": "yarn test:ui --watchAll=false --coverage",
"eject": "react-scripts eject",
"dev": "concurrently \"vite preview -l silent\" \"yarn build:watch\"",
"build": "vite build",
"build:watch": "vite build --watch",
"test": "vitest run",
"test:ci": "yarn test --coverage",
"test:unit": "yarn test:ci",
"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",
"postship": "git push ${GHOST_UPSTREAM:-origin} --follow-tags && npm publish",
"prepublishOnly": "yarn build"
},
"eslintConfig": {
@ -75,19 +65,11 @@
]
},
"devDependencies": {
"autoprefixer": "10.4.14",
"chalk": "4.1.2",
"chokidar": "3.5.3",
"copy-webpack-plugin": "11.0.0",
"minimist": "1.2.8",
"nock": "13.3.1",
"postcss": "8.4.27",
"rewire": "6.0.0",
"serve-handler": "6.1.5",
"webpack-cli": "5.1.4"
},
"resolutions": {
"//": "See https://github.com/facebook/create-react-app/issues/11773",
"react-error-overlay": "6.0.11"
"@vitejs/plugin-react": "4.0.3",
"eslint-plugin-jest": "27.2.3",
"eslint-plugin-react": "7.33.0",
"vite": "4.4.7",
"vite-plugin-svgr": "3.2.0",
"vitest": "0.33.0"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<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>

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/announcement-bar.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,167 +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');
/* 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 = 5371, 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 onBuildComplete(code) {
buildProcess = null;
printBuildComplete(code);
stdErrChunks = [];
stdOutChunks = [];
if (fileChanges.length > 0) {
buildPortal();
}
}
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();
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: '/announcement-bar', destination: 'umd/announcement-bar.min.js'},
{source: '/announcement-bar.min.js.map', destination: 'umd/announcement-bar.min.js.map'}
],
headers: [
{
source: '**',
headers: [{
key: 'Cache-Control',
value: 'no-cache'
},{
key: 'Access-Control-Allow-Origin',
value: '*'
}]
}
]
});
});
server.listen(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 = 5370} = 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 = {
announcementBar: {
url: `http://localhost:${port}/announcement-bar`
}
};
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 announcementBar 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: '/announcement-bar', 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

@ -18,7 +18,6 @@ export function Main({apiUrl}) {
getSiteSettings();
// We only do this for init
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (

View File

@ -1,5 +1,8 @@
// 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';
import {fetch} from 'cross-fetch';
// 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;

View File

@ -24,6 +24,6 @@ test('should call settings endpoint on init', () => {
headers: {'Content-Type': 'application/json'},
credentials: undefined,
body: undefined
}),
})
);
});

View File

@ -0,0 +1,65 @@
import {resolve} from 'path';
import fs from 'fs/promises';
import {defineConfig} from 'vitest/config';
import reactPlugin from '@vitejs/plugin-react';
import svgrPlugin from 'vite-plugin-svgr';
import pkg from './package.json';
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)
},
preview: {
port: 4177
},
plugins: [
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: true,
lib: {
entry: resolve(__dirname, 'src/index.js'),
formats: ['umd'],
name: pkg.name,
fileName: format => `${outputFileName}.min.js`
}
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
testTimeout: 10000
}
};
});

View File

@ -1,34 +0,0 @@
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: 'announcement-bar.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/announcement-bar.min.js.map'}
]
})
],
performance: {
hints: false,
maxEntrypointSize: 560,
maxAssetSize: 5600
}
};

View File

@ -55,8 +55,6 @@ Creates the production single minified bundle for external use in `umd/portal.mi
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
## Publish
@ -68,8 +66,3 @@ Run `yarn ship` to publish new version of script.
- Publishes package on npm as `@tryghost/portal` and creates an unpkg link for script at https://unpkg.com/@tryghost/portal@VERSION
(Core team only)
## Learn More
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

View File

@ -73,7 +73,6 @@
"@vitest/ui": "0.33.0",
"concurrently": "8.2.0",
"cross-fetch": "4.0.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-i18next": "6.0.3",
"jsdom": "22.1.0",
"react": "17.0.2",

View File

@ -54,7 +54,6 @@
"@vitejs/plugin-react": "4.0.3",
"autoprefixer": "10.4.14",
"concurrently": "8.2.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"eslint-plugin-tailwindcss": "3.13.0",

View File

@ -1,77 +1,16 @@
# Sodo Search
# Getting Started with Create React App
## Development
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
### Pre-requisites
## Available Scripts
- Run `yarn` in Ghost monorepo root
- Run `yarn` in this directory
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
### Running via Ghost `yarn dev` in root folder
You can automatically start the Sodo-Search dev server when developing Ghost by running Ghost (in root folder) via `yarn dev --search`.
# Copyright & License
Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE).
Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE).

View File

@ -17,34 +17,24 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.4.3",
"@tryghost/content-api": "1.11.7",
"flexsearch": "0.7.31",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "5.0.1"
"react-dom": "17.0.2"
},
"scripts": {
"start": "BROWSER=none react-scripts start",
"start:combined": "BROWSER=none node ./scripts/start-combined.js",
"start:dev": "node ./scripts/start-mode.js",
"dev": "concurrently \"vite preview -l silent\" \"yarn build:watch\" \"yarn tailwind\"",
"build": "vite build && yarn tailwind:base",
"build:watch": "vite build --watch",
"tailwind": "yarn tailwind:base --watch ",
"tailwind:base": "npx tailwindcss -i ./src/index.css -o ./umd/main.css --minify",
"dev": "concurrently \"node ./scripts/dev-mode.js\" \"yarn tailwind\"",
"build": "npm run build:combined && npm run tailwind:base",
"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:unit": "yarn test",
"test": "yarn test:ui --watchAll=false --coverage",
"eject": "react-scripts eject",
"test": "vitest run",
"test:ci": "yarn test --coverage",
"test:unit": "yarn test:ci",
"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",
"postship": "git push ${GHOST_UPSTREAM:-origin} --follow-tags && npm publish",
"prepublishOnly": "yarn build"
},
"eslintConfig": {
@ -77,21 +67,12 @@
]
},
"devDependencies": {
"autoprefixer": "10.4.14",
"chalk": "4.1.2",
"chokidar": "3.5.3",
"copy-webpack-plugin": "11.0.0",
"minimist": "1.2.8",
"nock": "13.3.1",
"ora": "5.4.1",
"postcss": "8.4.27",
"rewire": "6.0.0",
"serve-handler": "6.1.5",
"tailwindcss": "3.3.3",
"webpack-cli": "5.1.4"
},
"resolutions": {
"//": "See https://github.com/facebook/create-react-app/issues/11773",
"react-error-overlay": "6.0.11"
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@vitejs/plugin-react": "4.0.3",
"nock": "13.1.2",
"vite": "4.4.7",
"vite-plugin-svgr": "3.2.0",
"vitest": "0.33.0"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 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" />
<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/sodo-search.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,179 +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 = 5370, 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 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();
}
}
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: '/sodo-search', destination: 'umd/sodo-search.min.js'},
{source: '/sodo-search.min.js.map', destination: 'umd/sodo-search.min.js.map'}
],
headers: [
{
source: '**',
headers: [{
key: 'Cache-Control',
value: 'no-cache'
},{
key: 'Access-Control-Allow-Origin',
value: '*'
}]
}
]
});
});
server.listen(port, () => {
log(chalk.whiteBright(`Sodo-Search 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,181 +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 tailwindServerProcess;
let stdOutChunks = [];
let stdErrChunks = [];
let startYarnOutput = false;
const {v, verbose, port = 5370} = 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}/sodo-search`
}
};
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;
tailwindServerProcess = 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 doTailwindServerStart() {
if (tailwindServerProcess) {
return;
}
const options = getBuildOptions();
tailwindServerProcess = spawn('yarn tailwind', options);
['SIGINT', 'SIGTERM'].forEach(function (sig) {
tailwindServerProcess.on(sig, function () {
tailwindServerProcess && tailwindServerProcess.exit();
});
});
tailwindServerProcess.on('close', onProcessClose);
if (!showVerbose) {
tailwindServerProcess.stdout.on('data', (data) => {
stdOutChunks.push(data);
printYarnProcessOutput(data);
});
tailwindServerProcess.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: '/sodo-search', 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();
doTailwindServerStart();
});
}
clearConsole({withHistory: false});
startDevServer();

View File

@ -1,10 +1,25 @@
import {render} from '@testing-library/react';
import App from './App';
import React from 'react';
import nock from 'nock';
test('renders Sodo Search app component', () => {
nock('http://localhost:3000/ghost/api/content')
.get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC')
.reply(200, {
posts: []
})
.get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC')
.reply(200, {
authors: []
})
.get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic')
.reply(200, {
tags: []
});
window.location.hash = '#/search';
render(<App adminUrl="http://localhost" apiKey="69010382388f9de5869ad6e558" />);
render(<App adminUrl="http://localhost:3000" apiKey="69010382388f9de5869ad6e558" />);
// const containerElement = screen.getElementsByClassName('gh-portal-popup-container');
// eslint-disable-next-line testing-library/no-node-access
const containerElement = document.querySelector('.gh-root-frame');

View File

@ -1,4 +1,4 @@
import {Document} from 'flexsearch';
import Flexsearch from 'flexsearch';
import GhostContentAPI from '@tryghost/content-api';
export default class SearchIndex {
@ -9,7 +9,7 @@ export default class SearchIndex {
version: 'v5.0'
});
this.postsIndex = new Document({
this.postsIndex = new Flexsearch.Document({
tokenize: 'forward',
document: {
id: 'id',
@ -17,7 +17,7 @@ export default class SearchIndex {
store: true
}
});
this.authorsIndex = new Document({
this.authorsIndex = new Flexsearch.Document({
tokenize: 'forward',
document: {
id: 'id',
@ -25,7 +25,7 @@ export default class SearchIndex {
store: true
}
});
this.tagsIndex = new Document({
this.tagsIndex = new Flexsearch.Document({
tokenize: 'forward',
document: {
id: 'id',

View File

@ -3,11 +3,11 @@ import nock from 'nock';
describe('search index', function () {
test('initializes search index', async () => {
const adminUrl = 'http://localhost';
const adminUrl = 'http://localhost:3000';
const apiKey = '69010382388f9de5869ad6e558';
const searchIndex = new SearchIndex({adminUrl, apiKey, storage: localStorage});
const scope = nock('http://localhost/ghost/api/content')
const scope = nock('http://localhost:3000/ghost/api/content')
.get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC')
.reply(200, {
posts: []
@ -33,11 +33,11 @@ describe('search index', function () {
});
test('allows to search for indexed posts and authors', async () => {
const adminUrl = 'http://localhost';
const adminUrl = 'http://localhost:3000';
const apiKey = '69010382388f9de5869ad6e558';
const searchIndex = new SearchIndex({adminUrl, apiKey, storage: localStorage});
nock('http://localhost/ghost/api/content')
nock('http://localhost:3000/ghost/api/content')
.get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC')
.reply(200, {
posts: [{

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';
expect.extend(matchers);

View File

@ -0,0 +1,71 @@
import {resolve} from 'path';
import fs from 'fs/promises';
import {defineConfig} from 'vitest/config';
import commonjs from 'vite-plugin-commonjs';
import reactPlugin from '@vitejs/plugin-react';
import svgrPlugin from 'vite-plugin-svgr';
import pkg from './package.json';
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)
},
preview: {
port: 4178
},
plugins: [
reactPlugin(),
svgrPlugin(),
commonjs({
dynamic: {
loose: true
}
})
],
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: true,
lib: {
entry: resolve(__dirname, 'src/index.js'),
formats: ['umd'],
name: pkg.name,
fileName: format => `${outputFileName}.min.js`
}
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
testTimeout: 10000
}
};
});

View File

@ -1,34 +0,0 @@
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: 'sodo-search.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/sodo-search.min.js.map'}
]
})
],
performance: {
hints: false,
maxEntrypointSize: 560,
maxAssetSize: 5600
}
};

View File

@ -122,6 +122,7 @@
"devDependencies": {
"concurrently": "8.2.0",
"eslint": "8.44.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-ghost": "3.3.0",
"husky": "8.0.3",
"lint-staged": "13.2.3",

3015
yarn.lock

File diff suppressed because it is too large Load Diff