Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ts-node with project references in a lerna monorepo #897

Open
kerdany opened this issue Oct 16, 2019 · 37 comments
Open

ts-node with project references in a lerna monorepo #897

kerdany opened this issue Oct 16, 2019 · 37 comments
Labels
enhancement you can do this Good candidate for a pull request.

Comments

@kerdany
Copy link

kerdany commented Oct 16, 2019

I'm using lerna with typescript project references for a node application containing two packages. Package lib and package app (which depends on lib).

I've configured typescript project references so that package lib is built automatically whenever we run tsc --build inside package app (i.e. to build for production).

Here's how the tsconfig.json files are configured for each of the packages:

packages/lib/tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "tsBuildInfoFile": "dist/.tsbuildinfo",
    "composite": true
  }
}

packages/app/tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "tsBuildInfoFile": "dist/.tsbuildinfo",
    "incremental": true
  },
  "references": [
    { "path": "../lib" }
  ]
}

Currently, running tsc --build (inside app) compiles typescript to javascript in the dist directory in each of app and lib perfectly fine, the setup runs flawlessly in production mode.

The problem, however, is that trying to run the project in development with ts-node via nodemon --exec 'ts-node src/index.ts' in development fires the following error:

/path/to/packages/app/node_modules/ts-node/src/index.ts:245
    return new TSError(diagnosticText, diagnosticCodes)
           ^
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(1,23): error TS2307: Cannot find module '@myproj/lib'.
...

What seems to be happening, is that ts-node is looking for the @myproj/lib package inside node_modules directory (symlinked by lerna), instead of compiling it on the fly through typetcript's project references, as is setup inside both tsconfig.json files.

I validated my theory by:

  • Running a regular tsc --build first (which also builds the lib/dist code).
  • Then running nodemon --exec 'ts-node src/index.ts' again, and it ran fine then.

Which means that ts-node in this case is loading lib via the compiled .js code inside lib/dist (symlinked by lerna), NOT via compiling its .ts code on the fly (via references).

I'm using ts-node@8.4.1 (currently the latest version).

Some questions:

  • Doesn't ts-node currently support project-references yet or passing the --build flag to tsc yet?

  • Am I doing something wrong in my (e.g. tsconfig.json) configurations that's causing ts-node not to compile/build lib.

  • I can see that a new --build flag is being added to ts-node in the master branch (commit), but it seems that it's irrelevant to tsc's --build flag.

Note: Currently I'm working around this (without ts-node) via nodemon --exec 'tsc --build && node dist/index.js' till I get this figured out.

Thanks!

@kerdany kerdany changed the title ts-node with typescript project references ts-node with typescript project references in a lerna monorepo Oct 16, 2019
@kerdany kerdany changed the title ts-node with typescript project references in a lerna monorepo ts-node with project references in a lerna monorepo Oct 16, 2019
@blakeembrey
Copy link
Member

  1. I don't think so, though I can replicate project references working for other cases. It's using the language services API today which has typically had no problems, but I'd have to ask if you can build a simple repro for me to work with on this to confirm.
  2. I don't think so, more likely that references don't work as expected.
  3. Correct, I can probably rename to --emit to clarify the use-case. However, I run into issues with project references on the latest master build that need to be fixed (so your example might help me too) 😦

@kerdany
Copy link
Author

kerdany commented Oct 16, 2019

Thanks @blakeembrey

  1. Sure, I will prepare a sample repo to reproduce the issue first thing in the morning (UTC+2 here).

Thanks!

@kerdany
Copy link
Author

kerdany commented Oct 17, 2019

Hi again,
I've created code to reproduce issue here:
https://github.com/kerdany/ts-node_issue-897

@blakeembrey
Copy link
Member

@kerdany Thanks! Interestingly I get the same error in development with VS Code and tsc, until I enable --build. This makes sense, since I assume that TypeScript is actually building references first when you specify --build. It does make it trickier for ts-node though, I guess it should also build those dependencies somehow. We might have to do something in memory to make this possible...

@blakeembrey
Copy link
Member

