Node.js, TypeScript & building to different folders

This page was originally published here ▶️ Node.js, TypeScript & building to different folders .

I’m working on another post that explains the details of how VS Code handles debugging and how sourcemaps work for Node.js projects. In that post I explain how I set up my project so that I can debug a Node.js project written in TypeScript using VS Code, either by running the project, attaching to an existing process on my laptop or attaching to the Node.js process running in a local Docker container.

But before sharing that post, I wanted to have something that I could reference which explains how I set up my Node.js projects. I wasn’t planning to write this, but in the last few months I’ve had a few people ask how I do this so I figured this would be the easier approach: something to reference.

Keep in mind that this is how I do it… not how I say you should do it. Like so many other similar like this, use this to get some ideas or how to figure out your project structure & workflow.

Andrew Connell
Andrew Connell
Developer & Chief Course Artisan, Voitanos LLC. | Microsoft MVP

The Scenario

I like to do a lot of my server-side dev in Node.js for multiple reasons that are beyond the scope of this post. While Node.js is all JavaScript, I prefer to write everything in TypeScript. When I say everything, I mean everything, including gulp which I use to handle a bunch of my build tasks. So I have a project filled with TypeScript, no JavaScript, and two types of files to build: project/build files & app source files.

This presents a bit of a challenge as I need two different transpilation configurations: one for transpiling the build files, or those files that are used in the management, testing and building of the application, and another for transpiling the project files.

Building Project Files

Almost all of my build assets are in a folder /build. This includes gulp tasks project configuration settings and TypeScript type definitions. Here’s a sample of what that looks like (I’ve removed a lot of stuff that’s not relevant to the project like the source, or other stuff that doesn’t relate to this discussion)

├── build
│   ├── config
│   │   ├── build.ts
│   │   ├── index.ts
│   │   ├── projectArtifact.ts
│   │   └── projectPaths.ts
│   ├── gulp
│   │   ├── tasks
│   │   │   ├── build.ts
│   │   │   ├── clean.ts
│   │   │   ├── live-dev.ts
│   │   │   ├── test.ts
│   │   │   ├── update-changelog.ts
│   │   │   └── vet.ts
│   │   ├── BaseGulpTask.ts
│   │   ├── buildBarrel.ts
│   │   ├── gulp.d.ts
│   │   ├── gulpPlugins.ts
│   │   └── utils.ts
│   ├── global.d.ts
│   └── wallaby.d.ts
├── gulpfile.ts
├── package.json
├── tsconfig.build.json
├── tsconfig.json
├── tslint.json
├── typings.json
└── wallaby.conf.ts

Let me explain a few things while referencing the above structure:

  • config: this folder contains constants that are used by various build tasks, such as lists of where all TypeScript & JavaScript is in the project, where tests are, what folders are dynamically built that can be cleaned out (ie: /logs, /dist & /reports)
  • gulp: this contains a folder, tasks, that contains a separate class representing each gulp task used within the project. I wrote about how I do this in my post Dynamically Loading Gulp Tasks For Simplified Reuse and Maintenance . This folder also contains two barrels:
  • buildBarrel.ts: Exports the config and utility assets for easy reuse elsewhere in the codebase
  • gulpPlugins.ts: Barrel of all gulp plugins I’m using, easily exported for use elsewhere. This makes using plugins nice and clean, with IntelliSense, in the different tasks:
import {
  gulpIf,
  gulpPlumber,
  gulpPrint,
  gulpSourcemaps,
  gulpTypeScript
} from '../gulpPlugins';

For instance, here’s what one barrel looks like for a project I’m working on:

let gulpConvChangeLog: any = require('gulp-conventional-changelog');
let gulpEnv: any = require('gulp-env');
import * as gulpIf from 'gulp-if';
import * as gulpIstanbul from 'gulp-istanbul';
import * as gulpMocha from 'gulp-mocha';
import * as gulpPlumber from 'gulp-plumber';
let gulpPrint: any = require('gulp-print');
let gulpRimraf: any = require('gulp-rimraf');
import * as gulpSourcemaps from 'gulp-sourcemaps';
import * as gulpTslint from 'gulp-tslint';
import * as gulpTypeScript from 'gulp-typescript';
import * as gulpUtil from 'gulp-util';

