JavaScript Technology Setup

Background

Previously we chose an Initial Authorization Server. In this post we will describe how this blog uses JavaScript technology, for Node.js APIs, browser based apps and Electron desktop apps. The goal is to focus on our own requirements rather than being dictated to be technology.

Goal: Simple and Modern Coding Model

The main part of our SPA is the TypeScript logic, which can help to enable a simple coding model with a great separation of concerns. I use TypeScript since I like the extensions to JavaScript in areas such as these:

  • More Expressive Types, including interfaces and private class members
  • Better options for representing Types Exchanged with APIs
  • Easier Refactoring when items are renamed or moved

Web Development Pipeline

The main stages of the development pipeline are summarised in the below table for the browser case. It can be useful to articulate desired behaviour at each stage, in your own efforts to reduce complexity:

Stage Description
Download Dependencies Before we can write any real code we typically need to download libraries
Write Code To write TypeScript code we need to import third party libraries and use their classes and functions
Execute Tasks We sometimes need to execute arbitrary tasks as part of development, such as code quality checks
Build Code We next need to build code so that it runs in browsers, which involves compiling TypeScript
Execute Code We then execute compiled code in the browser, which is different to the code we wrote
Decompile Code When there is a problem, or to support debugging, we want to get back to the original TypeScript code

Downloading Dependencies: Package Manager

I use the mainstream npm tool, where libraries we depend upon and their versions are specified in the package.json file:

{
  "dependencies": {
    "axios": "^1.6.7",
    "mustache": "^4.2.0",
    "oidc-client-ts": "^2.4.0"
  },
  "devDependencies": {
    "@types/mustache": "^4.2.2",
    "@typescript-eslint/eslint-plugin": "^6.9.0",
    "@typescript-eslint/parser": "^6.9.0",
    "eslint": "^8.52.0",
    "ts-loader": "^9.5.0",
    "typescript": "^5.3.3",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-merge": "^5.10.0"
  }
}

I like to keep the number of third party libraries to a minimum, since each of them need to be kept working, understood and managed over time.

Downloading Dependencies:  Node Modules

When we execute npm install, these libraries and their dependencies get downloaded to a node_modules folder, from which we can import classes and functions:

Each library is consumed as JavaScript, even if it was originally coded in TypeScript. An index.d.ts file is provided for TypeScript consumers, which enables type checking and intellisense for the public interface.

Writing Code: Resolving Imports

We can import libraries in our own source files via one of the following syntaxes. The first of these imports a newer ECMAScript module, whereas the second uses the older CommonJS format.

import {UserManager, UserManagerSettings} from 'oidc-client';
import mustache from 'mustache';

This blog’s APIs use Node.js 18 or later, with the following tsconfig.json configuration. Some of these settings are used for importing modules, and others for producing compiled code:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "lib": ["ES2022"],
    "module":"ES2022",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "outDir": "dist",
    "sourceMap": true
  },
  "include": [
    "./src"
  ],
  "exclude": [
    "node_modules"
  ]
}

The following settings are relevant to importing modules. I aim to use only the first two of these, for all projects, including SPAs:

Setting Description
moduleResolution Use the most compatible rules when resolving external modules
allowSyntheticDefaultImports Enable the simplest import syntax for CommonJS modules
esModuleInterop Provides stronger compatibility with CommonJS modules when needed

We also need to tell the compiler that the project’s own source files use ECMAScript modules. This is done by specifying ‘type=module‘ in the package.json file.

For Node.js APIs, a ‘.js‘ suffix is also required when importing other classes. During coding this is a TypeScript file, though it will become JavasScript when it is executed:

import {CompanyRepository} from '../repositories/companyRepository.js';

Writing Code: Using Type Definition Files

The index.d.ts provided by libraries can sometimes exist in a non-standard location. If so, options such as typeRoots can be used to resolve paths. In some cases a separate type definitions file is used from Definitely Typed, and these are recognised from package.json entries prefixed with ‘@types.

For libraries with no type definitions, you can add a typings.d.ts file at the root level, then use a ‘declare module‘ statement. This gets past compiler errors, though there is then no type checking or intellisense:

