Express.js on the fly typescript compilation middleware.
MIT License
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.
import('abc')
supportnpm 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
resolveJsonModule
option in your tsconfig.jsonjson-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
global.d.ts
:
declare module "*.css";
express-typescript-compile
middleware will leave imports to the *.css
within es6 modules,*.css
// 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();
});
*.css
file, so we can add simple static app.use(express.static('.'));
http://localhost:3000/src/index.ts
) isrequest.path
(and __mi_ctx
query parameter) (eg. ./src/index.ts
)typescript
package__mi
) query parameterimport { app } from '/src/app/index.ts?__mi=true
,import { crateElement } from '/node_modules/react/cjs/react.production.min.js?__mi=true
)__mi_ctx
) query param is addedimport { crateElement } from '/node_modules/react/cjs/react.production.min.js?__mi=true&__mi_ctx=../../
)