Proposal for switching desktop-client to vite (#2084)

* Proof of concept for switching desktop-client to vite

* Fix other packages ts tests issues

* Update jsx tests to use vitest instead of jest

* Inject our global shims properly

* Add comment regarding new plugin

* Cleanup unnessary change after rebase

* Fix inter fonts pathing

* Remove manual chunks sizes for now

Just set the limit higher

* Bring back size compare

* Suppress victory warnings

* Remove craco config now that it's not used

* Add vite basic ssl plugin

- This autogenerates self-signed certs in dev mode when HTTPS env is set
- Made to match the CRA behaviour

* Add release note

* Remove warning suppression for victory

- Updated to a rollup version that includes the fix
This commit is contained in:
DJ Mountney
2024-01-11 10:18:49 -08:00
committed by GitHub
parent 3eee0b11d2
commit d5359a96ca
16 changed files with 1285 additions and 5117 deletions

View File

@@ -44,6 +44,9 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: { project: [path.join(__dirname, './tsconfig.json')] },
reportUnusedDisableDirectives: true,
globals: {
globalThis: false,
},
rules: {
'prettier/prettier': 'warn',

View File

@@ -66,11 +66,11 @@ jobs:
run: |
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./head/*.json
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
- uses: github/webpack-bundlesize-compare-action@v1.8.2
- uses: twk3/rollup-size-compare-action@v1.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
current-stats-json-path: ./head/desktop-client-stats.json
base-stats-json-path: ./base/desktop-client-stats.json
current-stats-json-path: ./head/web-stats.json
base-stats-json-path: ./base/web-stats.json
title: desktop-client
- uses: github/webpack-bundlesize-compare-action@v1.8.2

View File

@@ -10,11 +10,13 @@ test-results
# production
build
build-stats
stats.json
# misc
.DS_Store
.env
npm-debug.log
.swc
*kcab.*
public/kcab

View File

@@ -8,7 +8,6 @@ echo "Building the browser..."
rm -fr build
export IS_GENERIC_BROWSER=1
export INLINE_RUNTIME_CHUNK=false
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
yarn build
@@ -16,4 +15,4 @@ yarn build
rm -fr build-stats
mkdir build-stats
mv build/kcab/stats.json build-stats/loot-core-stats.json
mv build/stats.json build-stats/desktop-client-stats.json
mv ./stats.json build-stats/web-stats.json

View File

@@ -1,116 +0,0 @@
const path = require('path');
const {
loaderByName,
removeLoaders,
addAfterLoader,
addPlugins,
} = require('@craco/craco');
const chokidar = require('chokidar');
const TerserPlugin = require('terser-webpack-plugin');
const { IgnorePlugin } = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
if (process.env.CI) {
process.env.DISABLE_ESLINT_PLUGIN = 'true';
}
// Forward Netlify env variables
if (process.env.REVIEW_ID) {
process.env.REACT_APP_REVIEW_ID = process.env.REVIEW_ID;
}
module.exports = {
webpack: {
configure: (webpackConfig, { env, paths }) => {
webpackConfig.mode =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
// swc-loader
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
test: /\.m?[tj]sx?$/,
exclude: /node_modules/,
loader: require.resolve('swc-loader'),
});
// remove the babel loaders
removeLoaders(webpackConfig, loaderByName('babel-loader'));
addPlugins(webpackConfig, [
new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true,
}),
// Pikaday throws a warning if Moment.js is not installed however it doesn't
// actually require it to be installed. As we don't use Moment.js ourselves
// then we can just silence this warning.
new IgnorePlugin({
contextRegExp: /pikaday$/,
resourceRegExp: /moment$/,
}),
]);
webpackConfig.resolve.extensions = [
'.web.js',
'.web.jsx',
'.web.ts',
'.web.tsx',
'.js',
'.jsx',
'.ts',
'.tsx',
...webpackConfig.resolve.extensions,
];
if (process.env.IS_GENERIC_BROWSER) {
webpackConfig.resolve.extensions = [
'.browser.js',
'.browser.jsx',
'.browser.ts',
'.browser.tsx',
...webpackConfig.resolve.extensions,
];
}
webpackConfig.optimization = {
...webpackConfig.optimization,
minimize:
process.env.CI === 'true' || process.env.NODE_ENV !== 'development',
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` options will be passed to `swc` (`@swc/core`)
// Link to options - https://swc.rs/docs/config-js-minify
terserOptions: {
compress: false,
mangle: true,
},
}),
],
};
return webpackConfig;
},
},
devServer: (devServerConfig, { env, paths, proxy, allowedHost }) => {
devServerConfig.onBeforeSetupMiddleware = server => {
chokidar
.watch([
path.resolve('../loot-core/lib-dist/*.js'),
path.resolve('../loot-core/lib-dist/browser/*.js'),
])
.on('all', function () {
for (const ws of server.webSocketServer.clients) {
ws.send(JSON.stringify({ type: 'static-changed' }));
}
});
};
devServerConfig.headers = {
...devServerConfig.headers,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
};
return devServerConfig;
},
};

View File

@@ -8,25 +8,25 @@
/>
<title>Actual</title>
<link rel="canonical" href="/" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="shortcut icon" href="/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-touch-icon.png"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" crossorigin="use-credentials"/>
<link rel="manifest" href="/site.webmanifest" crossorigin="use-credentials"/>
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
@@ -102,5 +102,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -6,8 +6,6 @@
"build"
],
"devDependencies": {
"@craco/craco": "^7.1.0",
"@craco/types": "^7.1.0",
"@juggle/resize-observer": "^3.1.2",
"@playwright/test": "^1.37.1",
"@reach/listbox": "^0.18.0",
@@ -16,9 +14,11 @@
"@react-aria/utils": "^3.19.0",
"@react-stately/collections": "^3.10.0",
"@react-stately/list": "^3.9.1",
"@rollup/plugin-inject": "^5.0.5",
"@svgr/cli": "^8.0.1",
"@swc/core": "^1.3.82",
"@swc/helpers": "^0.5.1",
"@swc/plugin-remove-console": "^1.5.105",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/react": "^18.2.0",
@@ -28,6 +28,8 @@
"@types/uuid": "^9.0.2",
"@types/webpack-bundle-analyzer": "^4.6.0",
"@use-gesture/react": "^10.3.0",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vitejs/plugin-react-swc": "^3.5.0",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
"date-fns": "^2.29.3",
@@ -41,7 +43,7 @@
"jest-watch-typeahead": "^2.2.2",
"mdast-util-newline-to-break": "^2.0.0",
"memoize-one": "^6.0.0",
"pikaday": "1.8.0",
"pikaday": "1.8.2",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
@@ -52,7 +54,6 @@
"react-modal": "3.16.1",
"react-redux": "7.2.1",
"react-router-dom": "6.11.2",
"react-scripts": "^5.0.1",
"react-simple-pull-to-refresh": "^1.3.3",
"react-spring": "^9.7.1",
"react-virtualized-auto-sizer": "^1.0.2",
@@ -60,22 +61,26 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"remark-gfm": "^3.0.1",
"rollup-plugin-visualizer": "^5.11.0",
"sass": "^1.63.6",
"swc-loader": "^0.2.3",
"terser-webpack-plugin": "^5.3.9",
"uuid": "^9.0.0",
"victory": "^36.6.8",
"vite": "^5.0.10",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4",
"webpack-bundle-analyzer": "^4.9.1",
"xml2js": "^0.6.2"
},
"scripts": {
"start": "cross-env PORT=3001 craco start",
"start": "cross-env PORT=3001 vite",
"start:browser": "cross-env ./bin/watch-browser",
"watch": "cross-env BROWSER=none yarn start",
"build": "cross-env INLINE_RUNTIME_CHUNK=false craco build",
"build": "vite build",
"build:browser": "cross-env ./bin/build-browser",
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
"test": "craco test",
"test": "vitest",
"e2e": "npx playwright test --browser=chromium",
"vrt": "cross-env VRT=true npx playwright test --browser=chromium"
}

View File

@@ -0,0 +1,14 @@
const global = globalThis || this || self;
const process = {
env: {
...import.meta.env,
NODE_ENV: import.meta.env.MODE,
PUBLIC_URL: import.meta.env.BASE_URL.slice(0, -1),
},
};
// eslint-disable-next-line import/no-unused-modules
export { process };
// eslint-disable-next-line import/no-unused-modules
export { global };

View File

@@ -366,7 +366,7 @@ const TransactionHeader = memo(
/>
{showBalance && <Cell value="Balance" width={88} textAlign="right" />}
{showCleared && <Field width={23} truncate={false} />}
<Cell value="" width={5 + scrollWidth ?? 0} />
<Cell value="" width={5 + (scrollWidth ?? 0)} />
</Row>
);
},

View File

@@ -25,8 +25,8 @@ import { ResponsiveProvider } from '../../ResponsiveProvider';
import { SplitsExpandedProvider, TransactionTable } from './TransactionsTable';
jest.mock('loot-core/src/platform/client/fetch');
jest.mock('../../hooks/useFeatureFlag', () => jest.fn().mockReturnValue(false));
vi.mock('loot-core/src/platform/client/fetch');
vi.mock('../../hooks/useFeatureFlag', () => vi.fn().mockReturnValue(false));
const accounts = [generateAccount('Bank of America')];
const payees = [

View File

@@ -1,2 +1,4 @@
@use '~inter-ui/variable';
@use 'inter-ui/variable' with (
$inter-font-path: '../../../node_modules/inter-ui/Inter (web)'
);
@include variable.default;

View File

@@ -7,11 +7,15 @@ installPolyfills();
global.IS_TESTING = true;
global.Actual = {};
jest.mock(
vi.mock(
'react-virtualized-auto-sizer',
() =>
({ children }) =>
children({ height: 1000, width: 600 }),
() => ({
default: (props) => {
return (
props.children({height: 1000, width: 600})
);
}
})
);
global.Date.now = () => 123456789;

View File

@@ -0,0 +1,165 @@
import * as path from 'path';
import inject from '@rollup/plugin-inject';
import basicSsl from '@vitejs/plugin-basic-ssl';
import react from '@vitejs/plugin-react-swc';
import { visualizer } from 'rollup-plugin-visualizer';
/// <reference types="vitest" />
import { defineConfig, loadEnv, Plugin } from 'vite';
import viteTsconfigPaths from 'vite-tsconfig-paths';
const addWatchers = (): Plugin => ({
name: 'add-watchers',
configureServer(server) {
server.watcher
.add([
path.resolve('../loot-core/lib-dist/*.js'),
path.resolve('../loot-core/lib-dist/browser/*.js'),
])
.on('all', function () {
for (const wsc of server.ws.clients) {
wsc.send(JSON.stringify({ type: 'static-changed' }));
}
});
},
});
// Inject build shims using the inject plugin
const injectShims = (): Plugin[] => {
const buildShims = path.resolve('./src/build-shims.js');
const commonInject = {
exclude: ['src/setupTests.jsx'],
global: [buildShims, 'global'],
};
return [
{
name: 'inject-build-process',
config: () => ({
// rename process.env in build mode so it doesn't get set to an empty object up by the vite:define plugin
// this isn't needed in serve mode, because vite:define doesn't empty it in serve mode. And defines also happen last anyways in serve mode.
define: {
'process.env': `_process.env`,
},
}),
apply: 'build',
},
{
...inject({
...commonInject,
process: [buildShims, 'process'],
}),
enforce: 'post',
apply: 'serve',
},
{
...inject({
...commonInject,
_process: [buildShims, 'process'],
}),
enforce: 'post',
apply: 'build',
},
];
};
// https://vitejs.dev/config/
// eslint-disable-next-line import/no-default-export
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const devHeaders = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
};
// Forward Netlify env variables
if (process.env.REVIEW_ID) {
process.env.REACT_APP_REVIEW_ID = process.env.REVIEW_ID;
}
let resolveExtensions = [
'.web.js',
'.web.jsx',
'.web.ts',
'.web.tsx',
'.mjs',
'.js',
'.mts',
'.ts',
'.jsx',
'.tsx',
'.json',
];
if (env.IS_GENERIC_BROWSER) {
resolveExtensions = [
'.browser.js',
'.browser.jsx',
'.browser.ts',
'.browser.tsx',
...resolveExtensions,
];
}
return {
base: '/',
envPrefix: 'REACT_APP_',
build: {
target: 'es2022',
sourcemap: true,
outDir: 'build',
assetsDir: 'static',
manifest: true,
assetsInlineLimit: 0,
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
assetFileNames: assetInfo => {
const info = assetInfo.name.split('.');
let extType = info[info.length - 1];
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
extType = 'img';
} else if (/woff|woff2/.test(extType)) {
extType = 'media';
}
return `static/${extType}/[name]-[hash][extname]`;
},
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
},
},
},
server: {
headers: mode === 'development' ? devHeaders : undefined,
port: +env.PORT || 5173,
open: env.BROWSER
? ['chrome', 'firefox', 'edge', 'browser', 'browserPrivate'].includes(
env.BROWSER,
)
: true,
watch: {
disableGlobbing: false,
},
},
resolve: {
extensions: resolveExtensions,
},
plugins: [
injectShims(),
addWatchers(),
react({
plugins: [['@swc/plugin-remove-console', {}]],
devTarget: 'es2022',
}),
viteTsconfigPaths({ root: '../..' }),
visualizer({ template: 'raw-data' }),
!!env.HTTPS && basicSsl(),
],
test: {
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.jsx',
},
};
});

View File

@@ -19,6 +19,7 @@
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"jsx": "preserve",
"types": ["vite/client", "jest"],
// Check JS files too
"allowJs": true,
"checkJs": false,

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [twk3]
---
Switch desktop-client to the Vite JS framework.

6034
yarn.lock

File diff suppressed because it is too large Load Diff