Importing typescript from external node modules - node.js

I want to split my application into different node modules and have a main module which builds all other modules as well and I want to use typescript with es6 modules.
Here is my planned project structure:
main
node_modules
dep-a
dep-b
framework
interfaces
IComponent.ts
dep-a
components
test.ts
node_modules
framework
index.ts
dep-b
node_modules
framework
I want to be able to define interfaces in framework which can be consumed in dep-a, dep-b and main.
How do I set up this correctly? Can I compile everything from my main-module? Do I need to create different bundles for framework, dep-a, ... and another typing file? What is the best approach for this?
I already set up some test files and folders and used npm link to link the dependencies and webpack to bundle the files and I am always running into issues with files not being found:
error TS2307: Cannot find module 'framework/interfaces/IComponent'
and
Module not found: Error: Cannot resolve 'file' or 'directory' ./components/test

TL;DR generate declarations for the modules using declaration: true in tsconfig.json and specify the file for your generated typings in the typings entry of the package.json file
framework
Use a tsconfig file similar to this:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"noImplicitAny": true,
"removeComments": true,
"outDir": "dist",
...
},
"files": [
...
]
}
The important bit is declaration: true which will generate internal declarations in the dist directory
Assuming there is an index.ts file which (re)exports all the interesting parts of framework, create a package.json file with a main and typings entry pointing to, respectively, the generated js and the generated declaration, i.e.
{
"name": "framework",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
...
}
Commit this module to a git repo, say bitbucket at : "https://myUser#bitbucket.org/myUser/framework.git"
dep-a
in package.json create a dependency to framework
{
"dependencies": {
"framework": "https://myUser#bitbucket.org/myUser/framework.git"
},
}
That is it.
import * from 'framework'
will pull the dependency with the typings, automatically
Obviously, it is possible to do with dep-a what was done with framework i.e. generate the declarations, update package.json and use dep-a as a module with embedded typings in main
note: a file URL will do in package.json/dependencies if you do not want go to via an external git repo

What arrived in TypeScript 1.6 is typings property in package.json module. You can check the relevant issue on GitHub.
So assuming you want to create separate modules ( dep-a, framework ). You can do the following :
main.ts // (1)
package.json // (2)
node_modules/
dep_a/
index.js // (3)
index.d.ts // (4)
package.json // (5)
node_modules/
framework/
index.js // (6)
index.d.ts // (7)
package.json // (8)
So let's see what you have in your files :
//(1) main.ts
import * as depA from "depA";
console.log(depA({ a : true, b : 2 }) === true) // true;
//(2) package.json
{
name: "main",
dependencies: {
"dep_a" : "0.0.1"
}
...
}
For depA
//(3) dep_a/index.js
module.exports = function a(options) { return true; };
//(4) dep_a/index.d.ts;
import * as framework from "framework";
export interface IDepA extends framework.IFramework {
a : boolean
}
export default function a(options: IDepA) : boolean;
//(5) dep_a/package.json
{
name: "dep_a",
dependencies: {
"framework" : "0.0.1"
},
...
typings : "index.d.ts" // < Magic happens here
}
For framework
//(6) dep_a/node_modules/framework/index.js
module.exports = true // we need index.js here, but we will only use definition file
//(7) dep_a/node_modules/framework/index.d.ts;
export interface IFramework {
b : number;
}
//(8) dep_a/node_modules/framework/package.json
{
name: "framework"
...
typings : "index.d.ts"
}
What I don't include in this answer ( for clarity ) is another compilation phase, so you could actually write the modules ( dep_a, framework ) with typescript and then compile them to index.js before you use them.

For a detailed explanation and some background also see: https://medium.com/#mweststrate/how-to-create-strongly-typed-npm-modules-1e1bda23a7f4

Related

Bundle NPM Package so it has different import paths with Vite and Typescript

How can I bundle my NPM package in a way that I can have different import paths for different parts of the package? I have found webpack approaches, but I am using Vite and TS.
My package looks like this:
- src
- atoms
- molecules
- organism
- index.ts (currently simply imports and exports everything)
Now I can use this currently like this
import { Button } from '#mypackage/library'
How can I do it, so I get this outcome:
import { Button } from '#mypackage/library/atom'
Here is the relevant part of my package.json
{
"entry": "src/index.ts",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json",
"./atoms": "./src/atoms/index.ts",
"./molecules": "./src/molecules/index.ts",
"./organisms": "./src/organisms/index.ts",
"./theme": "./src/theme/index.ts"
},
}
Here is my vite.config.ts
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs'],
name: '#workdigtital/component-library-react',
fileName: (format) => `index.${format}.js`
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
},
exports: 'named'
}
}
},
plugins: [react(), dts({ insertTypesEntry: true })],
resolve: {
alias: {
'#': path.resolve(__dirname, './src')
}
}
});
If I currently try an import like this, inside another project (Laravel+React), in which installed the library.
import { ThemeProvider } from '#workdigital/component-library-react/theme';
I get the following run time error (But no Typescript errors, even IntelliSense is working):
Failed to load url /resources/js/theme/ThemeProvider (resolved id: /resources/js/theme/ThemeProvider). Does the file exist?
My resulting Dist folder looks like this:
You can't have TypeScript exports, this simply won't work. An npm package should have only JS exports.
If you want to be able to selectively import different parts of your package, you must transpile them to different files.
rollup can do it, but it is lots of work, as you will have to set up a separate target for each exported file. Normally you use rollup to create a single bundle, this what this tool is made for.
tsc with a tsconfig.json will be a much better choice in your case. It does this by default, you only need to specify the output directory and it will produce a separate file for each source.
There is an excellent guide on the TypeScript site about packaging TypeScript libraries, you should probably start there.

