1

Situation: you have a typescript project that's configured to also output JSON files. You have the correct tsconfig.json setup and the correct dependencies. You have also read this related Q&A and made sure your typescript files import the json files. Yet, when you run tsc, you notice that json files are missing.

Then you copy the project to a separate location, with the exact same file layout, exact same binaries, and run tsc. Now, the json files get generated. WTFNODE?

Here's a minimal repro, using node v16.13.2 and yarn v1.22.7

reference project

suppose you have nodejs and yarn installed at the ready. If using Nix, you can achieve this via nix-shell -p yarn nodejs. Then:

pwd
# /tmp/foolib
yarn init -y  # blank project
yarn add typescript  # this is all we need for the repro
export PATH=$(yarn bin):$PATH  # make sure we see `tsc` in the path

the remaining files:

src/index.ts

import Thing from './moveme.json'
console.log("Hello")

src/moveme.json

{ "foo": "bar" }

tsconfig.json

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "ES2015",
        "declarationMap": true,
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "outDir": "./dist",
        "skipLibCheck": true,
        "declaration": true,
        "jsx": "react"
    },
    "include": [
        "src/**/*"
    ]
}

if you run rm -rf dist;export PATH=$(yarn bin):$PATH; tsc; find dist you should see this output:

dist
dist/index.d.ts
dist/index.d.ts.map
dist/moveme.json
dist/index.js

another project

suppose you're importing the project as a library somewhere else. Let's create a fake one this way:

mkdir /tmp/fooproject
cd /tmp/fooproject
yarn init -y  # this creates package.json, yarn.lock, node_modules
cp -R /tmp/foolib ./node_modules/  # create a local copy
cd node_modules/foolib
tsc

now, the output of find dist

dist
dist/index.d.ts
dist/index.d.ts.map
dist/index.js

why is there no moveme.json

directed laugh
  • 573
  • 4
  • 8

1 Answers1

2

tl;dr

when you are running tsc from a location where the real path (after symlinks have been resolved) contains a node_modules directory anywhere in the hierarchy, the json files will not get copied.

quick fix

Based on the way tsc works, it's probably considered bad practice to ever run tsc out of a directory somewhere within node_modules. But if at some point you decide to do this, your quickest way out is to compile it at a different location, where node_modules is not in the path hierarchy (you can even rename node_modules and rename it back).

explanation

When you call tsc, it launches a bunch of async Workers that parse the source files and figure out the code dependency tree. The modules that are referenced in the code (such as the imported JSON file in the above example) are discovered here then resolved by worker logic here.

So in our example, processImportedModules() will discover that index.ts imports a module called moveme.json. The decision of whether to include this file as an output file depends on this line:

const isFromNodeModulesSearch = resolution.isExternalLibraryImport;

because it's not getting output, we know tsc thinks moveme.json is an "external library import". This determination is made by the tryResolve() function here and in particular, this line

return resolved && toSearchResult({ resolved, isExternalLibraryImport: contains(parts, "node_modules") });

finally tells us that any file that's categorized as a "module" whose path contains node_modules somewhere in its path will get counted as a an "external library import", and not get copied into the output folder.

After tracing the source, this behavior makes logical sense, but is highly surprising, as the output of what appears to be an isolated project actually depends on the structure all the way to the root of the filesystem. Without this knowledge, tsc's behavior is inconsistent; given:

  • inputs0 --{tsc}--> outputs1 from /tmp/foolib
  • inputs0 --{tsc}--> outputs2 from /tmp/fooproject/node_modules/foolib

here, outputs1 != outputs2

With this knowledge, tsc behavior is working-directory-dependent.

directed laugh
  • 573
  • 4
  • 8