Consume files generated by other loaders using data['ts-loader-files']#520
Consume files generated by other loaders using data['ts-loader-files']#520MortenHoustonLudvigsen wants to merge 25 commits intoTypeStrong:masterfrom
Conversation
|
Sure - when the tests are green I'll set aside some time for a review. |
| } | ||
|
|
||
| // Load files generated by previous loaders in the same chain | ||
| const generatedFiles = getGeneratedFiles(this); |
There was a problem hiding this comment.
Can we put the new functionality behind a loader option please? Something like usePreviousLoaderGeneratedFiles. (Maybe something shorter?) I don't want this to run unless people have "flicked the switch".
So calling getGeneratedFiles etc only happens when you're using usePreviousLoaderGeneratedFiles
There was a problem hiding this comment.
The option usePreviousLoaderGeneratedFiles has been implemented in the latest commit.
There was a problem hiding this comment.
Btw. I am happy with the option name usePreviousLoaderGeneratedFiles, but say the word, and I will change it.
| const fileVersion = updateFileInCache(filePath, contents, instance); | ||
|
|
||
| const { outputText, sourceMapText } = options.transpileOnly | ||
| ? getTranspilationEmit(filePath, contents, instance, this) |
There was a problem hiding this comment.
Is it significant that it's not passed down to getTranspilationEmit but is to getEmit?
There was a problem hiding this comment.
It is only in getEmit that dependencies are handled. Dependencies should not be added for generated files. For example a dependency should not be added for Game.css.d.ts if it is a generated file. We rely on the generator to add any dependencies, that are needed. In this example the generator (ts-css-loader) would add a dependency on Game.css, not Game.css.d.ts witch it generates.
There was a problem hiding this comment.
Hmm... I see that the above may not be entirely clear. The reason getEmit recieves generatedFiles is that it adds dependencies, and they need to be filtered so that generated files are excluded. getTranspilationEmit does not add dependencies and so does not need generatedFiles.
There was a problem hiding this comment.
I could easily move the filtering of dependencies out of getEmit. That would also make it easy to restrict it to those cases when generated files are enabled.
There was a problem hiding this comment.
Filtering of dependencies has been moved to new function removeGeneratedFileDependencies in the latest commit. The call to this function is, of course, only made if the usePreviousLoaderGeneratedFiles option is true.
There was a problem hiding this comment.
Actually removeGeneratedFileDependencies has become removeDependencies, and is also used in getEmit instead of clearDependencies.
| const output = instance.languageService.getEmitOutput(filePath); | ||
|
|
||
| loader.clearDependencies(); | ||
| // loader.clearDependencies(); |
There was a problem hiding this comment.
Is this commented for a reason? I think this is necessary.
There was a problem hiding this comment.
I commented it out because I am a bit unsure. But if loader.clearDependencies is called any dependencies added in previous loaders in the chain will be forgotten, which would require som jumping through hoops to handle.
I may be missing something, but I don't see that it is necessary to make this call. Normally (if there are no previous loaders in the chain) there would only be one file dependency - on the ts-file being loaded. Or am I misuderstanding something?
There was a problem hiding this comment.
I think what is really wanted is not to clear dependencies, but to replace the original dependency on resourcePath with a dependency on rawFilePath.
My latest commit implements this while preserving any other existing dependencies.
| .replace(/[\d]+[.][\d]* kB/g, ' A-NUMBER-OF kB') | ||
| // We also don't want a difference in the number of bytes to fail the build | ||
| .replace(/ \d+ bytes /g, ' A-NUMBER-OF bytes ') | ||
| // Sometimes "[built]" is written to output, and sometimes not. This should not fail the build |
There was a problem hiding this comment.
Actually having "[built]" is significant I believe. It indicates whether a module has been recompiled as part of a run. I believe this is important for the integration tests which have multiple iterations.
There was a problem hiding this comment.
Well, in the previous PR, and in the first integration test of this PR, I saw that one test failed, but only for Node 4, because [build] was not there. However the test succeeded for all other versions of Node and TypeScript.
There was a problem hiding this comment.
The test in question is aliasResolution. See Job #1484.6
There was a problem hiding this comment.
This also happens sometimes in nightly builds: #1473.20
There was a problem hiding this comment.
Oh, and I made a mistake by filtering out [build] in stead of [built], but the second integration test of this PR still passed. So there must be something, unrelated to this PR, that makes webpack output [built] where it is not expected on seldom occasions, but I am at a loss as to what that something is.
There was a problem hiding this comment.
Hey! The first thing to say is that the integration tests are flaky. As it's a full integration test you do sometimes get false failures. This happens during the nightly builds. I've never looked into it in detail but generally rerunning a failing test pack will result in a green run. Anyway, [built] is significant AFAIK as it indicates whether a module has been recompiled as part of a run.
So I'd take the occasional failure here as a "something to live with". I think we need this or we may break behaviour in ts-loader in future and not realise it.
There was a problem hiding this comment.
Right - I'll roll back that change then.
| const moduleResolutionHost = { | ||
| fileExists: (fileName: string) => utils.readFile(fileName) !== undefined, | ||
| readFile: (fileName: string) => utils.readFile(fileName), | ||
| fileExists: (fileName: string) => { |
There was a problem hiding this comment.
This looks unrelated to the change being made - could you provide some info on this please?
There was a problem hiding this comment.
When a file is generated by a previous loader in the chain, it does not necessarily exist in the file system. So to make sure the TypeScript compiler can resolve the file, I look up in intance.files first. Otherwise it would not be able to resolve generated files. This should actually give a slight performance improvement, as files will only be loaded once (and of course maybe in watch-run).
There was a problem hiding this comment.
Cool - could we document that in comments please? Perhaps it would be worth this code path only being executed if usePreviousLoaderGeneratedFiles is true; presumably otherwise it would not work if there weren't previous loaders in the chain?
There was a problem hiding this comment.
I'll add some comments.
However I don't think it is necessary to wrap the code with a check for usePreviousLoaderGeneratedFiles. The files object is the one already used in ts-loader to cache files. Its contents are loaded in the following places:
- From the file system initially in
getTypeScriptInstance:
const files: interfaces.TSFiles = {};
...
const filesToLoad = configParseResult.fileNames;
filesToLoad.forEach(filePath => {
normalizedFilePath = path.normalize(filePath);
files[normalizedFilePath] = {
text: fs.readFileSync(normalizedFilePath, 'utf-8'),
version: 0
};
});- In the loader itself, using
updateFileInCache(I've excluded my additional calls toupdateFileInCachefor this discussion):
const rawFilePath = path.normalize(this.resourcePath);
const filePath = utils.appendTsSuffixIfMatch(options.appendTsSuffixTo, rawFilePath);
const fileVersion = updateFileInCache(filePath, contents, instance);- In
watch-run.tswhere file changes are registered
So it should be safe enough to load a file from files and use that, if it exists, instead of reading the file from the file system. This should even result in a slight performance improvement.
I would actually recommend updating files after reading them from the file system, and being able to have text == undefined for those files, that do not exist. This would reduce the file system access to once per file (initially - watch-run can of cause re-read any number of files).
All uses of file.text should be checked so they can handle file.text == undefined.
I am very happy to make this change - either in this PR or a new one, if you like.
There is one fly in the ointment. When I look at watch-run.ts, I see:
file.text = utils.readFile(filePath) || '';This means that files that no longer exist are not deleted and have a valid text.
If I make the changes I recommend above I would change this to:
file.text = utils.readFile(filePath);There was a problem hiding this comment.
All seems reasonable. If existing tests pass with those changes then I don't have any objection. I'm guessing performance would not be hurt by this?
There was a problem hiding this comment.
Quite the contrary :-) I'll get to these changes within the next couple of days (I am actually at my day job right now).
There was a problem hiding this comment.
I have added comments to moduleResolutionHost. I have decided not to make the suggested changes to file caching though, as I think that deserves its own PR, and shouldn't delay this one.
|
Just wanted you to know I haven't forgotten you. It's Easter here and I'll be offline probably until Tuesday. |
|
@johnnyreilly |
|
@johnnyreilly |
| const output = instance.languageService.getEmitOutput(filePath); | ||
|
|
||
| loader.clearDependencies(); | ||
| // Remove original dependency on resourcePath and replace with rawFilePath |
There was a problem hiding this comment.
Could you provide some explanation on what is happening here? This replaces the simple loader.clearDependencies() with something that will clear dependencies and then perhaps add dependencies again. I'm not clear on what loader.resourcePath is and whether we always want to follow this path.
Some details of what this should achieve would be helpful. Also, we still have the addDependency call after removeDependencies - will this result in double adds?
There was a problem hiding this comment.
I think maybe you missed the comments on outdated changes (probably my fault, not being an experienced GitHub user). But the gist is this:
The problem with calling clearDependencies is that it clears dependencies for the entire chain, not just the current loader. So if a loader has been run before ts-loader, any dependencies it has added will be cleared (in the case of ts-css-loader this could be dependencies on .css files).
Normally, if there are no previous loaders in the chain, there would only be one file dependency: the ts-file being loaded (resourcePath).
I think what is really wanted is not to clear dependencies, but to replace the original dependency on resourcePath with a dependency on rawFilePath, which is defined like so:
const rawFilePath = path.normalize(this.resourcePath);That's why I replaced clearDependencies with removeDependencies and addDependency.
There was a problem hiding this comment.
Of course, I should check if rawFilePath === this.resourcePath, and if so skip this step entirely.
There was a problem hiding this comment.
To be honest I think the GitHub UI is making things more confusing than I would hope 😄
I'm still a little confused and at least part of that is down to my incomplete knowledge of the webpack API. (Just for history, ts-loader was originally written by @jbrantly and I'm the main caretaker at present.) I'll lay out my confusion and you can set me straight.
As I read it, removeDependencies does the following:
- it gathers up existing
contextDependenciesanddependenciesfrom webpack. - clears dependencies
- re-adds the previously gathered dependencies unless they are the
resourcePath- could you clarify what the intent of this is please? I can see what's happening but I'm not so clear as to what you want to not be added as a dependency and what you do.
There was a problem hiding this comment.
See my comment in the changes tab.
There was a problem hiding this comment.
Well my understanding is that the goal of the original call to clearDependencies is to replace the dependency on resourcePath with rawFilePath, because it might be different.
To understand what is going on, I have looked in webpack/loader-runner where dependencies for loaders are handled, and f.ex. addDependency and clearDependencies are implemented.
It turns out that there is no method that will remove individual dependencies. The only method that actually removes dependencies is clearDependencies.
There is a method getDependencies that returns file dependencies, but it returns a copy of the internal dependency array, so any changes to will have no effect. The method getContextDependencies works in the same way.
So, if I want to replace a dependency, I have to get the existing dependencies, call clearDependencies, add all the dependencies except the one I want to replace, and then add the new dependency.
It turns out that clearDependencies clears file and context dependencies, so I have to do the same for context dependencies.
There was a problem hiding this comment.
NB. I have just checked the code in webpack/loader-runner again, and my assumption that, if ts-loader is the only loader, resourcePath will be the only dependency, is correct.
So the only reason, I can think of, for the original call to clearDependencies is to replace resourcePath with the same path, but normalized.
There was a problem hiding this comment.
OK cool that makes sense. So given we weren't preserving dependencies / contextDependencies in our original code it wasn't an issue because we would only be dealing with a single file in that context. But this is not true in the world where css-ts-loader is chained before ts-loader. Is that correct?
There was a problem hiding this comment.
I wonder whether replacing the dependency on resourcePath is really necessary. Even if it isn't normalized it should still trigger a new webpack build if the file is changed. Otherwise webpack would have problems with other loaders. The only place I can think of where it would be an issue is in watch-run.ts, but it already normalizes the paths of changed files.
|
|
||
| /** | ||
| * Remove dependencies specified by argument exclude. | ||
| */ |
There was a problem hiding this comment.
I'm basically happy with this - I'd just like to do some variable renames to improve clarity. Please could
dependenciesbecomedependenciesToPreservecontextDependenciesbecomecontextDependenciesToPreserve
Bit of a mouthful but it makes intent obvious
| function removeDependencies(loader: interfaces.Webpack, exclude: (filePath: string) => boolean): void { | ||
| const dependencies = loader.getDependencies(); | ||
| const contextDependencies = loader.getContextDependencies(); | ||
| loader.clearDependencies(); |
There was a problem hiding this comment.
Could filePath be renamed to something so people don't mix it up with the filePath in the method signature for exclude please?
|
Hi @johnnyreilly, |
|
No worries! |
|
I'm so sorry I haven't got back to this till now. My day job got in the way 😄 |
| function removeDependencies(loader: interfaces.Webpack, exclude: (filePath: string) => boolean): void { | ||
| const dependenciesToPreserve = loader.getDependencies(); | ||
| const contextDependenciesToPreserve = loader.getContextDependencies(); | ||
| loader.clearDependencies(); |
There was a problem hiding this comment.
Identifier filePath has become dependency.
|
You're right @johnnyreilly this could be a solution to #506. To me, something like Also, will this PR allow a |
|
Hey @MortenHoustonLudvigsen - would you be able to respond to @grantila please? Just want to ensure we've missed nothing relevant.. thanks! |
|
Right, first of all, I agree that However, I don't think that this is easy to change without doing a major rewrite of
If we could get With this in mind I still think this PR is relevant. BTW. I have ts-css-loader and ts-json-loader ready to go, as soon as this PR is accepted 😄 |
|
Oh, and btw: I agree that the option |
I've actually recently been using this approach with https://github.com/Realytics/fork-ts-checker-webpack-plugin to do type checking. It's faster!
Should we do this instead? If there's no performance penalty then it seems reasonable |
I haven't looked at this package, but does it also read from disk or will it read from webpack modules? For transforms performed pre-ts-loader (f.ex.
I'm happy to make the |
I don't actually know - @piotr-oles may be able to confirm... |
|
Option usePreviousLoaderGeneratedFiles is now inferred. I've removed the documentation for the option as well, but should I write a section at the end? |
|
I would yes. The build is now failing though :-( |
|
Could you try running Job #1598.15 again? |
|
Done. Question: is |
|
Great work @MortenHoustonLudvigsen, you're right about how large a rewrite would be, so this is a good approach that will do for quite a while. @johnnyreilly Yeah, that's a micro-optimization - checking a bool or |
Right now I infer usePreviousLoaderGeneratedFiles by checking whether |
|
BTW. ts-css-loader and ts-json-loader both do caching, so typings for a css og json will be generated only once. |
|
@grantila - cool. Could you just elaborate on this please?
I didn't quite follow the point about compiling speed. The compiling will be much slower comment raised alarm bells. I don't want to increase compiling speed at all and I wasn't clear what you thought would make it slower. Either way I wish to avoid that! Also your "inline with webpack point".. (ie is it your view that caching the value more or less inline with webpack). Given what @MortenHoustonLudvigsen has said it doesn't sound like caching the value is a valid approach anyway but I wanted to be clear I fully had your meaning on the other points. |
|
@johnnyreilly No I just meant "property access optimizations" is going to be far less important than the compiling from TypeScript to JavaScript per se. That will still be 95%+ of the time of ts-loader. Same for caching of files - the OS handles this for us. Caching of pre-compiled files is different though, naturally. Regarding "inline with webpack", I meant that loaders should try to be as similar in philosophy as possible. That means, having defaults so that most users need no extra config, and respecting pre-loaders to provide the file (rather than reading from disk). Requiring an option for As of now, it's impossible to let a loader prepend |
|
Great - thanks for the clarification! |
|
Hey @johnnyreilly |
|
I saw - thanks. And I'm sorry it's taking so long to get this reviewed BTW. Life is pretty busy right now... |
|
No worries! 😄 |
|
@johnnyreilly |
|
Great - thanks for letting me know. I'll hold off for now. Better to do things right than having to back them out later. |
|
Closing as inactive |
This PR creates a mechanism where by ts-loader can consume files generated by other loaders.
Right now this is meant to support ts-css-loader, but it can be used with other loaders, that write to data['ts-loader-files'].
I have not made a test for this PR yet, but I wanted to get the code up here to be reviewed. :-)
See also #512