Creating packages for multiple Typescript projects that rely on a shared local module

I am currently developing a series of VSTS extensions, with each extension being its own independent Node project complete with its own package.json file and node_modules folder. The structure of the folders is as follows:

 - MyExtension
   - package.json // containing all development dependencies
   - tslint.json
   - Tasks
     - tsconfig.json
     - Common
       - common.ts    // contains shared functionality across tasks
       - package.json // includes runtime dependencies for all projects
     - My1stTask
       - package.json // holds production dependencies
       - task.ts      // implementation of the task
     - ...
     - ...
     - My6thTask
       - package.json // holds production dependencies
       - task.ts      // implementation of the task

In order to fully separate VSTS build tasks, I have been duplicating the contents of the Common project into each task and then running tsc to convert them all to JavaScript.

While this method has worked, it requires constant copying of content from Common for testing purposes.

I attempted to use local file references by adding a dependency in each task's package.json to file:../common, which functions during development but doesn't include the common module in the task after generating the extension.

Coming from a C# background rather than Node development, I have searched extensively for a solution that integrates well with vsts-extensions.

  • npm pack does not seem to function as expected, as the extension demands that all files be present.
  • package.json/bundleDependencies shows promise but does not bundle the local file reference.
  • ///<reference path="../common/common.ts"/>
    works nicely for editing but fails to run after building the extension.
  • Project reference with prepend does not work, as build tasks require the commonjs module resolver. Systems and AMD are unable to load modules, and prepend only functions with the latter.

Is there a way to seamlessly make this setup work without resorting to bower or grunt, and simply ensuring that each MyXthTask has a copy of the local common module in their node_modules folder?

Answer №1

Attempting @matt-mccutchen's method proved unsuccessful for me when working with VSTS build tasks as they necessitate the use of commonjs:

"compilerOptions": {
  "module": "commonjs",
  "target": "es6", 

Fortunately, I discovered a workaround that suits my needs.

I created a tsconfig.json file in the Tasks directory to establish my default configurations and incorporate files from the Common library:

{
  "compileOnSave": true,
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "sourceMap": true,
    "strict": false,
    "strictNullChecks": false,
    "removeComments": true
  },
  "files": [
    "./Common/uuidv5.d.ts",
    "./Common/Common.ts"
  ]
}

For each task, I then crafted a separate tsconfig.json setting the output folder to the project's current directory and inheriting configurations from the main tsconfig.json in the Tasks folder:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
      "outDir": "./", 
      "sourceRoot": "./"
  },
  "files": [
      "InstallExtension.ts"
  ]
}

This setup results in:

 - MyExtension
   - package.json // includes dev-dependencies
   - tslint.json
   - Tasks
     - tsconfig.json   // Default including Common
     - Common
       - common.ts     // shared functionality across tasks
       - package.json  // runtime dependencies for Common
       - tsconfig.json // build config for common files, inherits from ..\Task\tsconfig.json
     - My1stTask
       - package.json  // prod-dependencies for the task
       - task.ts       // task implementation
       - tsconfig.json // build config for task, inherits from ..\Task\tsconfig.json
     - ...
     - My6thTask
       - package.json // prod-dependencies
       - task.ts      // task implementation
       - tsconfig.json // build config for task, inherits from ..\Task\tsconfig.json

During task compilation, the following structure is produced:

     - My6thTask
       - Common
         - Common.js   // Compiled common
       - My6thTask
         - task.js     // Compiled task
       - package.json  // prod-dependencies
       - task.ts       // task implementation
       - task.json     // defines task UI
       - tsconfig.json // build configuration for task

Included in task.ts was:

///<reference path="../Common/Common.ts"/>
import * as common from "../Common/Common";

The execution handler in task.json was adjusted to point to the new location:

  "execution": {
    "Node": {
      "target": "InstallExtension/InstallExtension.js", // changed from: InstallExtension.js
      "argumentFormat": ""
    }
  }

These modifications have proven successful :D. Combined with utilizing glob-exec, my clean build time has been reduced to under a minute:

"initdev:npm": "npm install & glob-exec --parallel --foreach \"Tasks/*/tsconfig.json\" -- \"cd {{file.dir}} && npm install\"",
"compile:tasks": "glob-exec \"Tasks/*/tsconfig.json\" -- \"tsc -b {{files.join(' ')}}\"",
"lint:tasks": "glob-exec --parallel --foreach \"Tasks/*/tsconfig.json\" -- \"tslint -p {{file}}\"",

Answer №2

For those looking to streamline their tasks, consider utilizing project references with the prepend option. This method allows you to consolidate your output into a single file, provided that you have a compatible module loader.

If your project requires multiple-file outputs, check out this question for useful TypeScript insights and potential workarounds.

Answer №3

I had trouble with project references, so I decided to use webpack with ts-loader for building my projects.

I organized all the TypeScript code under a single root directory and used a single tsconfig file like this:

ts
 -core
 -plugin1
 -plugin2

With webpack 3.10, it is now possible to have multiple output configurations using a single config file:

const path = require("path");

const commonTsRule = {
  test: /\.tsx?$/,
  use: "ts-loader",
  exclude: /node_modules/,
};

const commonConfig = {
  devtool: "inline-source-map",
  mode: "development",
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
};

