express-typescript-compile

Express.js on the fly typescript compilation middleware. Full documentation.

Usage no npm install needed!

<script type="module">
  import expressTypescriptCompile from 'https://cdn.skypack.dev/express-typescript-compile';
</script>

README

express-typescript-compile

Express.js on the fly typescript compilation middleware. Full documentation.

Introduction

The main purpose of this package is to give ability for super simple, cheep and fast typescript code running, both, on the server and client side, without need of special configuration of project, like configuration for the bundler, compiler, without need of tons of dependencies.

This package delivers express.js middleware which on the fly, during the request, transpile typescript source file to the javascript code. Such transpiled source is cached and retranspiled only after the changes. As transpilation is not cheep, such approach should be only use within the development workflow, or for prototyping or demoing. On side, you can have the production configuration, with the compiler, bundler, linter ect.

Features

  • typescript/js code on the fly transpilation
  • minimal configuration
  • simple memory caching
  • tsconfig paths support
  • dynamic imports import('abc') support
  • support common.js modules imports by aliasing or/and transformers
  • support json modules imports
  • support css imports
  • no impact on the code, and development workflow
  • rich configuration

Installation

npm install express-typescript-compile

Usage

You can find bunch examples here.

Example minimal setup

This is an example of minimal runnable example of express-typescript-compile usage. In this example we are running the server typescript code trough ts-node-dev, and client code is on the fly compiled by the express-typescript-compile.

├── src
│   ├── index-client.ts    // client side entrypoint
│   └── index-server.ts    // server entry point
├── index.html             // client entry html page
├── package.json           // project package.json
└── tsconfig.json          // tsconfig
// src/index-client.ts
console.log('Hello word');
export {}
// index.html
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<!-- Import of typescript module -->
<script type="module" src="/src/index-client.ts"></script>
</body>
</html>
// src/index-server.ts
import express from 'express';
import { typescriptCompileMiddleware } from 'express-typescript-compile';
const app = express();
// dev - on the fly compilation
app.use(typescriptCompileMiddleware());
// index.html
app.get('/', (req, res) => res.sendFile(process.cwd() + '/index.html'));
// server start
app.listen(3000);
// tsconfig.json
{
    "compilerOptions": {
        // Required just because of 'express' import 
        "esModuleInterop": true,
        // As we are transpiling file by file, we have to use isolatedModules option
        "isolatedModules": true
    }
}
// package.json
{
   "scripts": {
      // runing server code by the ts-node-dev (or ts-node)
      "serve": "ts-node-dev -- src/index-server.ts"
   },
   "dependencies": {
      "express": "^4.17.1",
      "tslib": "^2.0.3"
   },
   "devDependencies": {
      "@types/express": "^4.17.9",
      "typescript": "^4.1.3",
      "express-typescript-compile": "0.1.0",
      "ts-node-dev": "^1.1.1"
   }
}

For more information please go to Api Reference.

Live reload

In order to use live page reload technique please use easy-livereload package like:

import liveReload from 'easy-livereload';
...
app.use(liveReload({
   watchDirs: ['public', 'src/app'].map(i => join(process.cwd(), i)),
   checkFunc: () => true,
}));

Import non typescript/es6 modules

This package by self is resolving only the imports to your source typescript modules, and to installed packages which are exports the es6 modules, as such modules are fully supported by the modern browsers. You still are able to import the commonjs, json, css, ... but you will need to handle that by self, please look at the fallowing list of recipes for more details:

Common.js modules

If your code is tries to import the common.js modules which are coming from the package which is not offers the es modules you have options.

Use import-maps

The modern browsers supports the imports-maps.

// src/index-server.ts
...
// dev - on the fly compilation
app.use(typescriptCompileMiddleware({
   resolve: {
       // marking libs as externals
       externals: ['react', 'react-dom' ]
   }
}));
...
// src/index-client.tsx
import { createElement } from 'react';
import { render } from 'react-dom';
const root = document.createElement('div');
document.body.append(root)
render(<div>Hello word</div>, root);
<!DOCTYPE html>
<html lang="en">
<head>
   <!-- Import maps -->
   <script type="importmap">
        {
            "imports": {
                "react": "https://cdn.skypack.dev/react", // can be also /node_modules/react/...
                "react-dom": "https://cdn.skypack.dev/react-dom"
            }
        }
    </script>
</head>
<body>
    <!-- Just import the tsx module -->
    <script type="module" src="/src/index-client.tsx"></script>
</body>
</html>

Aliasing

typescriptCompileMiddleware provides the way for configuring the aliases. Such aliases can map imports to the local files, but also to the public url, eg. to cdn. As we have skypack.dev we can use it to import es version of the common.js packages