blakeembrey commented Oct 31, 2019

Another interesting part of this is that it's related to node and TypeScript's understanding of module resolution. You can actually make this work with the current release of ts-node by importing directly the other .ts file:

import { greet } from "@myproj/lib/src/index";

Edit: Don't expect this to work in the next major version though, I've been having a lot of trouble with getting project references working and this also fails with VS Code tsc.

@blakeembrey blakeembrey added enhancement you can do this Good candidate for a pull request. labels Nov 8, 2019
@jomi-se
Copy link

jomi-se commented Dec 4, 2019

Have you done any progress or got any clues for this? I just ran into the same issue as well

@impaler
Copy link

impaler commented Dec 7, 2019

I also ran into this trying out packages in yarn workspaces. When wanting to debug with node -r ts-node/register as a work around for now I am composing a root tsconfig.json with all relevant references and running a tsc --build --watch running separately. Would be great to have project references resolve and build with ts-node alone.

@nickzelei
Copy link

FYI it looks like they've added API support for building project references.

microsoft/TypeScript#31432

interface SolutionBuilder<T extends BuilderProgram> {
    build(project?: string, cancellationToken?: CancellationToken): ExitStatus;
    clean(project?: string): ExitStatus;
    buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus;
    cleanReferences(project?: string): ExitStatus;
    getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject<T> | undefined;
}

@krzkaczor
Copy link

FYI: the obvious workaround is to set TS_NODE_TRANSPILE_ONLY=true but then you won't get any type checks.

@dchambers
Copy link

@krzkaczor, I tried setting this on the command line (using --transpile-only and -T) and it makes no difference -- project references still aren't built.

@dchambers
Copy link

I ended up using concurrently to build the project references for all the libraries within our monorepo (all inside a lib directory) like so:

"scripts": {
  "start": "concurrently \"tsc -b -w ../lib\" \"nodemon -w ./src -w ../lib --ignore 'lib/*/src/*' -e js,jsx,gql,ts,tsx --exec ts-node src/server.ts\"",
}

Here, the lib directory contained a tsconfig.json with references to all the composite projects within the directory.

@calvinwyoung
Copy link

I'd also like to use ts-node with project references. Until this is supported, I'm using the following workaround:

tsc-watch -b --onSuccess 'node dist/index.js'

On my project, this picks up changes slightly faster than using concurrently with tsc and nodemon.

@Sytten
Copy link

Sytten commented Nov 18, 2020

Any progress on it?

@JasonKleban
Copy link

JasonKleban commented Dec 30, 2020

// Write `.tsbuildinfo` when `--build` is enabled.

This comment is confusing me. Does it mean to say that there is a --build flag in ts-node? I can't find it in the code or the documentation. I assume it's just an artifact of a work in progress? (I'm trying to use naked pnpm rather than lerna, but I don't think that makes much difference.)

Here is a ~hidden pending change to the typescript compiler API wiki: https://github.com/microsoft/TypeScript-wiki/pull/225/files#diff-709351cd55688fbcb7ec0fc9973ee746R407

I am trying to figure out adding project references support to ts-node and then ts-node-dev, but I have low confidence that I can figure it out as a beginner contribution. If any of the maintainers have explored this before, can they please share their findings so far?

My basic guess is that we need a new CLI flag (--solution/--S) that basically replaces createIncrementalCompilerHost with createSolutionBuilderHost here:

ts-node/src/index.ts

Lines 787 to 799 in f77e1b1

const host: _ts.CompilerHost = ts.createIncrementalCompilerHost
? ts.createIncrementalCompilerHost(config.options, sys)
: {
...sys,
getSourceFile: (fileName, languageVersion) => {
const contents = sys.readFile(fileName)
if (contents === undefined) return
return ts.createSourceFile(fileName, contents, languageVersion)
},
getDefaultLibLocation: () => normalizeSlashes(dirname(compiler)),
getDefaultLibFileName: () => normalizeSlashes(join(dirname(compiler), ts.getDefaultLibFileName(config.options))),
useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames
}

