4

I am trying to make a library that works both in the browser as well as in node.

I have three json config files where the latter two extend tsconfig.json

  • tsconfig.json (just contains files for the build)
  • tsconfig.browser.json
  • tsconfig.node.json

tsconfig.browser.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "target": "es6",
    "module": "system",
    "outFile": "../dist/browser/libjs.js",
    "removeComments": true,
    "declaration": true
  }
}

tsconfig.node.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": "../dist/node",
    "removeComments": true,
    "declaration": true,
    "declarationDir": "../dist/node/typings"
  },
  "files": [
    "./index"
  ]
}

I have this index.ts file (only included on the node build):

export { collect } from './components/collections'
export { query } from './components/query'

Then I have this in the collections.ts file:

export namespace libjs {
  export function collect<T>(data: T[]) {
    // Do some stuff
  }
  export class collection(){}
}

And this query.ts file:

export namespace libjs {
  export class query<T> {
    private _results: collection<T>
  }
}

The issue I am having, is when I try to build to node, the index file cannot find the collect function, and when I build to the browser the query class cannot find the collection class. What is the best way to code this so I can build to both node and the browser? If I remove the export on the namespace I can build to the browser fine, but I cannot build to node.

The way I would like to use these are as follows:

Nodejs

const libjs = require('libjs')
let c = libjs.collect([1, 123, 123, 1231, 32, 4])

Browser

<script src="/js/libjs.js"></script>
<script>
    let c = libjs.collect([1, 123, 123, 1231, 32, 4])
</script>
Get Off My Lawn
  • 34,175
  • 38
  • 176
  • 338

1 Answers1

2

When you compile files that have export ... at the top level, each file is treated as a module with its own scope, and namespace libjs in each file is distinct and separate from libjs in every other file.

If you want to generate a single script that can be used in a browser without module loader (defining libjs as global), you have to remove all toplevel exports, and don't set module at all in tsconfig:

components/collections.ts

namespace libjs {
  export function collect<T>(data: T[]) {
    // Do some stuff
  }
  export class collection<T>{}
}

components/query.ts

namespace libjs {
  export class query<T> {
    private _results: collection<T>
  }
}

Now, you can use the same generated script in node too, if you add code that detects node environment at runtime and assigns libjs to module.exports:

index.ts

namespace libjs {
    declare var module: any;
    if (typeof module !== "undefined" && module.exports) {
        module.exports = libjs;
    }
}

single tsconfig.json for browser and node (note that I changed output to dist from ../dist)

{
  "compilerOptions": {
    "outFile": "./dist/libjs.js",
    "removeComments": true,
    "declaration": true
  },
  "files": [

    "components/collections.ts",
    "components/query.ts",
    "index.ts"

  ]
}

You can use generated script right away in node in javascript:

test-js.js

const lib = require('./dist/libjs')

console.log(typeof lib.collect);
let c = lib.collect([1, 123, 123, 1231, 32, 4])

Unfortunately, you can't use it in node in typescript with generated libjs.d.ts because it declares libjs as global. For node, you need separate libjs.d.ts that contains one additional export = libjs statement that you have to add manually or as part of build process:

complete dist/libjs.d.ts for node

declare namespace libjs {
    function collect<T>(data: T[]): void;
    class collection<T> {
    }
}
declare namespace libjs {
    class query<T> {
        private _results;
    }
}
declare namespace libjs {
}

// this line needs to be added manually after compilation
export = libjs;

test-ts.ts

import libjs = require('./dist/libjs');
console.log(typeof libjs.collect);
let c = libjs.collect([1, 123, 123, 1231, 32, 4])

tsconfig.test.json

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "removeComments": true
  },
  "files": [
    "./test-ts.ts",
    "./dist/libjs.d.ts"
  ]
}

You can choose to go a completely different route and build a library composed of modules, but for that you have to use module loader in the browser (or build with webpack), and you probably need to read this answer explaining why namespace libjs is totally unnecessary with modules.

artem
  • 46,476
  • 8
  • 74
  • 78
  • This doesn't seem to work, as I get the following error in node: `TypeError: libjs.collect is not a function` – Get Off My Lawn Jun 23 '17 at 14:55
  • Okay, I have got this working, however, when I build to the browser, it wraps in in `System.register("libjs", [], function (exports_1, context_1) {` Then when I try to access it in the browser I get an error saying `System` is not defined. – Get Off My Lawn Jun 23 '17 at 15:44
  • I now realize this answer is completely off-track. If you need everything in a single script without modules in browser, the only way is to remove all toplevel exports. Then for node, you can use the same compiled script, but for that you need to assign `libjs` to `module.exports` dynamically if the script runs in node. I'm going to test that and rewrite the answer. – artem Jun 23 '17 at 16:15
  • Thank you so much! I had to make a change to the index file so it looks like this for it to work the way I want (removed the namespace and added an else): `interface Window { libjs: any } declare var module: any; if (typeof module !== 'undefined' && module.exports) { module.exports = libjs; } else { window.libjs = libjs; }` – Get Off My Lawn Jun 23 '17 at 18:28