74

I'm trying to import package.json in my TypeScript application:

import packageJson from '../package.json';

My tsconfig.json contains the following:

{
  "compilerOptions": {
    "rootDir": "./src/"
    "outDir": "./dist/",
    "baseUrl": ".",
    "resolveJsonModule": true
  }
}

The problem is that when I compile this, I get

error TS6059: File '/path/to/package.json' is not under 'rootDir' '/path/to/app/src/'. 'rootDir' is expected to contain all source files.

I'm not sure I understand the issue, because both ./src/ and /.dist have the same parent .., so TypeScript could just leave alone the import '../package.json' and it would work from either rootDir or outDir.

Anyway, I've tried the following, with unsatisfactory results:

  • remove rootDir - compilation works, but the dist will contain dist/src, which I don't want
  • remove outDir - then src gets polluted with .js files (and .js.map if sourceMap was true)
  • add @ts-ignore - compilation stops the the file that imports ../package.json

What's the workaround for this limitation, to keep generated files in dist, and allow importing from the parent directory of rootDir?

Inigo
  • 12,186
  • 5
  • 41
  • 70
Kousha
  • 32,871
  • 51
  • 172
  • 296

7 Answers7

103

This is possible, and it turns out, not hard.

The reason the solution is not obvious is because Typescript relies on the rootDir to decide the directory structure of the output (see this comment from Typescript's bossman), and only code included in the output or in package dependencies can be imported.

  • If you set rootDir to the root of your project, package.json gets emitted to the root of outDir and can be imported. But then your compiled src files get written to outDir/src.
  • If you set rootDir to src, files in there will compile to the root of outDir. But now the compiler won't have a place to emit package.json, so it issues "an error because the project appears to be misconfigured" (bossman's words).

solution: use separate Typescript sub-projects

Each Typescript project is self-contained, defined by its own tsconfig, with its own rootDir. This lines up with the principle of encapsulation.

You can have multiple projects (e.g. a main and a set of libs) each in their own rootDir and with their own tsconfig. Dependencies between them are declared in the dependent's tsconfig using Typescript Project References.

I admit, the term "projects" is a poor one, as intuitively it refers to the whole shebang, but "modules" and "packages" are already taken. Think of them as subprojects and it will make more sense.

To solve your specific problem, we'll treat the src directory and the root directory containing package.json as separate projects. Each will have its own tsconfig.

  1. Give the src dir its own project.

    ./src/tsconfig.json:

    {
      "compilerOptions": {
        "rootDir": ".",
        "outDir": "../dist/",
        "resolveJsonModule": true
      },
      "references": [      // this is how we declare a dependency from
        { "path": "../" }  // this project to the one at the root dir`
      ]
    }
    
  2. Give the root dir its own project.

    ./tsconfig.json:

    {
      "compilerOptions": {
        "rootDir": ".",
        "outDir": ".",  // if out path for a file is same as its src path, nothing will be emitted
        "resolveJsonModule": true,
        "composite": true  // required on the dependency project for references to work
      },
      "files": [         // by whitelisting the files to include, you avoid the default TS behavior, which
        "package.json"   // will include everything, resulting in `src` being included in both projects (bad)
      ]
    }
    
  3. run tsc --build src and voilà!

    This will build the src project. Because it declares a reference to the root project, it will build that one also, but only if it is out of date. Because the root tsconfig has the same dir as the outDir, tsc will simply do nothing to package.json , the one file it is configured to compile.

this is great for monorepos

  • You can isolate modules/libraries/sub-projects by putting them in their own subdirectory and giving them their own tsconfig.

  • You can manage dependencies explicitly using Project References, as well as modularize the build:

    From the linked doc:

    • you can greatly improve build times

      A long-awaited feature is smart incremental builds for TypeScript projects. In 3.0 you can use the --buildflag with tsc. This is effectively a new entry point for tsc that behaves more like a build orchestrator than a simple compiler.

      Running tsc --build (tsc -b for short) will do the following:

      • Find all referenced projects
      • Detect if they are up-to-date
      • Build out-of-date projects in the correct order

      Don’t worry about ordering the files you pass on the commandline - tsc will re-order them if needed so that dependencies are always built first.

    • enforce logical separation between components

    • organize your code in new and better ways.

