/disw-web-app

A platform for my personal website.

Primary LanguageTypeScript

DISW Web App

This repository contains the Node.js project for the website. It is deployed as static content to a web server.

Prerequisites

  • Node.js, version 16 or newer.

  • Yarn, which is bundled with Node.js as an opt-in:

    (requires admin privileges)
    corepack enable

    or which you can install with npm:

    npm install -g yarn

Recommendations

Browser extensions:

IntelliJ IDEA plugins:

Getting Started

Specifying Your Git Identity

…​/disw-web-app> git config user.name "<first name> <last name>"
…​/disw-web-app> git config user.email "<id+username>@users.noreply.github.com"

Installing Third-Party Dependencies

This project uses the zero-install feature of the Yarn package manager. Upon cloning the Git repository, the .yarn/cache directory already contains a cache of the third-party dependencies, effectively replacing the usual node_modules directory.

Using IntelliJ IDEA

Open the project directory into which you cloned the repository: menu:File[Open].

Caution
You should not import the project or create a new project from existing sources.

Adjust the inspection settings to disable false positive warnings:

  1. Open the inspection settings: menu:File[Settings > Editor > Inspections].

  2. Disable warnings from this inspection:

    • menu:JavaScript and TypeScript[Imports and dependencies > Missing 'React' namespace import in JSX code].

Specify a pattern of files to exclude from indexing:

  1. Open the project module settings: menu:File[Project Structure > Modules].

  2. For the disw-web-app module, set Exclude files to .asciidoctor*;.editorconfig;.eslint*;.git*;.pnp*;*yarn*.

Defining a Profile

A profile describes the content to display in the web app. It defines three components: HeaderContent, MainContent, and FooterContent. A reference implementation is available in perserverance-dummy.tsx.

The VITE_PROFILE_NAME environment variable specifies the profile to use. It is defined in .env.development.

You can create a new profile:

  1. In the main/content directory, create a TypeScript file named e.g. my-profile.local.tsx which must export the name to be displayed in the title bar, a description for search engines and web browsers, and three function components:

    import { Hero, Main, Footer } from "+profile"
    
    export const name = "My Profile Name"
    export const description = "An accurate summary of the page content."
    
    export function HeaderContent() {
        return (
            <Hero> ... </Hero>
        )
    }
    
    export function MainContent() {
        return (
            <Main> ... </Main>
        )
    }
    
    export function FooterContent() {
        return (
            <Footer> ... </Footer>
        )
    }
  2. In the main/content directory, create a new directory named assets.local to contain image assets for the hero and the occupation timeline of the profile page. Use the HeroImage and ThemedArticleImage components to encapsulate the image assets in their own components. Provide images in multiple sizes to accommodate responsive design. Encode the images as WebP files and provide a JPEG file as a fallback.

  3. In the project root directory, create a new file named .env.development.local (or .env.production.local when building for production). Override the VITE_PROFILE_NAME environment variable to use the new profile by specifying the name of the TypeScript file without the .tsx extension:

    VITE_PROFILE_NAME=my-profile.local
Important

The Git repository in this project ignores files and directories whose names contain the substring .local. Use this to prevent personal information from leaking into the repository. Examples: my-profile.local.tsx and assets.local.

Tip

When switching between profiles in .env.development.local, you may hit this error in the web browser console:

Uncaught Error: Hook can only be invoked from render methods.

You can resolve this error by restarting the development server.

Common Tasks

The scripts field in package.json defines a set of daily work tasks. Corresponding IntelliJ IDEA configurations are located in .idea/runConfigurations.

Starting a Development Server

Run the develop configuration in IntelliJ IDEA to serve the web app in development, or execute:

…​/disw-web-app> yarn run develop

Visit one of the addresses printed in the console in a web browser. Use http://localhost:8000 when you want to access the web app from your local computer, or use the LAN IP address (usually something like 192.168.X.Y) when you want to access it from another device.

Equipped with hot module replacement (HMR), it reflects any changes you make in the source code immediately in the web browser.

Validating the Software Quality

Run the validate configuration in IntelliJ IDEA to validate the quality of the software, or execute:

…​/disw-web-app> yarn run validate

It runs the following means of validation:

  • Type checking via TypeScript (validate:type-check).

  • Static program analysis (linting) via ESLint (validate:lint).

  • Automated unit testing via Vitest (validate:unit-test).

The validate:lint:fix configuration applies an automated fix of certain issues reported by ESLint.

The validate:unit-test:watch configuration makes the unit test suite run continuously (i.e. in 'watch' mode).

Building for Production

Run the build configuration in IntelliJ IDEA to make a production-grade distribution of the web app, or execute:

…​/disw-web-app> yarn run build

It saves the output in the build/www directory.

Caution

The software validation criteria must pass before it attempts to build the distribution.

The build:generate configuration generates the distribution without validating the software quality.

The build:preview configuration serves a preview of the generated distribution at http://localhost:80.

Tools

Third-Party Dependencies

Following the Node.js convention, this project distinguishes between runtime dependencies and development dependencies. The dependencies and devDependencies fields in package.json declare these two sets of dependencies, respectively.

Tip

A dependency is a runtime dependency when it is imported by the production source code.