If it were that simple, it'd probably be done by now. I think that SolutionBuilderHost used to extend CompilerHost, but no longer does. CompilerHost rolls up to ModuleResolutionHost whereas SolutionBuilderHost rolls up to ProgramHost.

BTW, where is it that the compiled in-memory script is executed? I don't have any ideas of what to do after ts.createSolutionBuilder(host, Array.from(rootFileNames), {}).build(); Seems like I need to get a Program out of there somehow?

@sheetalkamat might know exactly what to do.

@cspotcode
Copy link
Collaborator

// Write `.tsbuildinfo` when `--build` is enabled.

This comment is confusing me. Does it mean to say that there is a --build flag in ts-node? I can't find it in the code or the documentation. I assume it's just an artifact of a work in progress? (I'm trying to use naked pnpm rather than lerna, but I don't think that makes much difference.)

--build is referring to the incremental flag in tsconfig.json:
if(options.emit && config.options.incremental)
options.emit refers to ts-node's --emit flag/option, and config.options.incremental refers to TypeScript's incremental tsconfig option.

Here is a ~hidden pending change to the typescript compiler API wiki: https://github.com/microsoft/TypeScript-wiki/pull/225/files#diff-709351cd55688fbcb7ec0fc9973ee746R407

I am trying to figure out adding project references support to ts-node and then ts-node-dev, but I have low confidence that I can figure it out as a beginner contribution. If any of the maintainers have explored this before, can they please share their findings so far?

My basic guess is that we need a new CLI flag (--solution/--S) that basically replaces createIncrementalCompilerHost with createSolutionBuilderHost here:

I think we can get away with enabling this behavior by default, or at least enabling by default when --files is enabled. We might need to make --files default behavior in the future anyway.

BTW, where is it that the compiled in-memory script is executed? I don't have any ideas of what to do after ts.createSolutionBuilder(host, Array.from(rootFileNames), {}).build(); Seems like I need to get a Program out of there somehow?

Here is where we hook into node's module loading mechanism:

ts-node/src/index.ts

Lines 1022 to 1049 in f77e1b1

/**
* Register the extension for node.
*/
function registerExtension (
ext: string,
register: Register,
originalHandler: (m: NodeModule, filename: string) => any
) {
const old = require.extensions[ext] || originalHandler // tslint:disable-line
require.extensions[ext] = function (m: any, filename) { // tslint:disable-line
if (register.ignored(filename)) return old(m, filename)
if (register.options.experimentalEsmLoader) {
assertScriptCanLoadAsCJS(filename)
}
const _compile = m._compile
m._compile = function (code: string, fileName: string) {
debug('module._compile', fileName)
return _compile.call(this, register.compile(code, fileName), fileName)
}
return old(m, filename)
}
}

Node internally reads the file's contents from disk, then passes it to _compile. We wrap the _compile function to take the source text, compile it, and pass the emitted output to node's native _compile implementation, which handles module execution.

@JasonKleban
Copy link

Isn't typescript's --build CLI flag more related to composite: true and project references (v3.0) while incremental and tsBuildInfo (v3.4) can be used regardless of whether there's a typescript project-reference involved or not?

https://www.typescriptlang.org/docs/handbook/project-references.html#caveats-for-project-references says:

to preserve compatibility with existing build workflows, tsc will not automatically build dependencies unless invoked with the --build switch.

which I don't really understand - but why wouldn't ts-node need to respect this opt-in behavior?

Has this been reattempted since the API was made public to the extent that it is currently, and with the knowledge of the documentation in the wiki PR? I didn't see any branches here for it. Just wondering what trees the community can avoid barking up.

@cspotcode
Copy link
Collaborator

The following answers are based on memory. They might be wrong, they're not perfectly written, and they're long, but I tried to provide as much detail as possible.

Isn't typescript's --build CLI flag more related to composite: true and project references (v3.0) while incremental and tsBuildInfo (v3.4) can be used regardless of whether there's a typescript project-reference involved or not?