It's also very easy:

  1. A root tsconfig for shared options and to build all subprojects with a simple tsc --build command (with --force to build them from scratch)

    src/tsconfig.json

    {
      "compilerOptions": {
        "outDir": ".", // prevents this tsconfig from compiling any files
    
        // we want subprojects to inherit these options:
        "target": "ES2019", 
        "module": "es2020", 
        "strict": true,
        ...
      },
    
      // configure this project to build all of the following:
      "references": [
        { "path": "./common" }
        { "path": "./projectA" }
      ]
    }
    
  2. A "common" library that is prevented from importing from the other subprojects because it has no project references

    src/common/tsconfig.json

    {
      "extends": "../tsconfig.json", //inherit from root tsconfig
    
      // but override these:
      "compilerOptions": {
        "rootDir": ".",
        "outDir": "../../build/common",
        "resolveJsonModule": true,
        "composite": true
      }
    }
    
    
  3. A subproject that can import common because of the declared reference. src/projectA/tsconfig.json

    {
      "extends": "../tsconfig.json", //inherit from root tsconfig
    
      // but override these:
      "compilerOptions": {
        "rootDir": ".",
        "outDir": "../../build/libA",
        "resolveJsonModule": true,
        "composite": true
      },
      "references": [
        { "path": "../common" }
      ]
    }
    
    
Inigo
  • 12,186
  • 5
  • 41
  • 70
  • 1
    @DanDascalescu let me know if I answered you question about monorepos. – Inigo Apr 28 '20 at 01:03
  • Thank you for the excellent answer! The other one was slightly simpler for my use case so I awarded the bounty, but thanks for reminding me about the [monorepo granular access solution with subtrees](https://stackoverflow.com/questions/54408829/granular-access-to-directories-within-monorepo/61273621#61273621) - accepted! – Dan Dascalescu Apr 30 '20 at 08:50
  • @DanDascalescu My pleasure. I need to figure this stuff out for myself anyway. You of course know your needs better than I, but as to "simpler"... consider the advantages that `tsc --build` offers that you'll miss out on. See [the bottom of this answer](https://stackoverflow.com/a/61470720/8910547) (the top is much the same, though the tsconfigs are correct... I missed some details above which I'll fix for future readers). – Inigo Apr 30 '20 at 09:12
  • This breaks missing import and unused import features in vscode for me: https://github.com/microsoft/TypeScript/issues/38605 – Seph Reed May 16 '20 at 04:08
  • @SephReed let's see what they come back with on the Issue report. Type checking on *used* imports work correctly? – Inigo May 16 '20 at 17:38
  • 1
    This is a super helpful and clear answer - it should be included in the Typescript docs, which are not as clear as this. – crimbo Oct 09 '21 at 18:27
  • @crimbo so flattered! thank you! – Inigo Oct 10 '21 at 05:02
  • 2
    Many thanks to you. I've been having issues even finding documentation that hints at these concepts. You've done a great job of making things clear. – Lance Gliser Nov 11 '21 at 02:28
  • @LanceGliser glad it was helpful! I just reread my answer, and was wondering if `2. Give the root dir its own project.` would be less confusing if it read `Give project.json its own project.`? The former is more technically correct, since `tsconfig` applies to the dir tree rooted at its location, but because we've whitelisted only `project.json` with the `files` property, the latter is effectively correct: a project for that one file. Thoughts? – Inigo Nov 11 '21 at 03:45
  • 2
    First, thanks for the great answer! Second, I'm trying to follow your monorepo example with a TypeScript project that has server, client, and common "subprojects" (for now I'm just using common and server). My setup is VERY similar to your example (though each project has its own src folder), but when I run `tsc --build client/tsconfig.json` I get an error like this: "Cannot write file '/build/tsconfig.tsbuildinfo' because it will overwrite '.tsbuildinfo' file generated by referenced project '/common'." Any clues? – FTLPhysicsGuy Feb 14 '22 at 00:32
  • @FTLPhysicsGuy First, Thanks! Second, check out https://github.com/Microsoft/TypeScript/issues/30925#issuecomment-491026990. Seems you'll have to tweak your config to adjust where `tsbuildinfo` will go for each of your subprojects, since by default they all go to the same location. `tsbuildinfo`, btw, is where ts keeps incremental compile info, which is why there is one per subproject. Let me know how it goes! – Inigo Feb 14 '22 at 05:28
  • 1
    @Inigo Thanks for the link -- that's just what I was looking for. As my friends and I say, my google-fu was weak. As it turns out, I ended up re-arranging my project to use a single src directory with projects under it (matching your example) and the problem, of course, went away. It's nice to know there's a way to go back to the original structure if I wanted. Problem solved! – FTLPhysicsGuy Feb 14 '22 at 13:52
  • @FTLPhysicsGuy My current monorepo project doesn't use a `src` dir at all. Each component's non-test source is in its own directory at the root of the repo, with all tests under a root `test` dir. I think that works because all my tests are written in JS, not TS (intentional). – Inigo Feb 14 '22 at 20:16
  • 1
    Just wanted to express my appreciation for this answer. Superb contribution, thanks! – bombillazo May 19 '22 at 01:36