For example, preact is a runtime dependency, as it is imported by main-client.tsx. On the other hand, tailwindcss is a development dependency, as it uses a JIT compiler in the build pipeline to generate CSS rulesets dynamically.

Use the custom dependenciesComments and devDependenciesComments fields to associate each dependency to a maintenance comment or a description that justifies its use in this project. Preferably, runtime dependencies should not have any transitive dependencies.

Important

For security reasons, always specify the exact version of a dependency in package.json.

Avoid using the ^ and ~ modifiers, which would otherwise allow the package manager to install a newer minor or patch version of the dependency than the one specified.

To update a third-party dependency to its latest version, execute these two commands:

…​/disw-web-app> yarn up --exact <dependency>
…​/disw-web-app> yarn up --recursive <dependency>

Preact

Preact is a reactive web UI framework with an API similar to that of React. It lets you define components as JavaScript functions using JSX.

Caution

The JSX dialect of Preact is slightly different from React.

For historical reasons, most tools support JSX transformations for React, compiling JSX to function calls of React.createElement by default.

However, in Preact, the factory function is h (also known as hyperscript).

Tailwind CSS

Tailwind CSS is a utility-first CSS framework. It encourages the developer to reuse styles by extracting components (e.g. via Preact) rather than defining CSS rules and abstractions as practised in traditional CSS development.

tailwind.config.cjs defines the configuration of the Tailwind CSS environment.

Tip
The .cjs file extension indicates that the JavaScript file follows the CommonJS module standard of Node.js instead of the modern ECMAScript module standard (ESM). The latter is usually indicated by the .mjs file extension.

PostCSS

PostCSS is a processing tool for CSS. Among other things, it permits the use of CSS syntax extensions such as the @tailwind and @apply directives from Tailwind CSS.

postcss.config.cjs defines the configuration of PostCSS as recommended by the Tailwind CSS documentation.

TypeScript

TypeScript is a programming language that extends JavaScript with syntax for static typing.

tsconfig.json defines the configuration of the TypeScript environment, except for the set of globally visible types which global.d.ts defines.

Vite

Vite is a frontend build tool. It hosts the development server and generates the distribution of the web app for production.

vite.config.ts defines the configuration of Vite. It picks up the PostCSS configuration in postcss.config.cjs automatically.

Caution

You can define the configurations of PostCSS and Tailwind CSS directly in the Vite configuration file.

However, doing so would prevent Vite from instantly applying configuration changes, particularly in Tailwind CSS themes, without requiring a restart of the development server.

To load the correct profile into the web app, the Vite configuration defines +content to be an alias for the TypeScript module designated by VITE_PROFILE_NAME.

Generating a production-grade distribution of the web app consists of two phases:

  1. A server-oriented build which produces a CommonJS module that generates static HTML from the initial state of the web app. The entry point is main-server.tsx. Vite operates in ssr mode during this phase.

  2. A client-oriented build which pre-renders the HTML page and produces a browser script that makes the web app interactive. The entry point is index.html, which in turn imports main-client.tsx. Vite operates in its normal mode during this phase. Additionally, the Vite configuration imports the CommonJS module produced by the server-oriented build to complete pre-rendering the HTML page at build-time.

Note

Pre-Rendering, also known as Static Site Generation (SSG), is a technique in which a static HTML page is generated at build-time.

Server-Side Rendering (SSR) is a slightly different technique in which the HTML page is generated dynamically by the web server at request-time. Both techniques allow search engines to discover the contents of the web app without having to execute any browser scripts.

If you need to debug the production-grade distribution, you can set VITE_DEBUG_PRODUCTION_BUILD=true in .env.production.local. This enables Preact debugging tools on runtime and skips minification of the build artifacts.

Vitest

Vitest is a unit testing framework for JavaScript. It relies on Vite to support TypeScript, JSX, and PostCSS. Its API is largely compatible with that of Jest.

The test field in vite.config.ts defines the configuration of Vitest.

ESLint

ESLint is a static program analysis tool that flags issues in the source code.

.eslintrc.cjs defines the configuration of ESLint, including the set of rules to be enforced.

It uses TypeScript ESLint to parse TypeScript sources and perform type-aware analysis according to tsconfig.json.

Caution
Every rule must be set to either error or off. We do not use the warning level, as it would only pollute the linting report while allowing rules to be violated without interrupting the build step.

AsciiDoc

AsciiDoc is a markup language for writing documentation. AsciiDoctor is a processing tool that converts AsciiDoc files to various output formats such as HTML and PDF.

No configuration is needed for accessing the AsciiDoc documentation files in plain text.

Tip

IntelliJ IDEA users with the AsciiDoc plugin may customise the HTML preview by providing a stylesheet:

  1. In the project root directory, create a new directory named .asciidoctor. Git will ignore this directory.

  2. Copy your stylesheet (e.g. my-preview-stylesheet.css) into the .asciidoctor directory.

  3. In the project root directory, create a new file named .asciidoctorconfig. Git will ignore this file. Insert this content into the file to apply the stylesheet:

    :experimental:
    :stylesdir: {asciidoctorconfigdir}/.asciidoctor/
    :stylesheet: my-preview-stylesheet.css

The configuration file and the stylesheet should not be checked into the Git repository. By refraining from doing so, developers may provide their own stylesheet to suit their preference, for example to match a light or a dark theme in IntelliJ IDEA.