Correctly exporting global.d.ts into an npm package

I want to publish a typescript npm package, with types embedded.
My folder structure is as follows
dist/
[...see below]
src/
global.d.ts
index.ts
otherfile.ts
test/
examples/
For the sake of an example, my global.d.ts file includes typings relevant to the whole project.
My index.d.ts file uses these types, and exports functions.
//global.d.ts
interface Dog {
name: string,
age: number
}
//more types...
//index.ts
import {func1, func2} from './otherfile.ts'
export default function getDogName(dog: Dog): string {
return dog.name
}
export {func1, func2}
I can build this code, and it all runs correctly, so typescript knows about global.d.ts. Yet, when I run tsc (using the below config), it generates files into my dist/ folder, which doesnt include the global.d.ts file. ie: dist only contains index.d.ts and otherfile.d.ts. Shouldn't dist/ also contain global.d.ts? If not - how will people who install my package know the types for Dog?
My config looks like this for build time.
//tsconfig.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "dist/",
"declaration": true
},
// Only point to typings and the start of your source, e.g. `src/index.ts`
"include": [ "src/*"],
"exclude": ["node_modules", "examples", "test"]
}
For reference, the actual repo is here: https://github.com/bhaviksingh/lsystem
Alright :) After much experimentation, I wanted to share what I did.
There are potentially better solutions (would love to find them), but the TLDR is that I changed it from a global declaration file, to a regular TS module, and exported/imported all the types.
So I renamed the global.d.ts file, to just be interfaces.ts, and then exported the type.
//interfaces.ts (renamed from global.d.ts)
export interface Dog {
name: string,
age: number
}
//more types...
I imported this type into index.d.ts and re-exported it
//index.ts
import Dog from "./interfaces"
export {Dog}
TSConfig knows to compile these declarations
//tsconfig.global.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "dist/",
"declaration": true
},
// Only point to typings and the start of your source, e.g. `src/index.ts`
"include": [ "src/*"],
"exclude": ["node_modules", "examples/", "test/"]
}
and my package.json points to the compiled declaration file.
//package.json
{
//...
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
//...
"build": "tsc -p tsconfig.global.json",
},
}
There are probably better solutions out there, potentially including /// <reference path="..." /> tags, but this is what I have for now and it works. When people install my library, they have access to the types in intellisense, and if they want to use the types they can be imported.

CRA + Yarn 2 + jsconfig.json = Can't run unit tests