declare module 'some-js-lib';

During development, I run Node.js APIs using ts-node, to run TypeScript classes. The ‘files: true‘ entry in the API’s tsconfig.json file then helps to ensure that all dependencies are detected correctly.

Writing Code: Using TypeScript Libraries

The tsconfig.json file for the SPA is a little different to that for the Node.js API. The lib option brings in browser type definitions and also enables the most modern coding syntax:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2017",
    "lib": ["ES2022", "DOM"],
    "module":"ES2015",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "outDir": "dist",
    "sourceMap": true
  },
  "include": [
    "./src"
  ],
  "exclude": [
    "node_modules"
  ]
}

These libraries are located under node_modules/typescript/lib and include web definitions such as HTMLInputElement:

Writing Code: Efficient Imports

When ECMAScript modules are imported, it is recommended to import only the functionality needed. This will reduce the size of built code. For browser based applications this reduce download times for end users:

import {SmallUtility} from 'LargeLibrary';

Writing Code: TypeScript Development

I  use a class based development model, to encapsulate data, behaviour and dependencies together in the most standard way.  Constructor injection is used for inter-class dependencies:

export class CompaniesView {

    private readonly _apiClient: ApiClient;

    public constructor(apiClient: ApiClient) {
        this._apiClient = apiClient;
    }

    public async load(): Promise {

        const data =  await this._apiClient.getCompanyList();
        this._renderData(data);
    }
}

Strong typing during coding allows the environment to make more checks for us during development. When using ECMAScript modules, the compiled code will remain expressive, and close to  the original TypeScript syntax.

Writing Code: Styling

The blog uses CSS in only a simple way, via the Bootstrap library. In particular the built-in styles enable us to use both presentation and grid based layout styles:

<div class='row listRow'>
    <div class='col-2 my-auto text-center'>
        {{name}}
    </div>
    <div class='col-2 my-auto text-center'>
        {{region}}
    </div>
    <div class='col-2 my-auto text-center'>
        <a href='#company={{id}}'>View Transactions</a>
    </div>
    <div class='col-2 my-auto moneycolor fw-bold text-end'>
        {{formattedTargetUsd}}<br/>
    </div>
    <div class='col-2 my-auto moneycolor fw-bold text-end'>
        {{formattedInvestmentUsd}}
    </div>
    <div class='col-2 my-auto fw-bold text-end'>
        {{noInvestors}}
    </div>
</div>

For this blog, I keep SPA styles simple, and the application CSS file is small. containing only padding, colours and minor adjustments to support mobile layouts:

.row
{
    margin-top: 5px;
}
.listRow
{
    height: 80px;
}
.valuecolor
{
    color: blue;
}
.moneycolor
{
    color:green;
}
.largetext
{
    font-size: larger;
}
.errorcolor
{
    color:red;
}
.errorlinecolor
{
    color:darkblue;
}

@media all and (max-width: 992px)
{
    body
    {
        font-size: 0.85em;
    }
}
@media all and (max-width: 768px)
{
    body
    {
        font-size: 0.45em;
    }
}

Executing Tasks

For the best control, this blog’s code examples are run from bash scripts. The scripting allows nuances to be controlled, and is the simplest way to ensure working code on Linux, macOS and Windows:

cd "$(dirname "${BASH_SOURCE[0]}")"

if [ ! -d 'node_modules' ]; then
  npm install
fi

npm run lint

npm start

read -n 1

Building Code: Linting

The first build task is to check code quality. The eslint library is used, which requires its own JSON configuration file containing rules:

{
    "root": true,
    "parser": "@typescript-eslint/parser",
    "plugins": [
        "@typescript-eslint"
    ],
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "rules": {
        "@typescript-eslint/explicit-module-boundary-types": ["error", {
            "allowArgumentsExplicitlyTypedAsAny": true
        }],  
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/no-non-null-assertion": "off",
        "@typescript-eslint/quotes": ["error", "single", { "avoidEscape": true }],
        "quotes": "off",
        "semi": "error",
        "indent": "error",
        "max-len": ["error", { "code": 120 }],
        "brace-style": ["error", "1tbs"],
        "no-multiple-empty-lines": ["error", {"max": 1}],
        "no-trailing-spaces": "error"
    }
}