export { gulpConvChangeLog,
         gulpEnv,
         gulpIf,
         gulpIstanbul,
         gulpMocha,
         gulpPlumber,
         gulpPrint,
         gulpRimraf,
         gulpTslint,
         gulpTypeScript,
         gulpSourcemaps,
         gulpUtil
};
  • *.d.ts: Various TypeScript type definitions that I use internally throughout the project. This can include type definitions that don’t exist that I wanted to create making my developer experience better (ie: wallaby.d.ts).
  • tsconfig.*.json: These are my TypeScript project files… I’ll explain why I need two of these in a moment.
  • wallaby.conf.ts: If you haven’t checked out WallabyJS , you really should. This is my project configuration.

One of the first things I have to do is build my project files, or files that help me work with the rest of the project. This is because as you can see above, all my gulp tasks and configuration files for things like WallabyJS are written in TypeScript… which won’t run. I need to transpile these to JavaScript. But it’s not that simple. See, I like to build my applications to a different folder from /src, such as /dist. This presents a challenge because now I have one part of my codebase that needs to build to /dist and another part where it does matter (the build stuff) since it won’t get distributed.

I created a separate TypeScript project configuration file, tsconfig.build.json, that excludes the folders I don’t want TypeScript to compile with all the compiler settings I want on it:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "inlineSourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  },
  "exclude": [
    "node_modules",
    "src"
  ]
}

To transpile the build stuff, I have a npm script called build:project that I use to download all type definitions, compile the project related TypeScript and run gulp to show the available tasks. The script from package.json looks like this:

"build:project": "typings install && tsc -p tsconfig.build.json && gulp"

So typically, right after running npm install, another npm script (install) runs this using npm run build:project.

At this point, my project build artifacts are now in JavaScript so I can run gulp like normal and get ready to build my project.

Building Application Files

The whole reason you’ve created a project is to build an app, right? So far I’ve explained how my project set up is done… the next step is to see how to handle the source files for the actual application. My entire application resides in the directory tree within a /src folder.

Building the application is a bit different from building the project files. As I mention above, I build all the project related TypeScript files (gulp tasks, utilities, etc) side-by-side to their generated JavaScript files. But for the application, I want the output to go to the /dist folder. To do this, I programmatically control the exclusions for the TypeScript project. Within my gulp task build, I first load the tsconfig.json file and then modify all the exclusions like so:

let tsProjectApp: gulpTypeScript.Project = gulpTypeScript.createProject('tsconfig.json');
// change default excludes
//  > default excludes are for build TS files... excludes /src
//  > need the inverse so exlude the TS build files & include /src, /typings /etc
tsProjectApp.config.exclude = [
  'gulpfile.ts',
  'wallaby.conf.ts',
  ProjectPaths.Build,
  ProjectPaths.NodeModules,
  ProjectPaths.Output,
  ProjectPaths.Reports
];

Notice here that I’m excluding two TypeScript files from the root of the project that are not part of the application, rather they are project management files. I then exclude a bunch of directories like where all my build utilities are, the node modules folder, where files are being build (output) and reports generated like test results and code coverage reports.

One extra set up I do run into is for things like websites that have static artifacts like CSS, images or views. For these, whenever I do a build, I also copy over these assets from the /src to the /dist folder. Typically this is just another gulp task that is set as a dependency on my build task so it’s always run without having to remember to run it when building the application.

Conclusion

In this post I shared how I like to set up my projects. You may look at this and think “dang that seems complicated” or “seems like a lot of project related stuff that’s duplicated in each project”. You’re right… there is a bunch of duplicated stuff. Adding it to each project isn’t hard as I set up my own custom Yeoman generator that I run to start a new project or to add things to an existing project (like additional gulp tasks).

I also like how I can modify the build process for each project if one is a bit different than others.

But I do admit that I’m looking for a way to make things a bit more reusable. I’d like to have an npm package that import as a dev dependency without having to dupe everything. It won’t handle everything, but it should handle a bunch of stuff. Maybe the way my gulpfile.ts loads all the tasks dynamically, maybe some common tasks and configuration stuff is in that shared package. Not sure… but for now, this works.

Andrew Connell
Developer & Chief Course Artisan, Voitanos LLC. | Microsoft MVP
Written by Andrew Connell

Andrew Connell is a web & cloud developer with a focus on Microsoft Azure & Microsoft 365. He’s received Microsoft’s MVP award every year since 2005 and has helped thousands of developers through the various courses he’s authored & taught. Andrew’s the founder of Voitanos and is dedicated to helping you be the best Microsoft 365 web & cloud developer. He lives with his wife & two kids in Florida.

Share & Comment