module.exports = [
  {
    ...commonConfig,
    entry: `${path.resolve(__dirname, "ts")}/plugin1/index.ts`,
    output: {
      filename: "bundle.js",
      path: path.resolve(__dirname, "build/plugin1/js"),
    },
    module: {
      rules: [
        {
          ...commonTsRule,
          // customize rule if needed
        },
      ],
    },
  },
  {
    ...commonConfig,
    entry: `${path.resolve(__dirname, "ts")}/plugin2/index.ts`,
    output: {
      filename: "bundle.js",
      path: path.resolve(__dirname, "build/plugin2/js"),
    },
    module: {
      rules: [
        {
          ...commonTsRule,
          // customize rule if needed
        },
      ],
    },
  },
];

This setup allows each project to be built from its own entry point to its designated output destination.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Concealing a VueJs component on specific pages

How can I hide certain components (AppBar & NavigationDrawer) on specific routes in my App.vue, such as /login? I tried including the following code in my NavigationDrawer.vue file, but it disables the component on all routes: <v-navigation-drawer ...

The compatibility of return value types between the constructor signature and the call signature interface is not maintained when they are used together

I'm new to TypeScript and I'm struggling to figure out why I'm getting a type error in the code below. Can someone help me identify what's wrong? interface CallOrConstruct { new (value: string): Person (value: number): number } cla ...

The arrangement of checkboxes in RTL is not optimal

In my Laravel project with Bootstrap UI, I have localized the project using the following code: <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ LaravelLocalization::getCurrentLocaleDirection() } ...

Incorporating TypeScript with jQuery for efficient AJAX operations

I recently added jQuery typings to my TypeScript project. I am able to type $.ajax(...) without encountering any compile errors in VS Code. However, when I test it on localhost, I receive an error stating that "$ is not defined." In an attempt to address t ...

Angular - Dividing Functionality into Multiple Modules

I am currently working with two separate modules that have completely different designs. To integrate these modules, I decided to create a new module called "accounts". However, when I include the line import { AppComponent as Account_AppComponent} from &a ...

Verify the occurrence of an element within an array inside of another array

Here is the scenario: const arr1 = [{id: 1},{id: 2}] const arr2 = [{id: 1},{id: 4},{id: 3}] I need to determine if elements in arr2 are present in arr1 or vice versa. This comparison needs to be done for each element in the array. The expected output sho ...

What is the process for filtering out a particular version of an npm package?

Here is my current configuration: "@vue/test-utils": "^1.0.0-beta.25" Can anyone recommend a way to exclude a particular version of this package while still using the caret ^ notation? I specifically need to exclude version 1.0.0-beta.31 as it has cause ...

What is the reason behind the mandatory credentials option for the CredentialsProvider?

When using NextAuth.js with a custom sign in page, some code examples for the credentials provider do not include the credentials option in the CredentialsProvider. According to the documentation (here), the credentials option is meant to automatically "ge ...

Compiling Vue with TypeScript: Troubleshooting common errors

Using Vue Components with Templates Multiple Times in TypeScript I am working on utilizing a component with a template multiple times within another component. The code is split between a .html file and a .ts file. The .html file structure is as follows: ...

Encountering a Connection Timeout Issue with Mssql in Node.js

I am currently developing a node.js console application that interacts with SQL Server 2008. var sql = require('mssql'); var connection = new sql.Connection({ user: 'sa', password: 'password', server: 'localh ...

Encountering problems while trying to install Bower

I'm having trouble with installing bower correctly. Whenever I try, it keeps giving me these errors. Can anyone provide some guidance or assistance? npm ERR! tar.unpack untar error /Users/Jeanwoo/.npm/bower/1.4.1/package.tgz npm ERR! Darwin 14.0.0 np ...

Are the effects of deleting the node_module similar to running the command "npm cache clean --force"?

Is there a distinction that I should be aware of? How can you determine if the node_modules have been updated? ...

Building a dropdown menu component in react native

Looking to implement a dropdown menu in React Native using TypeScript. Any suggestions on how to achieve this for both iOS and Android platforms? Check out this example of a dropdown menu ...

Create an asynchronous method within an object-oriented programming (OOP) class

Presenting my Activity class. export class Activity { _name: string _goIn: boolean constructor(name: string) { this._name = name; this._goIn = false; } isGoIn() { return this._goIn; } setGoIn() { // instructions to asyn ...

Facing a EACCES symlink error while trying to setup expo-cli

I am encountering some difficulties while attempting to install expo-cli using the command npm install -g expo-cli. Despite looking through other similar posts, none of the recommended solutions seem to work for me. I suspect that the issue lies in the way ...

I'm having trouble retrieving my variable within the socketcluster's socket.on function

How can I store the value of msg in the variable sample when sample is not accessible inside the callback function? import { Injectable } from '@angular/core'; import * as socketCluster from 'socketcluster-client'; @Injectable({ pro ...

Can someone please explain how I can extract and display information from a database in separate text boxes using Angular?

Working with two textboxes named AuthorizeRep1Fname and AuthorizeRep1Lname, I am combining them in typescript before storing them as AuthorizeRep1Name in the database. Refer to the image below for the result. This process is used to register and merge the ...

Arranging Angular Array-like Objects

I am in possession of an item: { "200737212": { "style": { "make": { "id": 200001510, "name": "Jeep", "niceName": "jeep" }, "model": { "id": "Jeep_Cherokee", "name": "Cherokee", "nice ...

Does the NPM publish feature copy all files to the NPM server?

After successfully publishing a package by following the official tutorial, I am curious - does NPM actually copy all of my files to their server when I use the command: npm publish I didn't include any other parameters. In comparison, when using B ...

NodeJS function does not pause for the PostgreSQL database call despite using await keyword

I am attempting to recursively insert entries into the database where each entry depends on the previous one (the ID of the previous entry will be the child_id of the next entry). However, I am facing difficulties in getting async/await to work correctly. ...