Documentation is also provided in the Github repository in the README file /docs/README.md.
Developer Documentation
Contents
Script | Description |
---|---|
analyze Copied! Failed! | Run webpack-bundle-analyzer against a stats.json in the root directory |
analyze:dev Copied! Failed! | Build dev assets and run webpack-bundle-analyzer against the generated asssets |
analyze:prod Copied! Failed! | Build prod assets and run webpack-bundle-analyzer against the generated asssets |
build:dev Copied! Failed! | Build dev assets and generate stats.json |
build:dist Copied! Failed! | Create a dist/ directory in the project root |
build:prod Copied! Failed! | Build prod assets and generate stats.json |
build:static Copied! Failed! | Copy the static/ directory ino the project root into dist/ |
clean Copied! Failed! | Remove stats.json and dist/ directory |
dev Copied! Failed! | Build dev assets and run the webserver, running server logs through pino-pretty for development purposes |
lint Copied! Failed! | Run ESLint |
prod Copied! Failed! | Build prod assets and run the webserver, running server logs through pino-pretty for development purposes |
run Copied! Failed! | Run dist/index.js |
start Copied! Failed! | Build prod assets and run the webserver, intended to be used in production environments |
test Copied! Failed! | Build prod assets and run the webserver, intended to be used in production environments |
test:snapshot Copied! Failed! | Update snapshots with Jest, can be used in lieu of npm run test -- -u |
watch Copied! Failed! | Run the watch server, will restart the server on every file change |
Tamsui uses environment-aware configurations via Webpack's DefinePlugin. This behavior is instrumented in webpack/define.js.
A config file project-configs.js exists in the project root directory. These variables are replaced with their corresponding values in the project code at build time.
Currently the following variables are used:
- PORT (number): The port the express server will run on.
- SERVE_STATIC (boolean): Whether the Express server should serve static assets located in dist/static/, dist/styles/, and dist/scripts/.
- PROJECT_URL (string): The intended deployment URL of the application in each environment, used to set canonical links and Open Graph meta tags.
Adding configuration variables
To add a configuration variable, add the variable as a property to project-configs.js, in both defined environments:
// project-configs.js
module.exports = {
development: {
NEW_CONFIG: false,
},
production: {
NEW_CONFIG: false,
},
};
Then add a type declaration for the new configuration variable to app/global.d.ts:
// app/globals.d.ts
declare const NEW_CONFIG: boolean;
To avoid running afoul of ESLint's no-undef rule when using these project config variables, a /* global */ comment should be used in any file that references a project config variable:
// example.js
/* global NEW_CONFIG */
if (NEW_CONFIG) {
// ...
}
Now NEW_CONFIG can be used in the project code. When building for development, instances of NEW_CONFIG will be replaced with false, when building for production instances will be replaced with true.
Note: String values must be double-wrapped in quotes, since all config values are passed into the DefinePlugin. This means that string values are treated as code fragments. Calling JSON.stringify() on string values will also preserve them as strings:
// project-config.js
module.exports = {
development: {
DOUBLE_WRAPPED_STRING: "'string value'",
JSON_WRAPPED_STRING: JSON.stringify('remains a string'),
},
production: {
// ...
},
};
Testing with configuration variables
New configuration variables can either be mocked into tests by modifying the global object, or adding the variable to the Jest globals object. The globals object can be added to sharedConfigs, appConfig, clientConfig, or serverConfig (as appropriate) in jest.config.js.
Run npm run watch to run the development watch server.
npm run dev will build and run the application in development mode. npm run prod will build and run the application in production mode. All of the above modes will run the server output through pino-pretty.
The client/ directory is intended to house files specific to the client-side bundle. At the moment it only contains the entrypoint file, which mounts and hydrates the root application.
The app/ directory houses application-level concerns: the root application contains the the html root, head, and body. The routes are housed in app/dataRoutes as a data routes object. The reason the routes are not declared in JSX is for compatibility with rendering React to a Node.js stream.
The pages/ directory houses the page-level react components. These are plugged into the application via the dataRoutes object.
The server/ directory houses the server-side rendering logic and defines the static asset directories. Pino is implemented as the logger.
To add additional application directories, or to remove existing ones:
- Path aliases need to be updated
- Jest projects need to be updated
The app/ and pages/ directories are set up with path aliases so that a module can be imported with absolute pathing rather than relative pathing:
import Module from 'app/module';
import PageModule from 'page/PageModule';
If any directory is removed, or if a new directory needs an alias:
- An alias should be updated in the Webpack configs.
- The alias mapping should be added to the eslint configuration to avoid linting errors when using the alias.
- The alias should be updated in the paths configuration for TypeScript to avoid type errors.
- The alias should be updated in the moduleNameMapper configuration in the Jest configuration to ensure module mocking will work in tests, and so there are no errors in using the alias.
- The directory should be updated in the appConfig.testMatch array to ensure that the test runner knows which directories to cover.
The directory static/, housed in the project root, is copied directly into the dist/static/ directory on project build. This directory is served statically by the Express server in development builds. static/ is meant to house static files (images, JSON files, etc.) used by Tamsui.
Similarly, JavaScript assets are built to dist/scripts/, CSS files are built to dist/styles/. For local development all of these directories are served by the Express backend server (dictated by the dist/styles/ project variable in project-configs.js). For production builds the SERVE_STATIC config is set to false by default.
On deployment: the directories /dist/static/, /dist/scripts/, and /dist/styles/ can be deployed to a CDN.
Tamsui implements a basic React Error Boundary in app/ErrorBoundary. This error boundary is configured as an errorElement in the dataRoutes object. It is recommended all root routes are wrapped in this error boundary. A utility, withErrorBoundary is provided to more easily (and declaratively) extend routes with this errorElement property:
// app/dataRoutes/index.ts
import PageComponent from 'pages/PageComponent';
import withErrorBoundary from './withErrorBoundary';
const dataRoutes = [
withErrorBoundary({
path: '/path-to-page',
Component: PageComponent,
}),
];
On the server, if an error in rendering the application occurs it will redirect the request to /error, configured by default to be the application's error page.
On the client, an error in a page will render the ErrorPage component as fallback.
To change the path to the error page: change the value of the constant errorPagePath in server/appHandler.tsx. Make sure you define this route in the app/dataRoutes/index.ts file.
Alternatively, keep the error page path and just replace the ErrorPage component.
Tamsui utilizes Jest as a test runner. Tests should be housed in a __tests__/ directory within their respective project directories. Test files should have the file extension .test.js
The default coverage threshold is set too 100% across the board. To reduce or remove the coverage requirements, modify the coverageThreshold field in the config.
To run the test suite locally: npm test. To update Jest snapshots npm run test:snapshot can be used. Alternatively, npm run test -- -u will also work.
Layouts can be directly applied to routes in React Router using Layout Routes.
// dataRoutes.js
import Layout from 'app/Layout';
import Home from 'pages/Home';
import AnotherPage from 'pages/AnotherPage';
const routes = [{
Component: Layout,
children: [{
path: '/',
Component: Home,
}, {
path: '/another-page',
Component: AnotherPage,
}],
}];
In the boilerplate, a default Layout has been created in app/Layout.
One issue that has arisen from the usage of a layout route is that client-side navigation will not reset the browser scroll upon page navigation: clicking a link to another page will land the user on the new page, scrolled the same amount as on the previous page.
A solution has been implemented in this boilerplate in the form of a custom react hook: useResetScroll. This hook needs to be invoked by any layouts in use, and Links to pages that need the scroll reset need to pass a state object with a property resetScroll set to true. A reusable component InternalLink has been provided to automatically pass this state object property.
Tamsui uses a React Router data router. A route can be passed a handle, which allows any arbitrary data to be passed to a route, retrieved in a component using the useMatches hook.
This boilerplate has a baseline implementation of this usage in place, passing a PageHandle object. This is intended to pass route-specific or page-specific data to a page component. A PageHandle is currently defined as:
type RouteHead = {
title?: string;
tags?: React.ReactNode;
};
type PageHandle = {
head?: RouteHead;
};
A PageHandle object can be passed into a given data route's handle field. If this PageHandle contains a head property with title or tags then these values will be used by the app/Head component to set the title and append route-specific head tags to a page.
NOTE: app/Head is rendered by app/HTMLBody, which rendered by app/Layout. This makes app/Head the ideal component to house site-wide document head tags. To leverage this in new layouts, follow the example set by app/Layout: wrap the contents of the layout in app/HTMLBody and invoke the hook useResetScroll to ensure proper scroll behavior on route change.
A custom hook, app/hooks/useRouteHead, has been created for use in app/Head to retrieve the contents of a route's PageHandle head property if it exists.
To add a PageHandle to a data route:
// dataRoutes.js
import Home from 'pages/Home';
import About from 'pages/About';
const dataRoutes = [{
path: '/',
Component: Home,
handle: {
head: {
title: 'Welcome to my homepage!',
tags: (
<>
<meta property ="og:title" content ="Check out my website!" />
<meta property ="og:description" content ="I think it might be the best website ever made." />
</>
),
},
},
}, {
path: '/about',
Component: About,
handle: {
head: {
title: 'Some things about my website',
tags: (
<>
<meta property ="og:title" content ="Learn about the best website" />
<meta property ="og:description" content ="10 reasons this site is best." />
</>
),
},
},
}];
export default dataRoutes;
These titles and tags will be rendered into the document head only for their respective routes.
Tamsui contains a Github Pull Request template that is intended to provide a scaffold for a thorough pull request. This template provides the following sections:
- Description:
- Link to a related issue/story - important for documentation of why this change is being made
- A prompt to provide a description of the intent of the changes
- Changes: A prompt to list the code changes in the pull request
- Steps to QA: A prompt to provide steps for reviewers to verify the changes achieve their intended result
The reason for these sections is two-fold:
- Make it easier for reviewers to review the code changes, with full context of the reasons the changes are being made, in order that the team can produce the best results for the codebase.
- Provide a history of documentation in the repository, so that every significant change is documented and vetted. This can be useful in the future to trace changes and intent when debugging or extending the codebase.
To build for production run npm build:prod. The production assets to be deployed will be generated in the dist/ directory.
To build the production assets and run the Express server in a single command, run npm start.