Express.js on the fly typescript compilation middleware. Full documentation.
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.
- 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
npm install express-typescript-compile
You can find bunch examples here.
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.
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,
}));
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:
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.
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>
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>
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() ]
}
}));
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() ]
}
}));
To import json modules directly to your typescript module you have to
- Enable
resolveJsonModule
option in your tsconfig.json - 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(); });
To import css files directly from your typescript source code you will have to
- To preserve compilation error we will create the typescript global declaration like
global.d.ts
:declare module "*.css";
- 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(); });
- 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('.'));
- the middleware on the *.ts, *.tsx files requests (eg.
http://localhost:3000/src/index.ts
) is - looking in cache and working dir for the file by the
request.path
(and__mi_ctx
query parameter) (eg../src/index.ts
) - transpile file to es module (respecting tsconfig options) by the
typescript
package - resolving all imports from the file by the aliases/typescript/enhanced-resolver
- 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
) - 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=../../
) - caching file
- on the next request to marked resource > go to 2.