This provides warnings, mostly about little things, that helps to keep code maintainable and consistent:

/home/gary/dev/oauth.websample1/spa/src/plumbing/oauth/authenticator.ts
   42:1  error  More than 1 blank line not allowed              no-multiple-empty-lines

/home/gary/dev/oauth.websample1/spa/src/views/titleView.ts
  20:6  error  Opening curly brace does not appear on the same line as controlling statement  brace-style

Building Code: SPA Bundles

For the SPA, a build phase is essential, for the following reasons:

Reason Description
Browser Support Ensure that JavaScript code is produced, for execution by browsers
Web Performance Reducing code size so that bundle sizes stay small, and the SPA performs well

The webpack tool is used, but I aim to keep usage simple. It only does one  job, to produce the JavaScript bundles expressed in the final index.html file:

<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <base href='/spa/' />
        <title>OAuth Demo App</title>

        <link rel='stylesheet' href='css/bootstrap.min.css'>
        <link rel='stylesheet' href='css/app.css'>
    </head>
    <body>
        <div id='root' class='container' />
        
        <script type='module' src='dist/vendor.bundle.js'></script>
        <script type='module' src='dist/app.bundle.js'></script>
    </body>
</html>

Building Code: Webpack Configuration

Webpack requires a configuration and the syntax can seem a little cryptic at first. Mostly though we just need to specify the SPA’s entry point source file, then provide an output folder for compiled bundles:

import path from 'path';

const dirname = process.cwd();
export default {
  
  context: path.resolve(dirname, './src'),
  target: ['web'],
  devtool: 'source-map',

  entry: {
    app: ['./app/app.ts']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    
    extensions: ['.ts', '.js']
  },
  output: {
    
    path: path.resolve(dirname, './dist'),
    filename: '[name].bundle.js'
  },
  optimization: {

    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          name: 'vendor',
          test: /node_modules/,
          enforce: true
        },
      }
    }
  }
}

Webpack has its own rules for resolving imports, and we also have to tell it to include extensions from both the SPA’s TypeScript code, and JavaScript code consumed from the node_modules folder.

Building Code: Processing TypeScript

The SPA’s own code is built to an app.bundle.js file. When compiling the SPA, the ts-loader library uses the settings from the tsconfig.json file. A second, larger bundle is also built, called vendor.bundle.js, containing third party code from the node_modules folder.

Building Code: Web Compilation

The end result of the build phase is that the following files are output to the dist folder. The source map files are useful for stepping through code in a debugger, or diagnosing exception stack traces:

vendor.bundle.js
vendor.bundle.js.map
app.bundle.js
app.bundle.js.map

For SPAs, we need to ensure that output works in older browsers, but we also want to produce simple and clean production JavaScript code, without the need for excessive polyfilling. The following tsconfig.json output settings are used:

Setting Description
target Building to ES2017 means built code uses async await syntax
module ES2015 means built code uses ECMAScript modules

Since we build to ES2017 JavaScript, the development build results remain close to the SPA’s TypeScript code, with types removed:

class CompaniesView {
    constructor(apiClient) {
        this._apiClient = apiClient;
    }
    async load() {
        const data = await this._apiClient.getCompanyList();
        this._renderData(data);
    }
}

All main mobile and desktop browsers have supported these features since around 2017, as shown by online compatibility sites.

Building Code: API Compilation

During development of TypeScript APIs, the simplest option is to avoid producing JavaScript files, since doing so adds confusion when searching the code base.

Instead, Node.js can be asked to execute TypeScript files directly using the tsx tool. This results in the node runtime using ECMAScript modules built in-memory:

tsx watch src/host/startup/app.ts

Deployed builds for Node.js APIs should instead include and run JavaScript code, which also ensures best startup times. The initial code sample uses an ‘npm run buildRelease‘ command to output to the dist folder. The most up to date Node.js 18 options can be used for the API build.