Correct, --build / composite imply and require incremental. If I had to guess, the comment you see in our source code about --build is conflating the two. The comment is potentially confusing; I wouldn't focus on it too much.

why wouldn't ts-node need to respect this opt-in behavior?

That's a good question. If the user runs a script in projectA which imports a file from projectB, what should we do? Should we follow project references and compile the file in projectB, using projectB's compiler options, essentially defaulting to --build mode? Should we eagerly type-check the entirety of projectB?

Also, ts-node has its own ignore option which determines which files we do/don't compile. Also, is having our own ignore option a good or bad idea?

To get more detailed, tsc and node load files in different ways, and we need to somehow bridge the gap.

TypeScript has the luxury of eagerly loading all projects and files, type-checking and emitting them all at once. It starts with a single tsconfig.json. It follows all project references, globs for all "files"/"includes", recursively parses all import statements, and adds those files, too. This process pulls more and more projects and files into the compilation, and all must be compiled and emitted. tsc's job is to eagerly pull all files into memory, parse and typecheck them all, and emit .js for all.

node, on the other hand, loads files on-demand when require()d or imported. When this happens, ts-node must ensure that tsc has already looked at the file. If it hasn't, ts-node forcibly adds it to the "files" array, triggering tsc to parse, type-check, and emit it. The language service is well-suited to this on-demand style of compilation, but I'm not sure on-demand fits well with project references.

I use the term tsc loosely to refer to the various TypeScript APIs we use.

Possible simplifications

This all needs research, but here are a few things that might simplify ts-node and help us support project references:

  • make --files on by default and/or required for project references
  • Disallow "forcibly adding" files as described above. If a file is not included in your "files" or "include" array, we throw a helpful error

About incremental and tsbuildinfo

I believe that implementing project references with the performance benefits of incremental will require writing all output into a separate, ts-node private cache. Related to #1161

ts-node uses potentially different compiler options than tsc, so our emitted output may look different. It can't be written to disk in the same place as tsc's output. For example, the user might specify TS_NODE_COMPILER_OPTIONS. Also, we override certain sourcemap options.

@cspotcode
Copy link
Collaborator

I realized I should clarify something:

That's a good question. If the user runs a script in projectA which imports a file from projectB, what should we do? Should we follow project references and compile the file in projectB, using projectB's compiler options, essentially defaulting to --build mode? Should we eagerly type-check the entirety of projectB?

What I mean by this is, if you import a file from projectB, then node is going to have a require() that needs to happen. What do we do when that file from projectB is require()d?

@cspotcode
Copy link
Collaborator

@JasonKleban I've been thinking about a good place for a beginner to start contributing to ts-node. I think #1161 is a great starting point. All contributions are appreciated, so of course you can focus your efforts anywhere you want. But if you're looking for something straightforward that will bring us closer to project references, #1161 is a great option.

@theomessin
Copy link

Bump

@vamcs
Copy link

vamcs commented Oct 15, 2021

I also have this problem and it would be really nice to avoid running the extra tsc --watch.

@theomessin
Copy link

theomessin commented Oct 18, 2021

This is a bit of a hack but I've written a small script ts-builder that can be used with Mocha to do tsc --build before loading the compiled JavaScript. It will emit the files as configured in tsconfig.json. This is what I'm now using with Mocha by doing:

mocha -r @theomessin/ts-builder/register **.test.ts

Should do until ts-node supports project references.

@wunderdaz
Copy link

This thread is really hard to find... Been on the hunt for hours on this issue.

It's a really necessary feature

@cspotcode
Copy link
Collaborator

Do you want to try your hand at writing a pull request for it? Check out #1514 and see if there's anything you want to help with.

Alternatively, if your employer is willing to pay for the feature to be prioritized, we might be able to work something out. We've not done that sort of thing before but we could give it a try.

@wunderdaz
Copy link

wunderdaz commented Dec 30, 2021

https://gitlab.com/darren-open-source/mo-ts-scaffold

If it helps at all, I setup a basic TS project with modular entrypoints... I've created a list of objectives (Which were based on many TS complaints across multiple forums/tickets) and have basically resolved most of them besides this issue.

