-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
[Experiment] Handle package.json watch in tsc --build #48889
base: main
Are you sure you want to change the base?
Conversation
…everything on package json change
Sounds related:
|
@sheetalkamat (C.f. the now closed question #48794 where @andrewbranch and @weswigham explained how to use SourceFile.impliedNodeFormat.) I saw this pull and assumed the content is necessary to get correct values for SourceFile.impliedNodeFormat.
I found that that it is compilerOptions:moduleResultion and not compilerOptions:module that needs to be set to "Node12". (See #48961). I have written a test that confirms this setting works, on the packageJsonWatch branch - its in pull #48958. Added further tests to #48958 |
The If that information was present, then when a package.json watch event occurred, it would be possible to test if the relevant package.json fields were changed or not, and avoid triggering project rebuilds unnecessarily. |
We would not be caching the package json's for life times since we want to have similar experience with or without watch. Normally we only watch whole file and if it changed the timestamp, we rebuild it |
Your logic is correct. However, improving both incremental-only and incremental+watch behavior should be OK. If the relevant package.json properties (or just a hash of their stringified version) were stored in a tsbuildinfo file, then both kinds of build could take advantage of the same code logic. In think the the only definitely TypeScript-relevant fields of a projects own package.json are:
|
Well we also need to consider size of build info before we start storing all these details .. so storing them isn’t practical esp when there are some huge projects with lots of package jsons |
By this do you mean a single Project (one project in a solution of projects) can have up to 1 one package.json for each source directory under it's root? So the worst case scenario would be one package.json per source file? Or is only one recognized package.json allowed per project? I had thought it was 1 recognized per project (always adjacent to or above the project tsconfig), but I am aware I could be wrong. IF it is only one package.json per project, then using the hash of the stringified collection of relevant properties would result in minimal impact on tsbuildinfo size. |
@andrewbranch @weswigham @amcasey ping on review |
I don’t quite understand (3). If I understand correctly, the issue (or part of it) is that files’
But the package.json file could be one directory above a tsconfig? |
Re: Is that a dead field now? (My runtime observation showed just an empty object). For discussion context only, this function in "moduleNameResolver.ts"
gets called from every |
I dont think this has anything to do with incremental build or not.
Did think about this and we could go levels up till root directory till we find the package.json file but i thought in most mono repo cases the package.json file would be next to tsconfig and we could add this extra thing when someone reports and avoid having to lookup extra locations. What are your thoughts on approach between incremental vs non incremental guess ? |
|
An important thing to keep in mind when talking about package.json is that TypeScript is trying to smoothly integrate user experience with the new changes in nodejs' use of package.json. Nodejs looks up the package.json at runtime, so the actual package.json being used is looked up relative to the output directory - it is the first one up from there. As you say about the most common case:
that is true. However, there are other cases. I think it is practical(*) to only support the most common case - certainly to start with it is much better to only support the most common case. However, it should be documented on the manual page about supporting new nodejs package.json features, if only to cut down on "bug" -> "working as intended" issue clutter. TypeScript supported package.json scenarios
(*) However, if any change to package.json triggers a rebuild, even a change to "scripts" which should not trigger a rebuild, that would not be practical. BUT - in fact, I seem to recall experiencing that in some situations a change to package.json does not trigger a rebuild. I think it might be that you only do a package.json date check on program startup, but that is only a guess. Perhaps you intentionally made it that way as a practical workaround to cut down on (but not eliminate) unnecessary rebuilds from changes to package.json that are not build-related (e.g., changes to When newer-than-oldest-output date checking is performed:
The documentation could be as simple as the above table with the unknown "?" entries correctly filled in, and placed on the manual page about supporting new nodejs package.json features. |
After we discussed this in the design meeting, I started to write down the different proposals we discussed, but ran some experiments with // @Filename: tsconfig.json
{
"compilerOptions": {
"incremental": false,
"outDir": "dist",
},
"files": ["index.ts"]
} // @Filename: index.ts
import "./other"; // @Filename: other.ts
export {}; Experiment 1
On step (3), the build is skipped with output
So only root files have their timestamps checked—this seems undesirable, but there’s no alternative without a complete file list. This is exactly the problem we were discussing with package.json files. Wes mentioned that the same problem exists for your Experiment 2With that realization, I thought enabling Experiment 3Try editing Experiment 4
Subsequent
but the build output says
Experiment 5Flipping How this relates to package.json discussionsMultiple proposals put forward in the design meeting about how to handle package.json as an up-to-dateness input when we only discover it is an input after program construction were centered around having a complete file list (either autogenerated and put in a build artifact like tsbuildinfo, or listed by the user in a tsconfig file). There was concern that adding any new restrictions would degrade existing scenarios. But it looks to me like package.json inputs have the exact same problem as non-root files, which already make existing scenarios behave unexpectedly. Conclusions
|
For posterity, I'll finish posting my "mental model" of how
As a first-pass optimization, if the input and output file list of a project can be statically determined ("statically" meaning without parsing any source files), and the outputs are at least as new as the inputs, then it's always safe for The I think the TL;DR for me on this is that the primacy of Everything beyond that just more "we can skip work without affecting observed behavior" optimization, e.g. doing a timestamp-only update of a downstream dependency when we detect that an upstream diff had no observable change through its .d.ts files.
Given all this the thing that makes me nervous is doing behavior "under |
I think that’s a perfectly coherent mental model, but it seems to be very much not what exists today. As far as I can tell, |
To ensure all the files are listed as root files, (barring any d.ts files) we have composite flag that is irrespective of whether you are using --build or not. The composite flag is needed by any project that gets referenced by any other project. When you enable composite flag in a project, it automatically means it has enabled "declaration" and "incremental" flags as well and those flags cannot be turned off.
Incremental build is per project thing where it determines based on file versions from old program and new program's file versions to determine which files need to be emiited/type checked. Without incremental we would always emit all files. With incremental we emit subset of files that need to be emitted. |
That confirms all my observations but the design seems weird to me. Timestamp checking and watching under |
You say "we should basically be validating that we can statically detect all files at least outside of node_modules when determining up-to-dateness". Suppose that package.json (in the supported location next to tsconfig and with nodeNext set etc,) now has full source file status. In that case changes to any project's package.json would trigger rebuilds of own project, and other projects through the composite hierarchy graph, just like any source file would. Do you mean there is something beyond that, that needs to be accounted for? (I can think of checking that a cjs module does not call an es6-only module, but I cannot immediately see anything beyond that). I hate be a bore and repeat myself but I do wish that that this table would be clarified for the package.json row: Current behavior of when project newer-than-oldest-output date checking is performed under
because it is relevant to whether package.json has full source file status or not. I mean, package.json has full source file status if and only if the entry is "true", "true". Otherwise it is a notable limitation. |
Is it correct that the hole is the case with these 3 conditions satisfied?
At least changes to the unlisted files won't fail to trigger other rebuilds of projects that depend on this project, because I guess no other projects depend on this project. But it is still a hole. |
{ | ||
createNewValue: (path, _input) => watchFile( | ||
if (!state.watch) return; | ||
// Container if no files are specified in the project, nothing to watch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚙️
// Container if no files are specified in the project, nothing to watch | |
// If no files are specified in the project, there is nothing to watch in the container. |
watcher: FileWatcher; | ||
projects: Set<T>; | ||
} | ||
|
||
/** | ||
* Updates the map of shared extended config file watches with a new set of extended config files from a base config file of the project | ||
*/ | ||
export function updateSharedExtendedConfigFileWatcher<T>( | ||
export function updateSharedExtendedConfigFileWatcher<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✂️
export function updateSharedExtendedConfigFileWatcher<T>( | |
export function updateSharedExtendedConfigFileWatcher<T>( | |
resolvedPackageJsonPaths.set(resolved, packageJsonPath); | ||
return packageJsonPath; | ||
} | ||
|
||
function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please specify the return type for this function?
Changes in this:
tsc --build --watch mode earlier relied on what package.json were looked up during program construction. This was unreliable way to determine uptodate ness as when doing incremental build (that is tsc --b then some change to package json and then tsc --b, we couldnt look at the package.json as program wasnt created) Now instead we watch and look at all the package.json files that are next to own config or referenced projects's config.
Note we continue to watch using watchFile instead of watching for only file add or delete as package.json is now much more important as brought up here
Fixes #48314