I have the following configuration in my jsconfig.json file:
{
"compilerOptions": {
"baseUrl": "./src"
},
"include": ["src"]
}
Which lets me do this:
import { App } from 'components'
import * as actions from 'actions/app.actions'
Instead of this:
import { App } from '../components'
import * as actions from '../actions/app.actions'
To get started with unit testing, I've created a simple App.test.jsx in src/components/tests
import { render } from '#testing-library/react'
import { App } from 'components'
it('renders without crashing', () => {
render(<App />)
})
However, when I run yarn test (which is sugar for react-scripts test), it throws with this ugly error:
FAIL src/components/tests/App.test.jsx
● Test suite failed to run
Your application tried to access components, but it isn't declared in your dependencies;
this makes the require call ambiguous and unsound.
Required package: components (via "components")
Required by: C:\Users\Summer\Code\sandbox\src\components\tests\
23714 | enumerable: false
23715 | };
> 23716 | return Object.defineProperties(new Error(message), {
| ^
23717 | code: { ...propertySpec,
23718 | value: code
23719 | },
at internalTools_makeError (.pnp.js:23716:34)
at resolveToUnqualified (.pnp.js:24670:23)
at resolveRequest (.pnp.js:24768:29)
at Object.resolveRequest (.pnp.js:24846:26)
It seems like Jest (or Yarn?) thinks components is a node package, because it's not aware of the absolute imports setting in my jsconfig.json. Is there a way to make it aware? Or do I have to choose between 0% coverage and relative imports?
I've tried entering "moduleNameMapper" under "jest" in my package.json like the documentation explains, but it didn't help. I got the same error + one more after it.
I've also tried changing components in the test file to ../components but then it complains about actions/app.actions which is inside the <App /> component.
Module name mapper config:
/* package.json */
{
/* ... */
"jest": {
"moduleNameMapper": {
"actions(.*)$": "<rootDir>/src/actions$1",
"assets(.*)$": "<rootDir>/src/assets$1",
"components(.*)$": "<rootDir>/src/components$1",
"mocks(.*)$": "<rootDir>/src/mocks$1",
"pages(.*)$": "<rootDir>/src/pages$1",
"reducers(.*)$": "<rootDir>/src/reducers$1",
"scss(.*)$": "<rootDir>/src/scss$1",
"store(.*)$": "<rootDir>/src/store$1",
"themes(.*)$": "<rootDir>/src/themes$1",
"api": "<rootDir>/src/api.js",
}
}
}
This is because Yarn takes control of the resolution pipeline, and thus isn't aware of resolution directives coming from third-party configuration (like moduleNameMapper).
This isn't to say you have no options, though - specifically, the fix here is to avoid moduleNameMapper, and instead leverage the builtin link: dependency protocol. This has other advantages, such as being compatible with all tools (TS, Jest, ESLint, ...) without need to port your aliases to each configuration format.
See also: Why is the link: protocol recommended over aliases for path mapping?

Loading a static file in Angular during build

I'm want to load the contents of a file and inject it as a string in TypeScript at build time. I understand that this code would ordinarily be server code, but what I want is to have a build step that reads the file and injects its contents as a string.
import { readFileSync } from 'fs';
import { Component } from '#angular/core';
#Component({
template: `<pre>${readFileSync('./example.code')}</pre>`
})
export class ExampleComponent { }
Assuming example.code just has "Hello World" I would want this file to be built as:
template: `<pre>"Hello World"</pre>`
I have found babel-plugin-static-fs which I think should allow me to do this, but I was originally using ng (angular-cli) to build the project. I have done ng eject and updated webpack:
module: {
rules: [
/* snip */
{
"test": /\.ts$/,
"use": [
{
loader: "babel-loader",
options: {
plugins: ['babel-plugin-static-fs']
}
},
{
"loader": "#ngtools/webpack"
}, ] } ] }
However, when I run webpack, I still get
Cannot find module 'fs'
If I reverse the order of the loaders, it seems like babael does not like the # used in may annotations such as the #Component above so that loader does not work.
Is there any way to load a file as static content during an Angular project build?
The issue here is actually related to the tsconfig.app.json file that Angular creates and uses for AoT. This is separate from the tsconfig.json used to actually build the project which does load #types/node as expected.
If you've created a project with ng new, you can change tsconfig.app.json:
- "types": [],
+ "types": ["node"],
This will have the AoT compiler use the type definitions from #types/node.

TypeScript AMD compilation and "barrel" modules

I'm trying to set up a Node.js + TypeScript project using Intern for testing. Everything works fine when I compile the project using "commonjs" (which I do for the normal build); and TypeScript is equally happy when compiling for "amd", which is required by Intern. However, when passing the tests with intern-client, it complains about a couple of things:
First, imports from "index.ts" files (so-called "barrel" modules) won't work. My setup is something like this (everything in the same directory):
// index.ts
export { x } from './x'
// x.ts
export function x() {}
// x.test.ts
import { x } from '.' // "Error: Failed to load module ..."
In fact, the generated JavaScript code (for x.test.ts) looks something like this:
define(["require", "exports", "."], function (...) { ... })
And I'm not sure that AMD knows how to handle the ".".
The second issue happens under the same circumstances (TypeScript compiles happily, but intern-client complains). In summary, I get an error when doing:
import jsdom = require('jsdom')
Which I need to transform to:
const jsdom = require('jsdom')
For Intern to be able to deal with it.
Here is the tsconfig.json file I use to compile the tests:
{
"compilerOptions": {
"target": "es6",
"module": "amd",
"moduleResolution": "node",
"sourceMap": true,
"rootDir": "src",
"outDir": "build/tests",
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
And here is my intern.js configuration file, in case it helps:
define({
suites: ['build/tests/**/*.test.js'],
excludeInstrumentation: true,
filterErrorStack: true
})
Edit (2017-05-03)
To help understand the issue, here is an excerpt of the directory tree of the project:
build
tests // The compiled tests will end up here
src
core
utils
x.ts
x.test.ts
// Other files, each containing a function that I would like to unit-test...
intern.js
package.json
tsconfig.json
...
Regarding the first issue, AMD's handling of an import like '.' is different than Node's. While both of them will map '.' to a package, Node uses a default module name of index.js, while AMD uses main.js. To get things working in an AMD loader, you'll need to first define a package for '.', and then tell the AMD loader what default module to use for that package. Given your project layout, you could configure Intern like this:
loaderOptions: {
map: {
// When a module in src/ references 'src/utils', redirect
// it to 'utils'
'src': {
'src/utils': 'utils'
}
},
packages: [
// Define a package 'utils' with files in 'src/utils' that defaults
// to the module index.js
{ name: 'utils', location: 'src/utils', main: 'index.js' }
]
}
Regarding the second issue, its not clear what the problem actually is. Import statements will be transpiled into define dependencies by TypeScript, so Intern should never be seeing them.

Resources