// src/index-server.ts
...
// dev - on the fly compilation
app.use(typescriptCompileMiddleware({
   resolve: {
      alias: {
          // we are mapping all import ... from 'react' to import ... from 'https://cdn.skypack.dev/react' 
         'react': 'https://cdn.skypack.dev/react',
         // we are mapping all import ... from 'react-dom' to import ... from 'https://cdn.skypack.dev/react-dom' 
         'react-dom': 'https://cdn.skypack.dev/react-dom'
      }
   }
}));
...
// src/index-client.tsx
import { createElement } from 'react';
import { render } from 'react-dom';
const root = document.createElement('div');
document.body.append(root)
render(<div>Hello word</div>, root);
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
    <!-- Just import the tsx module -->
    <script type="module" src="/src/index-client.tsx"></script>
</body>
</html>

Transform common.js to es modules

typescriptCompileMiddleware provides the way for configuring custom transformers. There is package called cjstoesm which is a transformer of common.js to es modules, and it is compatible with typescriptCompileMiddleware. Unfortunately cjstoesm has some limitation, please read documentation for better info.

// src/index-server.ts
import { cjsToEsmTransformerFactory } from "cjstoesm";

// dev - on the fly compilation
app.use(typescriptCompileMiddleware({
   compile: {
      // adding the common.js to es modules transform
      transformers: [ cjsToEsmTransformerFactory() ]
   }
}));

Mixing both techniques

Transforming solution will not always solve the problem, some libraries (eg React) can't be handled properly by the cjstoesm as the code contains the conditional imports/exports. If we can't or do not want to use skypack.dev cdn, we can try to mix transformers with aliases, which will firstly map imports to file which are easy to handle by the cjstoesm, and then use cjstoesm transform for converts that files from common.js in to es modules.

// src/index-server.ts
import { cjsToEsmTransformerFactory } from "cjstoesm";

// dev - on the fly compilation
app.use(typescriptCompileMiddleware({
   resolve: {
      alias: {
         // forcing import of production bundles as cjstoesm is not 
         // able to handle development versions  
         'react': 'react/cjs/react.production.min.js',
         'react-dom': 'react-dom/cjs/react-dom.production.min.js',
         'scheduler': 'scheduler/cjs/scheduler.production.min.js'
      }
   },
   compile: {
      // adding the common.js to es modules transform
      transformers: [ cjsToEsmTransformerFactory() ]
   }
}));

Json

To import json modules directly to your typescript module you have to

  1. Enable resolveJsonModule option in your tsconfig.json
  2. Deliver the express middleware which will transform the json files in to the es6 modules on the fly, you can use package called json-es6-loader:
     import jsonEs from 'json-es6-loader';
     // serving json as es6 modules
     app.get('*.json', (req, res, next) => {
         // checking is a module import request
         if (req.moduleImport) {
             // set proper content type
             res.contentType('application/javascript');
             // serve transformed json
             res.send(
                 // transform json to es6 module
                 jsonEs(
                     // simple reading the file by the provided request path
                     readFileSync(join(cwd, req.path)).toString('utf-8')));
             return;
         }
         next();
     });
    

Css

To import css files directly from your typescript source code you will have to

  1. To preserve compilation error we will create the typescript global declaration like global.d.ts:
    declare module "*.css";
    
  2. As the express-typescript-compile middleware will leave imports to the *.css within es6 modules, we will need to serve es6 modules which are represents *.css files. So we will add an express middleware:
    // so for all requests for the css files
    app.get('*.css', (req, res, next) => {
        // if the request is the es6 module import
        if (req.moduleImport) {
            // we will send proper content type
            res.contentType('application/javascript');
            // and simple js code which will create the link element
            // to the same path as was requested
            // but that time the link will be to plain css, not to es6 module
            res.send(`
                const link = document.createElement('link');
                link.setAttribute('href', '${req.path}');
                link.setAttribute('rel', 'stylesheet');
                document.body.append(link);
            `);
            return;
        }
        next();
    });
    
  3. Last think which we have to do is to serve regular *.css file, so we can add simple static files middleware:
     app.use(express.static('.'));
    

How it works

  1. the middleware on the *.ts, *.tsx files requests (eg. http://localhost:3000/src/index.ts) is
  2. looking in cache and working dir for the file by the request.path (and __mi_ctx query parameter) (eg. ./src/index.ts)
  3. transpile file to es module (respecting tsconfig options) by the typescript package
  4. resolving all imports from the file by the aliases/typescript/enhanced-resolver
  5. replacing all imports by resolved value and mark them by special (__mi) query parameter (eg. import { app } from '/src/app/index.ts?__mi=true,
    import { crateElement } from '/node_modules/react/cjs/react.production.min.js?__mi=true)
  6. if import is resolved to upper dir the additional (__mi_ctx) query param is added
    (eg. import { crateElement } from '/node_modules/react/cjs/react.production.min.js?__mi=true&__mi_ctx=../../)
  7. caching file
  8. on the next request to marked resource > go to 2.