Skip to content

Switch to nodejs export conditions from TS project references #67

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

Merged
merged 6 commits into from
Jan 24, 2024

Conversation

theseanl
Copy link
Contributor

@theseanl theseanl commented Jan 20, 2024

Previously, typescript project references were used to wire imports between workspace packages. During compilation, it allows one to resolve any workspace modules imported from another workspace module to their respective declaration files, and if it hasn't been built, it automatically tries to build the dependent module first. Also, in IDE, TS project references allows IDEs to resolve imports to their source .ts file instead of the .d.ts file as during the compilation; this allows one to get feedback directly from IDE when something is changed from a dependent workspace package without the need of re-building it.

However, it seemed that type information available from .ts and .d.ts were sometimes different, so that there was no error caught on IDE but the build fails. I believe it must be a bug of TS, or maybe due to a defective d.ts generated for yet another issue in the meantime, but this has been pretty confusing. Also, having to reference .d.ts to build requires a dependency graph to be acyclic, which hampers quickly implementing and testing features (regardless of the end output dependency graph being acyclic or not).

This PR tries to get rid of such issues by ditching TS project references and utilizing nodejs native package.json export conditions. Note that export conditions were already being used to support consuming the package in both esm and cjs format. We add an artificial condition "_bundle", and use the respective bundler's export conditions feature https://esbuild.github.io/api/#conditions and https://www.typescriptlang.org/tsconfig#customConditions to achieve a similar effect as what was provided by TS project references: IDE resolve any workspace package imports to their respective Typescript source file, not their build output .d.ts files. Moreover, now the compilation also consults the original .ts source files instead of .d.ts, so the build order does not need to be topologically sorted.

We lose an ability to compile only the necessary packages by abandoning TS project references, but I believe the number of packages are not too large, so we can manually accommodate it or just run a full build. If this feature is strictly needed, one may adopt a dedicated task runner such as Make.

+) This PR also tweaks build commands so that it works equally on Windows.

Previously, typescript project references were used to wire imports
between workspace packages. Now this is done by package.json export
conditions. This nodejs feature has already been used to support
consuming packages in both esm and cjs format. We add an artificial
condition "_bundle", and use the respective bundler's export conditions
feature https://esbuild.github.io/api/#conditions and https://www.typescriptlang.org/tsconfig#customConditions
to achieve that IDE and TS compiler resolve any workspace package
imports to their respective Typescript source file, not their build
output .d.ts files.

This has an additional benefit of reducing bundle size of cjs build.
Previously, CJS build operated on ESM build output; They fed
dist/index.js files to esbuild. This changes this so that esbuild
directly operates on Typescript source files even in CJS build. As
esbuild will have more information, it will be able to better tree-shake
unused codes.
@theseanl theseanl force-pushed the ditch-ts-project-references branch from 17dd95f to da0e74c Compare January 20, 2024 13:56
@siefkenj
Copy link
Owner

Could you reformat the JSON with Prettier, please? It should be 4 space indentation.

@theseanl
Copy link
Contributor Author

theseanl commented Jan 22, 2024

I just learned that Typescript project references are not transitive. Relevant discussions are at microsoft/TypeScript#30608, microsoft/TypeScript#46153. This could be a source of weird baheviors I have been experiencing with tsc -b.

@@ -19,6 +19,7 @@ import { isCjsPackage } from "../../scripts/esbuild-module-check.mjs";
format: "esm",
target: "node14",
external: [...explicitDeps],
conditions: ["_bundle"],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of adding a custom condition. Why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a key addition of the PR that makes the build work without using Typescript project references.

TS project reference allows Typescript to resolve import statements that import from another workspace package, such as @unified-latex/unified-latex-types, to their respective .ts source files or .d.ts declaration files depending on circumstances (more on it in the original post).

If TS project reference is removed, Typescript and ESBuild will follow the native nodejs module resolution algorithm, and it involves finding matching exports condition, which will be import if without _bundle, and it will then look for /dist/index.js file, which may or may not exist at that point. _bundle is there to point it to the .ts source files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly I don't like this as well, it feels like this custom condition is not designed for a use-case like this. I'll try to consult how other large Typescript monorepos are doing. That said, it was definitely an improvement in terms of dev experience because there's no longer any surprising build errors.

"clean": "rimraf ./dist",
"compile:tsc": "tsc",
"compile:esm_and_cjs": "node build.js",
"compile": "run-p compile:tsc compile:esm_and_cjs",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I didn't know about this utility!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like npm scripts at all, I don't like npm or Typescript project references trying to be a task runner!
It'd be nice to maximally parallelize the entire build, but currently there's no way to run npm scripts on each workspace in a parallel way.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My other PR uses wireit to do this, but it turns out the build didn't get sped up at all...(it was slowed down a little bit). You can look at it and see if I did something obviously wrong...

@@ -18,19 +18,23 @@
],
"exports": {
".": {
"_bundle": "./index.ts",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if we just pointed to the ts files via "import"? This file gets rewritten when packaging, so that might be a solution without introducing a new condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import field is what will be used when a consumer of this package installs it from npmjs and import it via import '@unified-latex/unified-latex. If it pointed to .ts file, because .ts files aren't present in the production package, consumers won't be able to import anything.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't matter, because the package.json file is completely rewritten for production. You can look in the scripts/make-package.mjs file for what happens when preparing to publish the package on npm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, didn't know that. Just tried "import": "./*.ts", and it seems that all the builds succeeds, but there's one nuisance, tests in unified-latex-cli invokes generated .mjs files directly and it needs the correct import values to locate imported modules. Not sure how this can be fixed.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm...I suppose we could bundle all the deps when the cli is built. It seems reasonable that unified-latex-cli be a standalone thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make import point to .ts, and real_import or whatever name to point whatever the previous import did, and call node with node -C real_import in cli testing. Then we will still be able to remove custom conditions from tsconfig and esbuild build scripts

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that require real_import to be added to every package.json? Either way, it seems reasonable. Let's call it dist_import or prebuilt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably yes, it should be added to every package.json that unified-latex-cli depends on

Since package.json is re-written before publishing, we are free to point
`import` condition to `.ts` source files. However, CLI tests need to be
able to resolve to `dist/*.js` files, so a custom condition `prebuilt`
pointing to those are kept in every package.json.
@theseanl theseanl force-pushed the ditch-ts-project-references branch from 7fa26c2 to af5e25d Compare January 23, 2024 23:34
@siefkenj siefkenj merged commit 4811a0b into siefkenj:main Jan 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants