Azure Functions + Node.js + TypeScript + Webpack
I’m a big TypeScript fan. If my blog posts and GitHub projects haven’t already made that clear, I’m now putting that on record.
So when I found out that the latest azure-functions-core-tools
now comes with the ability to create a Functions app in TypeScript out of the box, I got excited.
In this post, I’d like to share my experience setting up an Azure Functions App project for Node.js in TypeScript, with Webpack for generating the app bundles.
The setup
You’ll first need to install azure-functions-core-tools
globally. This is not absolutely necessary though, since you can also install it to an empty folder, and execute commands using the npx
helper. That’s what I did, since my primary project was still running on the Azure Functions 1.x runtime, and I didn’t want to change anything at the time.
func init
to start the wizard. This will install the project to the current directory.- Set the worker runtime to
node
. - Set the language to
typescript
.
Congratulations. You have just created an Azure Functions project in TypeScript.
Under the hood, it’s using tsc
to compile the TypeScript files, which gets placed in the dist
folder. The function.json
has been set up to point to the compiled code using the recently added scriptFile
property that tells the Azure Functions runtime where to find the code when it runs the Function app.
That’s it, right? Job well done, right? Er, no.
Paths/Aliases
As my Functions project got larger, I started having to share code across multiple Functions, and so I started using the paths
property in my tsconfig.json
file to handle module aliasing.
But as soon as I replaced ../../common/sharedModule
with common/sharedModule
, my Functions apps stopped working. Unable to find module
, it said.
Turns out, tsc
does not handle module resolution. It does not transform aliases to their corresponding paths, and the TypeScript team has no intention to implement this feature, as described in this old GitHub issue.
Solution?
Webpack All The Things!
You can use other bundlers too, if you want. I just like Webpack, so I’m going to use that for this project.
Firstly, you’ll need Webpack (duh), and ts-loader
to transpile TypeScript code to regular JavaScript.
Next, you’ll need to add a webpack.config.js
to your project.
Here’s a sample configuration file.
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
target: 'node', // IMPORTANT!
entry: {
QueueTrigger: path.resolve(__dirname, './QueueTrigger/index.ts')
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
plugins: [
new TsconfigPathsPlugin()
]
},
output: {
filename: '[name]/index.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs' // IMPORTANT!
}
};
target
Ensure the output target
is set to node
to ensure that the code is compiled to work in a Node.js environment. The default value is web
, and certain libraries, such as Axios, depend on this to determine which version of the code it should load up during compilation, and using the wrong code has some dire consequences, as I have come to discover.
My project depended on Axios, and prior to setting the build target to node
, my Functions app kept throwing this error about XMLHttpRequest
not being defined. This is because Webpack built the project for the web environment where XMLHttpRequest
actually exists, and not for the Node environment, where it doesn’t.
Setting the build target to node
fixed this problem.
libraryTarget
You’ll need to set the output libraryTarget
to commonjs
to ensure the entry point’s default export is the output module’s default export. Without this, you’ll get the following error when you run your Azure Functions app.
[error] Worker was unable to load function QueueTrigger: 'Unable to determine function entry point. If multiple functions are exported, you must indicate the entry point, either by naming it 'run' or 'index', or by naming it explicitly via the 'entryPoint' metadata property.'
tsconfig-paths-webpack-plugin
The next part is optional, but crucial for my situation. I wanted to use aliases, so I can avoid having to resolve module paths using relative paths. To this end, I went with tsconfig-paths-webpack-plugin
to use the aliases I’ve set up in my tsconfig.json
file.
Alternatively, you can also set this up in your webpack.config.js
file like so:
module.exports = {
resolve: {
alias: {
'common': path.resolve(__dirname, './common')
}
},
};
But who would want to do that anyway, right?
Update project NPM scripts
Finally, you will want to update the NPM scripts defined in the project package.json
file, specifically the build
and watch
scripts.
{
"scripts": {
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch"
}
}
So the next time you run npm run start
, it will start up the Azure Functions project, and at the same time run the Webpack watcher.
It should be noted that in order for the changes to be picked up by the runtime, you’ll need to make sure that the dist
folder is added to the watch list. This is done in the host.json
file.
{
"watchDirectories": [
"dist"
]
}
Now every time Webpack rebuilds the code and updates the files in the dist
folder, the Azure Functions runtime will detect this, and proceeds to restart the Functions app.
Deployment
There are a number of ways you could go about deploying your Functions App to Azure, as described in the official Azure Functions documentation, under How-to Guides > Deploy. I went with the Continuous Deployment option from GitHub.
After setting up my deployment configuration, Azure automatically ran the deployment job. Awesome.
Unfortunately, at the end of the deployment process, I found myself with an Azure Functions App that had no jobs.
What gives, Azure?
Turns out that while the documentation states that npm install
will be triggered when a package.json
file is found, Azure does not automatically run the build job. According to their documentation, transpilation is done before the host is initialized and during the deployment process. I suspect that the transpilation process probably failed for the same reason mentioned above.
Solution? NPM scripts.
While I haven’t found a way to manually invoke an NPM command during the deployment process, I do know that NPM has multiple events I can hook into when npm install
is called. So I decided to hook onto the postinstall
event to call my build task.
{
"scripts": {
"postinstall": "npm run build",
"build": "webpack --mode=production"
}
}
This ensures the project is built during deployment, and sure enough, I now see my new Azure Functions listed under the Functions list.
Conclusion
It’s awesome that Azure functions now support TypeScript out of the box. However, while their current implementation is probably good enough for simple apps, they are still missing a few functionalities, most notably aliasing. Fortunately, by utilising a combination of webpack and the NPM script pipeline, I was able to overcome these obstacles.
I hope this helps anyone else facing the same issue.