Happy for anyone to check it out and try get things working. Hoping to create a good minimum viable TS (For node) project so developers have a great starting point.

I've found that, unfortunately, for any decent project there needs to be a build pipeline. I didn't go towards babel, as I prefer webpack to just put everything into one bundle file... This also solves a lot of potential deployment and production reference issues.

@zomars
Copy link

zomars commented Mar 2, 2022

I've just stumbled upon this very same issue. I couldn't find any info on the best possible way to work with ts-node inside a monorepo. Tried using both "require": ["tsconfig-paths/register"] and "experimentalResolverFeatures": true with no luck. :/

@zachkirsch
Copy link

+1 I would also love a way of running ts-node on a cli that lives in a monorepo (and imports from other monorepo packages using tsconfig references)

@softmarshmallow
Copy link

Essential for developing monorepo cli apps. +1.
I have no solid solutions for this problem yet, but I'll share my progress here with my production project - https://github.com/gridaco/cli

@zomars
Copy link

zomars commented Jul 20, 2022

adrien2p added a commit to adrien2p/medusa-extender that referenced this issue Aug 2, 2022
adrien2p added a commit to adrien2p/medusa-extender that referenced this issue Aug 2, 2022
adrien2p added a commit to adrien2p/medusa-extender that referenced this issue Aug 2, 2022
@nicoabie
Copy link

nicoabie commented Aug 8, 2022

https://github.com/calcom/cal.com/blob/d1d467d28d03811b50ec2942c8eeef82e2e425a5/packages/tsconfig/base.json#L20-L28

thanks @zomars !!!

I was having issues with a custom typing and the files: true flag solved the issue.

This is my tsconfig.json of the test folder that references the source and has to reference a custom typing the source uses:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "composite": true
  },
  "include": ["./", "../types/index.d.ts"],
  "references": [{ "path": "../src" }],
  "ts-node": {
    "files": true,
  }
}

tran-simon pushed a commit to tran-simon/jami-web that referenced this issue Nov 24, 2022
Improvements:
- The common/ project is automatically built by dependent projects
- Building common/ in in the "prepare" script is thus no longer necessary
- Vite and server hot-reloading now work if files in common/ change
- Recompiling common after every change is done automatically
- Using "go to definition" now jumps to the .ts file rather than the .d.ts

Changes:
- Use TypeScript project references to refer to common/ from client/ and server/
- Set "composite" and "declarationMap" options in common/tsconfig.json
    - See https://www.typescriptlang.org/docs/handbook/project-references.html
- Use tsc --build in order to build references automatically
- Replace nodemon and ts-node with ts-watch in server in order to use the new tsc --build mode
    - See TypeStrong/ts-node#897 (comment)
    - Remove now unneeded SIGUSR2 signal handler which was for nodemon
- Use tsc-watch before Vite in client in order for hot-reloading to work if common/ changes
- Update TypeScript version
- Add vite.config.node.json to be consistent with expected Vite project defaults

GitLab: #151
Change-Id: Id2f84fe45e44c4d8b4e6d3b324e1aee322c52df6
tran-simon pushed a commit to tran-simon/jami-web that referenced this issue Dec 2, 2022
Improvements:
- The common/ project is automatically built by dependent projects
- Buildling common/ in in the "prepare" script is thus no longer necessary
- Vite and server hot-reloading now work if files in common/ change
- Recompiling common after every change is done automatically
- Using "go to definition" now jumps to the .ts file rather than the .d.ts

Changes:
- Use TypeScript project references to refer to common/ from client/ and server/
- Set "composite" and "declarationMap" options in common/tsconfig.json
    - See https://www.typescriptlang.org/docs/handbook/project-references.html
- Use tsc --build in order to build references automatically
- Replace nodemon and ts-node with ts-watch in server in order to use the new tsc --build mode
    - See TypeStrong/ts-node#897 (comment)
    - Remove now unneeded SIGUSR2 signal handler which was for nodemon
