Compare commits

...

2 Commits

Author SHA1 Message Date
Yana c8c2cd098a Add styles to fig in Storybook 2022-10-21 17:37:40 +02:00
Mark Sinclair 0f2a6ac87b Template project to visualise Fig specs for user docs 2022-10-20 14:43:21 +01:00
25 changed files with 15341 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
@@ -0,0 +1,40 @@
/* eslint-disable no-param-reassign */
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
},
// webpackFinal: async (config, { configType }) => {
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// // You can change the configuration based on that.
// // 'PRODUCTION' is used when building the static version of storybook.
webpackFinal: async (config) => {
config.module.rules.forEach((rule) => {
// look for SVG import rule and replace
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
if (rule.test?.toString().includes('svg')) {
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
}
});
// handle asset loading with this
config.module.rules.unshift({
test: /\.svg(\?.*)?$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
// Return the altered config
return config;
},
features: {
emotionAlias: false,
},
};
@@ -0,0 +1,56 @@
/* eslint-disable react/react-in-jsx-scope */
import { NymThemeProvider } from '@nymproject/mui-theme';
import { Box } from '@mui/material';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
const withThemeProvider = (Story, context) => (
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
<div>
<NymThemeProvider mode="light">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
</Box>
</NymThemeProvider>
</div>
<div>
<NymThemeProvider mode="dark">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
</Box>
</NymThemeProvider>
</div>
</div>
);
export const decorators = [withThemeProvider];
+98
View File
@@ -0,0 +1,98 @@
# Example with React + Typescript + Webpack 5 + MUI
An example of using default Webpack and Typescript settings with React and MUI, including theming.
You can use this example as a seed for a new project.
Remember to build the dependency packages from the root of this repo by running:
```
yarn
yarn build
```
If you need to make changes to the dependency packages, you can run `yarn watch` in that package to watch for chagnes and build them. This project will pick up the changes in the built package and hot-reload / recompile.
## Features
### Yarn workspaces
Packages from `ts-packages` are shared using Yarn workspaces. Make sure you add you new project to [package.json](../../package.json) to use the shared packages.
> ⚠️ **Warning**: Yarn workspaces will share all dependencies between projects and works by falling back to parent directories until a `node_modules` directory is found. So be careful when messing around with `node_modules` and resolution, because unexpected things could happen - for example, if you do not run `yarn` from the root and you have a `node_modules` in a directory that is a parent of the directory where you checkout out this repository, that `node_modules` will be used for resolving packages 🙀.
### Typescript
Shared Typescript config is in [tsconfig.json](./tsconfig.json), with specific production settings in [tsconfig.prod.json](./tsconfig.prod.json) that:
- exclude Storybook stories and Jest tests
- do not output typing `*.d.ts` files
### Webpack
Inherit config for Webpack 5 with additional tweaks including:
- favicon generation from [favicon asset files](../../assets/favicon/favicon.png)
- asset handling (svg, png, fonts, css, etc)
- minification
The development settings include:
- `ts-loader` for quick transpilation
- threaded type checking using `tsc`
- hot reloading using `react-refresh`
### Storybook
Storybook is available in [@nymproject/react](../react-components/src/stories/Introduction.stories.mdx) and can be run using `yarn storybook`.
### MUI and theming
The [Nym theme](../mui-theme/src/theme/theme.ts) provides a theme provider that you can add as follows:
```typescript jsx
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
...
}
```
And augment typings for the Theme by adding [mui-theme.d.ts](./src/theme/mui-theme.d.ts):
```typescript
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
declare module '@mui/material/styles' {
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
```
Adding the above, means that any component now has the correct typings, for example, below the Nym palette interface is available for all MUI `Theme` instances with code completion for VSCode and IntelliJ:
```typescript jsx
import { Typography } from '@mui/material';
...
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
The quick brown fox jumps over the white fence
</Typography>
```
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+96
View File
@@ -0,0 +1,96 @@
{
"name": "@nymproject/cli-fig-docs",
"description": "A library to render user docs from a Fig spec",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@mui/icons-material": "^5.5.0",
"@mui/lab": "^5.0.0-alpha.72",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/react": "^1.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.8",
"@storybook/addon-essentials": "^6.5.8",
"@storybook/addon-interactions": "^6.5.8",
"@storybook/addon-links": "^6.5.8",
"@storybook/builder-webpack5": "^6.5.8",
"@storybook/manager-webpack5": "^6.5.8",
"@storybook/react": "^6.5.8",
"@storybook/testing-library": "^0.0.9",
"@babel/core": "^7.15.0",
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@nymproject/webpack": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@withfig/autocomplete-types": "^1.23.0",
"babel-loader": "^8.2.2",
"babel-plugin-root-import": "^5.1.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.12",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "^2.5.1",
"react-refresh-typescript": "^2.0.3",
"style-loader": "^3.2.1",
"thread-loader": "^3.0.4",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
},
"scripts": {
"start": "webpack serve --progress --port 3000",
"build": "webpack build --progress --config webpack.prod.js",
"build:dev": "webpack build --progress",
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
}
}
+80
View File
@@ -0,0 +1,80 @@
import * as React from 'react';
import { Box, Container, Grid, Typography } from '@mui/material';
import { NymLogo } from '@nymproject/react/logo/NymLogo';
import { useIsMounted } from '@nymproject/react/hooks/useIsMounted';
import { NymThemeProvider } from '@nymproject/mui-theme';
import { useTheme } from '@mui/material/styles';
import { ThemeToggle } from './ThemeToggle';
import { AppContextProvider, useAppContext } from './context';
import { FigDocs } from './components/FigDocs';
import NymClientSpec from '../../../tools/nym-cli/user-docs/fig-spec';
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
const { mode } = useAppContext();
const theme = useTheme();
const isMounted = useIsMounted();
if (isMounted()) {
console.log('Content is mounted');
}
const swatches: Record<string, string> = {
'palette.primary.main': theme.palette.primary.main,
'palette.secondary.main': theme.palette.secondary.main,
'palette.info.main': theme.palette.info.main,
'palette.success.main': theme.palette.success.main,
'palette.text.primary': theme.palette.text.primary,
'theme.palette.nym.networkExplorer.mixnodes.status.active':
theme.palette.nym.networkExplorer.mixnodes.status.active,
'theme.palette.nym.networkExplorer.mixnodes.status.standby':
theme.palette.nym.networkExplorer.mixnodes.status.standby,
};
return (
<Container sx={{ py: 4 }}>
<Box display="flex" flexDirection="row-reverse" pb={2}>
<ThemeToggle />
</Box>
<NymLogo height={50} />
<h1>Example App</h1>
<Box mb={10}>
<Typography sx={{ color: ({ palette }) => palette.nym.networkExplorer.mixnodes.status.active }}>
This is an example app that uses React, Typescript, Webpack and the Nym theme + components.
</Typography>
<h4>Some colours from the theme (mode = {mode}) are:</h4>
<Grid container spacing={2}>
{Object.keys(swatches).map((key) => (
<Grid item key={key}>
<Box display="flex" alignItems="center">
<svg height="50px" width="50px">
<rect width="100%" height="100%" fill={swatches[key]} />
</svg>
<Typography mx={2}>
<code>{swatches[key]}</code>
<br />
<code>{key}</code>
</Typography>
</Box>
</Grid>
))}
</Grid>
</Box>
<h1>Docs</h1>
<FigDocs figSpec={NymClientSpec} />
</Container>
);
};
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
@@ -0,0 +1,21 @@
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import { useAppContext } from './context';
export const ThemeToggle: React.FC = () => {
const { mode, toggleMode } = useAppContext();
return (
<Button variant="outlined" color="secondary" onClick={toggleMode} sx={{ display: 'flex', alignItems: 'centre' }}>
{mode === 'dark' ? (
<DarkModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
) : (
<LightModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
)}
<Typography ml={1} color={(theme) => theme.palette.primary.light}>
Switch to {mode === 'dark' ? 'light mode' : 'dark mode'}
</Typography>
</Button>
);
};
@@ -0,0 +1,13 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { FigDocs } from './index';
import NymClientSpec from '../../../../../tools/nym-cli/user-docs/fig-spec';
export default {
title: 'Docs/Fig',
component: FigDocs,
} as ComponentMeta<typeof FigDocs>;
export const Default = () => <FigDocs />;
export const NymCli = () => <FigDocs figSpec={NymClientSpec} />;
@@ -0,0 +1,140 @@
import { Card, CardContent, Typography, CardActions, Button, Stack, Divider, Box } from '@mui/material';
import * as React from 'react';
export const FigOption = (option: Fig.Option): JSX.Element | null => {
return (
<Box marginBottom={2} marginTop={2}>
<Typography variant="h6" fontWeight={700}>
<pre style={{ margin: 0 }}>
<code>{option.name}</code>
</pre>
</Typography>
<Typography color="text.secondary" marginBottom={2}>
{option.description}
</Typography>
{option.args && Array.isArray(option.args) ? (
<>
<Typography component="span" marginBottom={2}>
Args:
</Typography>
{option.args.map((arg, i) => (
<>
<Typography variant="h6" component="span" key={i}>
{arg.name} {arg.isOptional}
</Typography>
{Array.isArray(option.args) && i < option.args.length - 1 && <Divider />}
</>
))}
</>
) : (
option.args && (
<Typography component="span">
Args: {option.args.name} {option.args.isOptional === true && '--is optional'}
</Typography>
)
)}
</Box>
);
};
export const FigSubcommand = (subcommand: Fig.Subcommand): JSX.Element | null => {
return (
<Box marginLeft={2} marginTop={2}>
<Typography variant="h6" fontWeight={700}>
<pre style={{ margin: 0 }}>
<code>{subcommand.name}</code>
</pre>
</Typography>
<Typography color="text.secondary">{subcommand.description}</Typography>
{subcommand.subcommands && (
<>
<Typography component="div" margin={2}>
Subcommands:
</Typography>
<Divider />
</>
)}
{subcommand.subcommands
? subcommand.subcommands.map((command, i) => {
return (
<Box key={i}>
<FigSubcommand {...command} />
{i < subcommand.subcommands!.length - 1 && <Divider />}
</Box>
);
})
: null}
{subcommand.options && (
<>
<Typography component="div" margin={2}>
Options:
</Typography>
<Divider />
</>
)}
{subcommand.options
? subcommand.options.map((option, i) => {
return (
<Box marginLeft={2} key={i}>
<FigOption {...option} />
{subcommand!.options && i < subcommand!.options.length - 1 && <Divider />}
</Box>
);
})
: null}
</Box>
);
};
export const FigDocs: React.FC<{
figSpec?: Fig.Spec;
}> = ({ figSpec }) => {
let subcommand: Fig.Subcommand | undefined;
if (figSpec) {
if (typeof figSpec !== 'function') {
subcommand = figSpec as Fig.Subcommand;
}
}
return (
<>
<div>Render some docs here</div>
{subcommand && (
<div>
<div>Name: {subcommand.name}</div>
{subcommand.subcommands && (
<Typography variant="h6" component="div" marginBottom={4}>
Subcommands:
</Typography>
)}
{subcommand.subcommands?.map((command, i) => {
return (
<Card sx={{ minWidth: 275, marginBottom: 2 }} key={i}>
<CardContent>
<FigSubcommand {...command} />
{subcommand!.subcommands && i < subcommand!.subcommands.length - 1 && <Divider />}
</CardContent>
</Card>
);
})}
{subcommand.options && (
<Typography variant="h6" component="div" marginBottom={4}>
Options:
</Typography>
)}
{subcommand.options &&
subcommand.options.map((option, i) => {
return (
<Card sx={{ minWidth: 275, marginBottom: 2 }} key={i}>
<CardContent>
<FigOption {...option} />
{subcommand!.options && i < subcommand!.options.length - 1 && <Divider />}
</CardContent>
</Card>
);
})}
</div>
)}
</>
);
};
@@ -0,0 +1,34 @@
import { PaletteMode } from '@mui/material';
import * as React from 'react';
interface State {
mode: PaletteMode;
toggleMode: () => void;
}
const AppContext = React.createContext<State | undefined>(undefined);
export const useAppContext = (): State => {
const context = React.useContext<State | undefined>(AppContext);
if (!context) {
throw new Error('Please include a `import { AppContextProvider } from "./context"` before using this hook');
}
return context;
};
export const AppContextProvider: React.FC = ({ children }) => {
// light/dark mode
const [mode, setMode] = React.useState<PaletteMode>('dark');
const value = React.useMemo<State>(
() => ({
mode,
toggleMode: () => setMode((prevMode) => (prevMode !== 'light' ? 'light' : 'dark')),
}),
[mode],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nym Example with React, Typescript, Webpack</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App />, document.getElementById('app'));
@@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Nym docs components
Renders user docs from a Fig spec
@@ -0,0 +1,13 @@
import React, { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { App } from '../App';
describe('App', () => {
beforeEach(() => {
render(<App />);
});
it('should render without exploding', () => {
const { container } = render(<App />);
expect(container.firstChild).toBeInTheDocument();
});
});
+37
View File
@@ -0,0 +1,37 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true
},
"include": [
".storybook/*.js",
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.stories.*",
],
"exclude": [
"node_modules",
"dist"
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"types": ["@withfig/autocomplete-types"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist",
"src/**/*.test.tsx"
]
}
@@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"declaration": false
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist",
"**/*.stories.*",
"**/*.test.*",
"**/*.spec.*"
]
}
@@ -0,0 +1,18 @@
const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(webpackCommon(__dirname), {
entry: path.resolve(__dirname, 'src/index.tsx'),
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
});
@@ -0,0 +1,67 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
options: {
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
transpileOnly: true,
},
},
],
},
plugins: [
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
new webpack.HotModuleReplacementPlugin(),
],
target: 'web',
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
historyApiFallback: true,
},
// recommended for faster rebuild
optimization: {
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
cache: {
type: 'filesystem',
buildDependencies: {
// restart on config change
config: ['./webpack.config.js'],
},
},
});
+42
View File
@@ -0,0 +1,42 @@
const { mergeWithRules } = require('webpack-merge');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'production',
// TODO: no source maps, add back
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
optimization: {
minimizer: ['...', new CssMinimizerPlugin()],
splitChunks: {
chunks: 'all',
},
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
output: {
pathinfo: false,
filename: '[name].[contenthash].js',
},
});
File diff suppressed because it is too large Load Diff
+5
View File
@@ -6124,6 +6124,11 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe"
integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==
"@withfig/autocomplete-types@^1.23.0":
version "1.23.0"
resolved "https://registry.yarnpkg.com/@withfig/autocomplete-types/-/autocomplete-types-1.23.0.tgz#50d9fb3ae0c0660b5863ba7a3e827ca7fcf1cda4"
integrity sha512-dB9gccfRmkARPadT0hPZwzl4seAjeX6ILj9X3ClWeZ/FExJicRGYGTT8B+iF/gW+7EoPbmdY+C5Akl+NQSCEQQ==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"