I avoid building API code to bundles. By keeping built code easier to read, you are better able to diagnose potential production problems, and understand exception stack traces.

Executing Code: Recompiling after Code Changes

The SPA code recompiles automatically when it is edited, via webpack’s ‘watch‘ parameter:

For the API, the same behaviour is enabled by using the watch option of tsx.

Decompiling Code: Debugging the API

In order to step through code in the initial code sample’s API, first open the root level workspace file. The ‘api/.vscode/launch.json’ file enables the API to be debugged from the ‘Run and Debug‘ tab of Visual Studio Code:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch API",
            "runtimeArgs": ["--import", "tsx"],
            "args": ["src/host/startup/app.ts"],
            "outputCapture": "std",
            "skipFiles": [
                "<node_internals>/**"
            ]
        }
    ]
}

This enables you to step through TypeScript code and inspect the state of variables:

Decompiling Code: Debugging the SPA

There is a similar file at ‘spa/.vcscode/launch.json’. This is a little more complex since it needs to reference source maps, which webpack has built to the dist folder:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch SPA",
            "url": "http://web.mycompany.com/spa/",
            "webRoot": "${workspaceRoot}/src/*",
            "sourceMaps": true,
            "sourceMapPathOverrides":{
                "webpack:///./*": "${workspaceRoot}/dist/*"
            }
        }
    ]
}

You can then step through the actual lines of TypeScript code, while the browser executes the corresponding JavaScript code:

Decompiling Code: SPA Exception Stack Traces

The Webpack configuration also has separate development and production configurations, to complete the setup. The first of these is shown below. It contains a property needed for code debugging to work, and also a property to determine whether to render stack traces:

import webpack from 'webpack';
import {merge} from 'webpack-merge';
import baseConfig from './webpack.config.base.mjs';

export default merge(baseConfig, {

  mode: 'development',
  devtool: 'source-map',
  
  output: Object.assign({}, baseConfig.output, {
    devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]'
  }),
  
  plugins:[
    new webpack.DefinePlugin({
      SHOW_STACK_TRACE: 'true',
    })
  ]
});

To prevent TypeScript compilation errors, this variable is declared in the SPA’s typings.d.ts file, after which it can be used in the SPA code:

public static getErrorStack(error: UIError): ErrorLine | null {

    if (SHOW_STACK_TRACE) {
        if (error.stack) {
            return ErrorFormatter._createErrorLine('Stack', error.stack);
        }
    }

    return null;

The output is intentionally then rendered in a raw format from which it can be copied. In a production build you could use a Send Home feature rather than displaying the technical details:

Tools such as https://sourcemaps.info can then be used to find the original source location from source map files. Paste the stack trace into the left hand pane and the source map data into the small edit box.

The right pane then displays an updated stack trace based on the original TypeScript lines of code. In the following example output we can see that the failure occurred at line 55 of the ApiClient source file:

Error: simulating an exception at ApiClient._callApi (webpack:///./api/client/apiClient.ts:55:18) at async ApiClient.getCompanyList (webpack:///./api/client/apiClient.ts:31:15) at async CompaniesView.load (webpack:///./views/companiesView.ts:25:25) at async Router.loadView (webpack:///./views/router.ts:43:12) at async App._loadMainView (webpack:///./app/app.ts:118:8) at async App.execute (webpack:///./app/app.ts:51:12)

Web Deployment

The web pipeline steps for a development computer are now complete. Later in this blog we will apply some finishing touches to the SPA pipeline, by deploying the SPA to a content delivery network.

Electron Desktop Apps

In the native apps section of this blog we will implement cross-platform OAuth secured desktop apps, using Electron. This can also be built to ECMAScript modules, though doing so is a little tricky to figure out. The Desktop Coding Key Points post explains a way to get it working.

Where Are We?

We have enabled an initial setup that is modern and productive, and have also kept the web technology simple and understandable. Later, we will follow the same steps for the Final SPA, which is developed in React.

Next Steps