From 2117eb4f317519e0d1fb05038a87dd2ebf2d6526 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 14:31:31 -0300 Subject: [PATCH] chore: Improve bundle size (#5761) --- .gitignore | 3 + .prettierignore | 3 + site/.eslintignore | 3 + site/.prettierignore | 3 + site/jest.setup.ts | 23 ---- site/package.json | 3 + site/src/components/IconField/IconField.tsx | 9 +- .../components/IconField/LazyIconField.tsx | 12 +++ site/src/components/IconField/types.ts | 5 + .../CreateTemplatePage/CreateTemplateForm.tsx | 4 +- .../GroupsPage/SettingsGroupPageView.tsx | 89 ++------------- .../TemplateSettingsForm.tsx | 101 ++---------------- .../TemplateSettingsPage.test.tsx | 2 +- site/src/testHelpers/entities.ts | 2 +- site/vite.config.ts | 15 ++- site/yarn.lock | 16 ++- 16 files changed, 84 insertions(+), 209 deletions(-) create mode 100644 site/src/components/IconField/LazyIconField.tsx create mode 100644 site/src/components/IconField/types.ts diff --git a/.gitignore b/.gitignore index 5580ff0a66..5e47330833 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ cli/testdata/.gen-golden /dist/ site/out/ +# Bundle analysis +site/stats/ + *.tfstate *.tfstate.backup *.tfplan diff --git a/.prettierignore b/.prettierignore index 8d8a892167..de248ccdd1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -39,6 +39,9 @@ cli/testdata/.gen-golden /dist/ site/out/ +# Bundle analysis +site/stats/ + *.tfstate *.tfstate.backup *.tfplan diff --git a/site/.eslintignore b/site/.eslintignore index e5f5eba856..e699b562e7 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -39,6 +39,9 @@ playwright-report/* ../dist/ out/ +# Bundle analysis +stats/ + *.tfstate *.tfstate.backup *.tfplan diff --git a/site/.prettierignore b/site/.prettierignore index e5f5eba856..e699b562e7 100644 --- a/site/.prettierignore +++ b/site/.prettierignore @@ -39,6 +39,9 @@ playwright-report/* ../dist/ out/ +# Bundle analysis +stats/ + *.tfstate *.tfstate.backup *.tfplan diff --git a/site/jest.setup.ts b/site/jest.setup.ts index cd9ffd4b52..f5f19c9f35 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -1,7 +1,6 @@ import "@testing-library/jest-dom" import { cleanup } from "@testing-library/react" import crypto from "crypto" -import * as util from "util" import { server } from "./src/testHelpers/server" import "jest-location-mock" @@ -32,27 +31,5 @@ afterEach(() => { // Clean up after the tests are finished. afterAll(() => server.close()) -// Helper utility to fail jest tests if a console.error is logged -// Pulled from this blog post: -// https://www.benmvp.com/blog/catch-warnings-jest-tests/ - -// For now, I limited this to just 'error' - but failing on warnings -// would be a nice next step! We may need to filter out some noise -// from material-ui though. -const CONSOLE_FAIL_TYPES = ["error" /* 'warn' */] as const - -// Throw errors when a `console.error` or `console.warn` happens -// by overriding the functions -CONSOLE_FAIL_TYPES.forEach((logType: typeof CONSOLE_FAIL_TYPES[number]) => { - global.console[logType] = (format: string, ...args: Type[]): void => { - throw new Error( - `Failing due to console.${logType} while running test!\n\n${util.format( - format, - ...args, - )}`, - ) - } -}) - // This is needed because we are compiling under `--isolatedModules` export {} diff --git a/site/package.json b/site/package.json index c5da3d5028..33b0ec3a4e 100644 --- a/site/package.json +++ b/site/package.json @@ -24,6 +24,7 @@ "test:coverage": "jest --selectProjects test --collectCoverage", "test:watch": "jest --selectProjects test --watch", "typegen": "xstate typegen 'src/**/*.ts'", + "stats": "STATS=true yarn build && npx http-server ./stats -p 8081 -c-1", "deadcode": "ts-prune | grep -v \".stories\\|.typegen\\|.config\\|e2e\\|__mocks__\\|used in module\\|testHelpers\\|typesGenerated\" || echo \"No deadcode found.\"" }, "dependencies": { @@ -58,6 +59,7 @@ "jest-location-mock": "1.0.9", "js-untar": "2.0.0", "just-debounce-it": "3.1.1", + "lodash": "4.17.21", "playwright": "^1.29.2", "react": "18.2.0", "react-chartjs-2": "4.3.1", @@ -69,6 +71,7 @@ "react-router-dom": "6.4.1", "react-syntax-highlighter": "15.5.0", "remark-gfm": "3.0.1", + "rollup-plugin-visualizer": "5.9.0", "sourcemapped-stacktrace": "1.1.11", "ts-prune": "0.10.3", "tzdata": "1.0.30", diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index 5646574345..5f62997772 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -1,7 +1,7 @@ import Button from "@material-ui/core/Button" import InputAdornment from "@material-ui/core/InputAdornment" import Popover from "@material-ui/core/Popover" -import TextField, { TextFieldProps } from "@material-ui/core/TextField" +import TextField from "@material-ui/core/TextField" import { OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { useRef, FC, useState } from "react" import Picker from "@emoji-mart/react" @@ -9,10 +9,9 @@ import { makeStyles } from "@material-ui/core/styles" import { colors } from "theme/colors" import { useTranslation } from "react-i18next" import data from "@emoji-mart/data/sets/14/twitter.json" +import { IconFieldProps } from "./types" -export const IconField: FC< - TextFieldProps & { onPickEmoji: (value: string) => void } -> = ({ onPickEmoji, ...textFieldProps }) => { +const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { if ( typeof textFieldProps.value !== "string" && typeof textFieldProps.value !== "undefined" @@ -111,3 +110,5 @@ const useStyles = makeStyles((theme) => ({ paddingBottom: theme.spacing(0.5), }, })) + +export default IconField diff --git a/site/src/components/IconField/LazyIconField.tsx b/site/src/components/IconField/LazyIconField.tsx new file mode 100644 index 0000000000..a196ac6cf2 --- /dev/null +++ b/site/src/components/IconField/LazyIconField.tsx @@ -0,0 +1,12 @@ +import { lazy, FC, Suspense } from "react" +import { IconFieldProps } from "./types" + +const IconField = lazy(() => import("./IconField")) + +export const LazyIconField: FC = (props) => { + return ( + + + + ) +} diff --git a/site/src/components/IconField/types.ts b/site/src/components/IconField/types.ts new file mode 100644 index 0000000000..03a30875bf --- /dev/null +++ b/site/src/components/IconField/types.ts @@ -0,0 +1,5 @@ +import { TextFieldProps } from "@material-ui/core/TextField" + +export type IconFieldProps = TextFieldProps & { + onPickEmoji: (value: string) => void +} diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 0af1e1bf8a..48fe9dd55f 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -7,7 +7,6 @@ import { TemplateExample, } from "api/typesGenerated" import { FormFooter } from "components/FormFooter/FormFooter" -import { IconField } from "components/IconField/IconField" import { ParameterInput } from "components/ParameterInput/ParameterInput" import { Stack } from "components/Stack/Stack" import { @@ -23,6 +22,7 @@ import { CreateTemplateData } from "xServices/createTemplate/createTemplateXServ import * as Yup from "yup" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip" +import { LazyIconField } from "components/IconField/LazyIconField" const validationSchema = Yup.object({ name: nameValidator("Name"), @@ -154,7 +154,7 @@ export const CreateTemplateForm: FC = ({ variant="outlined" /> - void isLoading: boolean }> = ({ group, errors, onSubmit, onCancel, isLoading }) => { - const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) const form = useFormik({ initialValues: { name: group.name, @@ -48,9 +40,6 @@ const UpdateGroupForm: FC<{ onSubmit, }) const getFieldHelpers = getFormHelpers(form, errors) - const hasIcon = form.values.avatar_url && form.values.avatar_url !== "" - const emojiButtonRef = useRef(null) - const styles = useStyles() const { t } = useTranslation("common") return ( @@ -65,65 +54,16 @@ const UpdateGroupForm: FC<{ label="Name" variant="outlined" /> - - (e.currentTarget.style.display = "none")} - onLoad={(e) => (e.currentTarget.style.display = "inline")} - /> - - ) : undefined, - }} + onPickEmoji={(value) => form.setFieldValue("avatar_url", value)} /> - - - { - setIsEmojiPickerOpen(false) - }} - > - { - form - .setFieldValue("avatar_url", `/emojis/${emojiData.unified}.png`) - .catch((ex) => { - console.error(ex) - }) - setIsEmojiPickerOpen(false) - }} - /> - - = ({ ) } -const useStyles = makeStyles((theme) => ({ - "@global": { - "em-emoji-picker": { - "--rgb-background": theme.palette.background.paper, - "--rgb-input": colors.gray[17], - "--rgb-color": colors.gray[4], - }, - }, - adornment: { - width: theme.spacing(3), - height: theme.spacing(3), - }, - iconField: { - paddingBottom: theme.spacing(0.5), - }, -})) - export default SettingsGroupPageView diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 193acde566..d77aefedc5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -1,20 +1,12 @@ import Box from "@material-ui/core/Box" import Checkbox from "@material-ui/core/Checkbox" import Typography from "@material-ui/core/Typography" -import data from "@emoji-mart/data/sets/14/twitter.json" -import Picker from "@emoji-mart/react" -import Button from "@material-ui/core/Button" -import InputAdornment from "@material-ui/core/InputAdornment" -import Popover from "@material-ui/core/Popover" -import { makeStyles } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { FormFooter } from "components/FormFooter/FormFooter" import { Stack } from "components/Stack/Stack" import { FormikContextType, FormikTouched, useFormik } from "formik" -import { FC, useRef, useState } from "react" -import { colors } from "theme/colors" +import { FC } from "react" import { getFormHelpers, nameValidator, @@ -25,6 +17,7 @@ import * as Yup from "yup" import i18next from "i18next" import { useTranslation } from "react-i18next" import { Maybe } from "components/Conditionals/Maybe" +import { LazyIconField } from "components/IconField/LazyIconField" const TTLHelperText = ({ ttl }: { ttl?: number }) => { const { t } = useTranslation("templateSettingsPage") @@ -81,7 +74,6 @@ export const TemplateSettingsForm: FC = ({ isSubmitting, initialTouched, }) => { - const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) const validationSchema = getValidationSchema() const form: FormikContextType = useFormik({ @@ -108,10 +100,6 @@ export const TemplateSettingsForm: FC = ({ initialTouched, }) const getFieldHelpers = getFormHelpers(form, error) - const styles = useStyles() - const hasIcon = form.values.icon && form.values.icon !== "" - const emojiButtonRef = useRef(null) - const { t } = useTranslation("templateSettingsPage") return ( @@ -145,65 +133,15 @@ export const TemplateSettingsForm: FC = ({ rows={2} /> -
- - (e.currentTarget.style.display = "none")} - onLoad={(e) => (e.currentTarget.style.display = "inline")} - /> - - ) : undefined, - }} - /> - - - - { - setIsEmojiPickerOpen(false) - }} - > - { - // See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222 - form.setFieldValue( - "icon", - `/emojis/${emojiData.unified.replace(/-fe0f$/, "")}.png`, - ) - setIsEmojiPickerOpen(false) - }} - /> - -
+ form.setFieldValue("icon", value)} + /> = ({ ) } - -const useStyles = makeStyles((theme) => ({ - "@global": { - "em-emoji-picker": { - "--rgb-background": theme.palette.background.paper, - "--rgb-input": colors.gray[17], - "--rgb-color": colors.gray[4], - }, - }, - adornment: { - width: theme.spacing(3), - height: theme.spacing(3), - }, - iconField: { - paddingBottom: theme.spacing(0.5), - }, -})) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index 77ba216ea7..e0608628d3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -26,7 +26,7 @@ const validFormValues = { name: "Name", display_name: "A display name", description: "A description", - icon: "A string", + icon: "vscode.png", default_ttl_ms: 1, allow_user_cancel_workspace_jobs: false, } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dc57d18573..580f4e47b8 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3,7 +3,7 @@ import { FieldError } from "api/errors" import { everyOneGroup } from "util/groups" import * as Types from "../api/types" import * as TypesGen from "../api/typesGenerated" -import { range } from "lodash" +import range from "lodash/range" import { Permissions } from "xServices/auth/authXService" export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = { diff --git a/site/vite.config.ts b/site/vite.config.ts index d4049d0b67..2aeced727e 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -1,9 +1,20 @@ import react from "@vitejs/plugin-react" import path from "path" -import { defineConfig } from "vite" +import { defineConfig, PluginOption } from "vite" +import { visualizer } from "rollup-plugin-visualizer" + +const plugins: PluginOption[] = [react()] + +if (process.env.STATS !== undefined) { + plugins.push( + visualizer({ + filename: "./stats/index.html", + }), + ) +} export default defineConfig({ - plugins: [react()], + plugins: plugins, publicDir: path.resolve(__dirname, "./static"), build: { outDir: path.resolve(__dirname, "./out"), diff --git a/site/yarn.lock b/site/yarn.lock index cda1ad3f3f..07f5d5fb6d 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -9549,7 +9549,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12226,6 +12226,16 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rollup-plugin-visualizer@5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz#013ac54fb6a9d7c9019e7eb77eced673399e5a0b" + integrity sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg== + dependencies: + open "^8.4.0" + picomatch "^2.3.1" + source-map "^0.7.4" + yargs "^17.5.1" + rollup@~2.78.0: version "2.78.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.1.tgz#52fe3934d9c83cb4f7c4cb5fb75d88591be8648f" @@ -12680,7 +12690,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.3, source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -14409,7 +14419,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.5.1: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==