29

We can set resolveJsonModule to false and declare a module for *.json inside typings.d.ts which will require JSON files as modules and it will generate files without any directory structure inside the dist directory.

Monorepo directory structure

monorepo\
├─ app\
│  ├─ src\
│  │  └─ index.ts
│  ├─ package.json
│  ├─ tsconfig.json
│  └─ typings.d.ts
└─ lib\
   └─ package.json

app/typings.d.ts

declare module "*.json";

app/src/index.ts

// Import from app/package.json
import appPackageJson from '../package.json';

// Import from lib/package.json
import libPackageJson from '../../lib/package.json';

export function run(): void {
  console.log(`App name "${appPackageJson.name}" with version ${appPackageJson.version}`);
  console.log(`Lib name "${libPackageJson.name}" with version ${libPackageJson.version}`);  
}

run();

app/package.json contents

{
  "name": "my-app",
  "version": "0.0.1",
  ...
}

lib/package.json contents

{
  "name": "my-lib",
  "version": "1.0.1",
  ...
}

Now if we compile the project using tsc, we'll get the following dist directory structure:

app\
└─ dist\
   ├─ index.d.ts
   └─ index.js

And if we run it using node ./dist, we'll get the output from both app and lib package.json information:

$ node ./dist
App name "my-app" with version 0.0.1
Lib name "my-lib" with version 1.0.1

You can find the project repository here: https://github.com/clytras/typescript-monorepo