- Use tsc-watch before Vite in client in order for hot-reloading to work if common/ changes
- Update TypeScript version to fix issue
- Add vite.config.node.json to be consistent with expected Vite project defaults

Future work:
- Updating to the latest TypeScript version was needed in order for this to work. However, ESLint complains that the TypeScript version is too recent. An update for ESLint's TypeScript parser should be applied as soon as it is released.

GitLab: #151
Change-Id: Id2f84fe45e44c4d8b4e6d3b324e1aee322c52df6
tran-simon pushed a commit to tran-simon/jami-web that referenced this issue Dec 2, 2022
Improvements:
- The common/ project is automatically built by dependent projects
- Building common/ in in the "prepare" script is thus no longer necessary
- Vite and server hot-reloading now work if files in common/ change
- Recompiling common after every change is done automatically
- Using "go to definition" now jumps to the .ts file rather than the .d.ts

Changes:
- Use TypeScript project references to refer to common/ from client/ and server/
- Set "composite" and "declarationMap" options in common/tsconfig.json
    - See https://www.typescriptlang.org/docs/handbook/project-references.html
- Use tsc --build in order to build references automatically
- Replace nodemon and ts-node with ts-watch in server in order to use the new tsc --build mode
    - See TypeStrong/ts-node#897 (comment)
    - Remove now unneeded SIGUSR2 signal handler which was for nodemon
- Use tsc-watch before Vite in client in order for hot-reloading to work if common/ changes
- Update TypeScript version
- Add vite.config.node.json to be consistent with expected Vite project defaults

GitLab: #151
Change-Id: Id2f84fe45e44c4d8b4e6d3b324e1aee322c52df6
@pjanickovic-ph
Copy link

pjanickovic-ph commented Aug 4, 2023

Hello, I was searching for a solution for like 2 weeks, and I think I found a perfect workaround. the whole idea is that I run my main app using nodemon and ts-node, and except watching my app folder, i added another folder to watch but not the src folder of my library but the build (output folder) and extended the watch to .js files as well. Then, run tsc -b -w on my library which watches the library itself, and rebuild if necessary, and if it emits new .js files, nodemon catches it as well. and it is even in the right order

@vorant94
Copy link

Hello, I was searching for a solution for like 2 weeks, and I think I found a perfect workaround. the whole idea is that I run my main app using nodemon and ts-node, and except watching my app folder, i added another folder to watch but not the src folder of my library but the build (output folder) and extended the watch to .js files as well. Then, run tsc -b -w on my library which watches the library itself, and rebuild if necessary, and if it emits new .js files, nodemon catches it as well. and it is even in the right order

so assuming you have like 3 different libs and 1 app in your monorepo... do you start basically 4 processes each time you start coding (3 tsc -b -w and one nodemon)?

also this workaround means that each change in the lib would be propagated within 2 rebuilds (1 of the lib itself and 1 of the app that is using the lib), but the changes in the app would be propagated within 1 rebuild, which means inconsistency of the time interval between saving changes and ability to run the code...

it indeed does the job but not without tradeoffs

@daniel-savu
Copy link

Ran into this issue today as well, although I'm just using yarn workspaces rather than lerna. Maybe worth changing the issue title since this is a broader problem? I've reproduced with a minimal setup here if it helps anyone: https://github.com/daniel-savu/ts-node-bug

@evgeny-myasishchev
Copy link

I have an npm workspaces and have same issue

@JeffJassky
Copy link

Anybody have any insight into this as of March 2024? Is this supported yet? Thanks!

@vorant94
Copy link

vorant94 commented Mar 6, 2024

Anybody have any insight into this as of March 2024? Is this supported yet? Thanks!

I ended up avoiding ts-node and other typesctipt runners. Here is an example repo for how achieve live-reload and so on solely with node built-in watch mode and tsc

https://github.com/vorant94/typescript-monorepo

@adrian-gierakowski
Copy link

https://www.npmjs.com/package/tsc-watch can be used as well for tsc driven workflows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement you can do this Good candidate for a pull request.
Projects
None yet
Development

No branches or pull requests