Enable relative paths for cache keys #1
Labels
No labels
bug
dependencies
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
repos/gulp-once#1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A use-case we have when optimizing images is a nested folder structure, where the same filename might appear in different folders.
This is a problem for this library because the cache keys only use the basename, so it incorrectly tracks such files.
One simple (hacky) change that fixes this is the following:
This results in the cache keys being relative from the project root (ie. from where Gulp is being run), and is the correct thing to do in our case.
Would you be open to a PR that enables something like this via an option? Perhaps it should be possible to also base a base path from which the relative path should be resolved?
If you like the suggestion, and if you have any thoughts on how you'd like this implemented, or a better idea, let me know and I can do it.
I do agree there needs to be more flexibility in this area, I appreciate you bringing this suggestion up.
This plugin has been long overdue for me to go in and make some updates/refactor a few things (not the least of which is to remove the 'gulp-util'). I've mostly put it off because it doesn't seem to get that much usage and I'm using it in a very limited scope now.
I should actually have time to jump in and do this today. I'm definitely open to using relative paths for cache keys and adding a context option to work from relatively. I'm curious, in your case, is the 'namespace' option not a feasible solution (I can definitely think of situations where it wouldn't be) and if not, if the 'namespace' option was a function that was passed the vinyl file object and then returned a namespace key based on any custom logic you provided, would that be workable for you?
As I understood correctly, the namespace option is there if someone wants to, for example, separately track caches for scripts and styles - but within a namespace, the files processed would still only have the cache keyed by the
basename()which isn't ideal in cases where multiple files with the same name may exist in different paths.The workaround with the
namespacefunction could be a good solution, as we could, for example, return the path to the file (minus the actual filename) as the namespace (e.g. a namespace for each folder), or even the full file path could be a namespace (e.g. a namespace for each file) - it doesn't really matter.Thanks for the very quick response!
EDIT: BTW, I'm not sure why, but among the many caching plugins for Gulp, I'm pretty sure this is the only one I was able to find that natively does built-in caching across Gulp runs - most of them are just meant to be used with watch-style tasks, and I've found a few where a cache object can be passed on initialization and retrieved after completion, which still requires wiring up some additional code to persist the cache. It's a bit surprising that this isn't a more common use-case. We are using this for optimizing images via
imagemin, and are even considering committing the cache storage file to the repo, so that caching would work team-wide.Ha yeah, that was exactly why I wrote it. Specifically, for preventing extra calls to the tinypng api since I didn't want to eat up my free quota every time I booted gulp up. I do actually commit the
.checksumfile for that very reason. Though I'm not in a team setting, I do work from multiple devices so this can be pretty big help.All that to say, I've published an update that adds a
contextoption. Still have to add it to the readme, but after getting in there and seeing what all I'd want to change long-term, decided to go ahead and publish this little "workaround" for the time being to get you going.By default, the plugin will continue to operate as it has been. But, if you provide a context, all of the checksum keys will be the filename and path relative to the context.
Hope this helps!
Huh, so it seems I'm having some trouble running the code - I was on version 1.0.2 previously and updating to 1.2.0 stopped the plugin from working - so I tested 1.1.0 and it isn't working either for me (I'm on Node v6.11.4).
So I've literally gone line by line through the diff between 1.0.2 and 1.1.0, starting with 1.0.2 and replacing everything to match your changes - and the plugin stops working for me after this change near the bottom of
index.js:Any ideas what could cause this?
P.S. I'm looking at this diff:
github.com/corneliusio/gulp-once@d37691e2dfP.P.S. https://www.diffchecker.com/y0I0exOo - the code on the left is 1.1.0 and that doesn't work; the code on the right works.
That's a real headscratcher. That diff should be completely equivalent.
I've reverted to pushing the file to the stream explicitly and published the change. Give it a shot (I would really quickly first, but in my current setup I have some await/async calls that aren't supported in node@6).
I just tested both syntaxes on node 6.11, no issues. Might be something else going on with your setup.
Double check you're definitely working with gulp-once@^1.2.0 (
yarn outdatedornpm outdated). And if you wouldn't mind sending me your gulp task and any specific error you're getting I could take a look and see what might be going on.I've just tried updating all of my npm dependencies, thinking it could be something like that - though it's still not working, even with
gulp-once@1.2.1. The setup is pretty complicated so it would take me a bit longer to come up with a minimal repro example. However, I've done some debugging as below:The main difference I see between the newer versions and
1.0.2is that thereturnis now within thefile.isBuffer()block, and nothing happens if that check fails.I'm passing
./public/images/**/*intogulp.src()which is immediately piped intogulp-once.I've added
console.log(file.path, file.isBuffer());at the very beginning of thestream._transformfunction ingulp-once, and the first (and only) output I get when running my task is:(This is within a Docker container, and
/optis the project root)However,
public/images/adminis a folder, not a file, so I think this is where the plugin just stops any further processing. Any ideas on how to fix this in the plugin, or whether it's me doing something wrong/unexpected with Gulp?It does indeed seem that the problem is with folders being matched by the glob passed to
gulp.src(), because when I callgulp.src(source, {nodir: true})(as per thenode-globoptions), the plugin works correctly.Do you think this is something that the plugin should handle without failing?
Ok, that actually makes sense.
tl;dr
Sorry, I tend to over explain. Most recent version should work for you now. Exposition below, ha.
Yeah, gulp can be a little finicky about directories. Since basically all gulp plugins work off the assumption that you are going to pass it data and either transform the data and pass it along or pass the data along unchanged, the gulp stream tends to be pretty transparent with what goes through it. (i.e. If it goes in, regardless if it's a buffer, stream, directory, or null) it comes back out again) No muss no fuss.
However, with a plugin such as this, you are intentionally being restrictive about what goes through the stream. Which, as I discovered today, has resulted in it being a bit tricky to setup tests. At any rate, this plugin has suffered from a lack of robust testing on my part and where I made a critial mistake was only interacting with buffers in the plugin. Unfortunately, since I'm not using subdirectories in any of my sources that pass through this plugin, I never ran into the issue gulp basically failing silently when it hits a directory. Though, I have actually run into this issue with other plugins and just worked around it by making my src glob something like
src/**/*.*which kind of, basically only grabs files. I'd never made the connection with my plugin and this case of passing directories doesn't seem to be explicitly addressed anywhere that I can find in gulps documentation.Ultimately, the fix is rediculously simple. I only needed to make sure I passed all data along untouched if it wasn't a buffer, and only run all the filtering logic on the buffers. Might ultimately handle the issues I had with setting up tests, but we'll see. Hopefully I'll be able to push out a more robust and flexible 2.0 of this plugin with actual tests so I don't run into an issue like this again. Ha.
1.2.2 works great now with the
contextoption, appreciate it! I'll close this issue as the original request has now been implemented.Also, thanks for the in-depth explanation, I'm totally with you on the over-explaining aspect, and thanks for the only plugin that does what we actually need in terms of caching! :)
One thing that we're doing, which feels a bit weird, but I don't know a better way, is piping through
gulp-oncetwice :) We pass the same options both times - once at the beginning, just aftergulp.src(), and once at the end, aftergulp.dest(). This way, the has is computed first on entry, and if an image is not cached yet (or its hash has changed), it gets optimized - and then its hash is computed again after the optimization, so it gets stored in the cache file and doesn't get processed again on the next run. It's a micro-optimization I guess, but if you have a better suggestion on how to accomplish this, I'd love to hear it.Hmm… I'm not sure I'm following the reasoning on this, in fact, I would think this could cause issues for you guys. So for example…
I run this, and let's say the first call to once gives me this
.checksumsWithout context:
With context:
Now, this would mean that, at this point, if
foo.txtwas passed back into this gulp task unchanged it would hit this call to once, see the file exists in the checksums, see that the content hash is still "abcd", and not pass the file along to ourtransformPlugin()call.But on this first run,
foo.txtcontinues down the stream and hit'stransformPlugin()where it's content is changed and then passed along togulp.dest(). When it hit's the second call toonce()we'll end up with one of the two possibilities.Without context:
With context:
So now, let's saw we come back and run gulp again with no changes (i.e. src/foo.txt has a hash of "abcd"). In the first case, gulp will hit once the first time and see that the hash it has doesn't match the hash for
foo.txt, since this hash now represents the has generated from the transformed file and not the file in it's source state. So it passes the file along, transforms it, and recalculates the hash again. Meaning that the call to once will always pass files along—not preventing any calls totransformPlugin().In the second case (with context), with no changes made, once will correctly see that the hash for
src/foo.txtis still "abcd"—stopping the file there. At which point, the file never makes it togulp.destand never does anything with thedist/foo.txtentry. It's just taking up space in our.checksumsfile.Now, let's say we do make a change to
foo.txt. In the first case, things will work the exact same way, source will not match hash and the file will continue down the stream.In the second case, the only key checked against will be
src/foo.txt. We'll see a new hash since we changed the files and the file will continue down the stream. And at the end, we'll end up with a new hash value fordist/foo.txt. But keeping in mind that this value will never be checked against anything—we either repeat our above process or this one.Let me know if this is clarifying, or if totally missed the mark on what you were describing.
Kinda in a rush at the moment so no time for a long answer, but one fact I've failed to mention which might help make more sense is that we're editing the files in-place, ie. the source and the destination are always the same for us.
Our actual task is split into several files, some for configuration, some for the logic, but I've tried to create a self-contained Gulp task to show you what I mean (no guarantees this will work):
I've tried to explain in code comments what the code is supposed to be doing - does that maybe make more sense, or is this not going to do what we expect it to? Because in practice, as far as we can tell, it does exactly what we think it does.
Ahhh, yes. Then that would change everything. In that case what you're doing is correct. While I personally am not a huge fan of destructive transforms (i.e. overwriting source files), there shouldn't be anything wrong with this setup. And, in fact, there wouldn't be any way to get around having two function calls in your pipeline to the same plugin.
A plugin written to be more explicitly used in this way might expose two methods, something like a
once.test()for reading and filtering andonce.update()to update the stored hash. It just so happens that the call toonce()will simply do both regardless.I'm trying to think through scenarios where this behavior might cause issues, but I can't off the top of my head. If for some reason, you guys come across a situation where it does you can def let me know and I can change that functionality to be controlled more explicitly.
I guess separate methods might make sense for readability and explicitness, but our team is perfectly fine with using it like this.
I think it might cause issues if one would want to pipe some additional stuff after the second call to
once(). Right now, the next pipe would receive only the files which were updated (e.g. optimized images in our case) - though if one would want to do something with those, it's something that might even be useful - so I'm not sure whether separating the functionality is required. Maybe something more semantic likeonce.onlyNewOrChanged()andonce.updateCache()might work to cover all those situations in a readable way.Either way, if we encounter any issues with our current workflow, we'll be sure to let you know :)
As for destructive transforms, I wholeheartedly agree with you on that point in general, and I also prefer storing data in some kind of a source format, and then providing a transformed output separately as needed - an example that comes to mind is when people protect against XSS by escaping HTML before storing it in the database, while the correct course of action is of course to store it as entered, and escape it when outputting (different outputs might require different escaping strategies).
That said, we do make an exception in this specific case for two reasons. The first one is completely pragmatic - we prefer to keep the size of our repos lower, so we don't even commit the images before they're optimized (unless by mistake of course, though we try not to). The second reason sort of builds upon that - in an ideal world we would expect to already have fully optimized images before using them (e.g. a designer performing optimization before providing images to developers) - which implies that we don't really care enough about the non-optimized images to store them in our codebase history. So while the optimization is likely a destructive transformation, for our specific use-case we don't care about the original data at all, thus we're fine with this :)
Thank you for the interesting conversation and for acting so quickly on the original issue - during the updates to our build process we've actually decided to go with your advice regarding tracking the cached hashes in our repos - as it makes perfect sense to do that!
Cheers :)
For sure! It was good to talk through these things with you and hear how you all were are using the plugin.
And just as an FYI, I was able to get a lot done in the way of testing today and was able to work in both things we've discussed in this thread. If you're interested, I published the 2.0 version a little earlier—biggest change being a default of
{context: process.cwd()}but you can also now pass a function tonamespaceand dynamically build that up. And with the tests setup now, you shouldn't have to worry about updating in the future.Cheers mate.