henriquehbr
  • 1,063
  • 4
  • 20
  • 41
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
  • 1
    upvoted. between [mine](https://stackoverflow.com/a/61467483/8910547) and yours we have two legit solutions :) – Inigo Apr 27 '20 at 20:09
  • 1
    Simplest answer, so I'll award it the bounty. Thanks! The only, very minor, issue I see is a warning for `value` in `typings.d.ts`: `A default export can only be used in an ECMAScript-style module`. – Dan Dascalescu Apr 30 '20 at 08:48
  • @DanDascalescu thank you for the bounty. Where do you get that warning, when you try to import the modules, or maybe when running using `ts-node`? It would be very helpful if you can PR an example that causing that warning to the repository so I can further investigate it. – Christos Lytras Apr 30 '20 at 10:11
  • Might be from [WebStorm](https://github.com/clytras/typescript-monorepo/issues/2)? – Dan Dascalescu Apr 30 '20 at 10:25
  • 1
    Yes it might, I don't see that warning in latest VSCode that uses `TS 3.8.3`. I'll check this out to find out if it's related to the WebStorm or to the TS version that WS is using. – Christos Lytras Apr 30 '20 at 10:58
  • I've tried this for a `lib.common.ts` file in `../../` but I still got the `file ../../common.ts not under 'rootDir'` error. Is this `declare module '*.json'` trick limited to JSON files? I used `declare module '*.common.ts'` in the same `typings.d.ts` file. What I'm trying to do is share this `lib.common.ts` between a `backend` and a `website` directories in a monorepo. – Dan Dascalescu May 17 '20 at 12:33
  • Your solutions always works properly. thanks alot. – AmerllicA Jan 10 '21 at 17:28
  • In my situation, I need to put `typings.d.ts` into the `/src` folder – xwa130 Jul 19 '22 at 23:00
6

There is a tidy three-step solution with Node 16+ LTS and TypeScript 4.7+ for packages that use ES modules instead of CommonJS.

The "imports" field in package.json defines internal pseudo-packages that can be imported only from within your actual package. Define an internal import specifier, such as #package.json in package.json:

{
  "type": "module",
  "exports": "./dist/index.js",
  "imports": {
    "#package.json": "./package.json"
  }
}

Enable TypeScript support for ES modules and JSON imports in tsconfig.json:

{
  "compilerOptions": {
    "module": "nodenext",
    // "moduleResolution" defaults to "nodenext", just made explicit here
    "moduleResolution": "nodenext",
    "resolveJsonModule": true,
  }
}

Lastly, import #package.json from your TypeScript module:

import packageJson from '#package.json' assert { type: 'json' };

console.log(packageJson);
ide
  • 19,942
  • 5
  • 64
  • 106
2

It is not possible for now. Typescript compiler try to keep your directory structure.

For example, your project look like:

src/
  shared/
    index.ts
  index.ts
package.json
tsconfig.json

Your tsconfig.json contains:

{
  "compilerOptions": {
    "outDir": "./build",
    "module": "commonjs",
    "target": "es6",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "noImplicitAny": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true
  },
  "include": [
    "src/**/*"
  ]
}

As you see, the file does not include rootDir property, but when you call tsc command to compile the project, the output will look like:

build/
  shared/
    index.js
  index.js

The output does not contain src folder, because in my code, I just import and use inside src folder, like:

src/index.ts

import someName from './shared';

then, build/index.js will look like:

...
const shared_1 = __importDefault(require("./shared"));
...

as you see - require("./shared"), this mean it working fine with build folder structure.

Your "issue" appeared when you import a "outside" module

import packageJson from '../package.json';

So, what happen with "back" action - '../'? If you hope your output structure will be:

build/
  package.json
  index.js

then, how do they work with const packageJson = __importDefault(require("../package.json"));. Then Typescript compiler try to keep project structure:

build/
  package.json
  src/
    index.js

With a monorepo project, I think you need to create declaration files for each library, end then use references setting in the tsconfig file. Ex:

  1. In the ./lib01 folder, the lib import ./lib02 in their code. Tsconfig file will be like:
{
  "compilerOptions": {
    "declarationDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*"],
  "references": [ // here
    {
      "path": "../lib02"
    }
  ]
}
  1. lib02's tsconfig.json
 {
   "compilerOptions": {
    "declarationDir": "dist",
    "rootDir": "src",
    "composite": true // importance.
  }
 }
hoangdv
  • 15,138
  • 4
  • 27
  • 48
  • I was thinking that since `src/index.ts` imports `../package.json`, TypeScript can simply keep the same path, change nothing, and generate `dest/index.js` with the same `import '../package.json'`. (I use `esnext` modules, but that doesn't matter.). Both `src` and `dest` have the same parent, so `..` will resolve to the same directory. – Dan Dascalescu Apr 25 '20 at 22:31
  • 2
    ['Tis totally possible](https://stackoverflow.com/a/61467483/8910547)! – Inigo Apr 27 '20 at 20:02
1

It depends on how and when you're reading "package.json". You can read it as file with NodeJS "fs" module at runtime, or just type const package = require("package.json").

In 2nd case Typescript will search it in root dir at compile time (refer to Typescript module resolution documentation).

You also can use "rootDirs" property instead of "rootDir" to specify array of root folders.

1

When using // @ts-ignore on top of the import call and setting "rootDir": "./src" it works. In this case enabling resolveJsonModule will still work, but only for files under the ./src. See: https://github.com/MatrixAI/TypeScript-Demo-Lib/pull/33 for how I applied it to our template repository. This way it is possible to import json files from within ./src as normal, but when you import ../package.json, you have to use // @ts-ignore to ensure that TSC ignores it. It's a one-off special case so this works.

The reason it all works is because setting https://www.typescriptlang.org/tsconfig#rootDir will force tsc not to infer the project root dir to be the src. And thus will enforce the expected dist structure, while throwing warnings/errors on importing outside the rootDir. But you can ignore these warnings.

CMCDragonkai
  • 6,222
  • 12
  • 56
  • 98
-1

I solve this problem by using symlink: in windows:

cd src
mklink package.json ..\package.json

or in linux:

cd src
ln -s package.json ../package.json