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

watch: fix reloading for rebuild/compiled files #45259

Closed

Conversation

ruyadorno
Copy link
Member

TLDR; I was trying to use --watch in a JS project that requires code from a build/*.js module that is transpiled with tsc but the watcher would not trigger a reloading once the file was regenerated. Having a sticky list of filtered files fixes that.


When watching folders that contain files that result from a build step, e.g: TypeScript output, stored template result, etc. The watcher will not reload the application in case one of these resulting files gets regenerated, given that when these files are removed they're no longer tracked by the watcher.

This changeset enables reloading the app when changing files that result from a build step by removing the reset of the tracked filtered files on every reload. Fixing the ability to watch and reload the app when a previously-known output file is changed.

cc @MoLow

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Oct 31, 2022
When watching folders that contain files that result from a build step,
e.g: TypeScript output, stored template result, etc. The watcher will
not reload the application in case one of these resulting files gets
regenerated, given that when these files are removed they're no longer
tracked by the watcher.

This changeset enables reloading the app when changing files that result
from a build step by removing the reset of the tracked filtered files on
every reload. Fixing the ability to watch and reload the app when a
previously-known output file is changed.

Signed-off-by: Ruy Adorno <ruyadorno@google.com>
@ruyadorno ruyadorno force-pushed the fix-watch-reload-rebuilt-files branch from 15b48f6 to df6b0cd Compare October 31, 2022 19:52
child.kill();
await once(child, 'exit');

// TODO(ruyadorno): fs.watch is flaky when removing files from a watched
Copy link
Member

Choose a reason for hiding this comment

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

Can you create an issue for this and assign it to me? And can you give platform information for this line, since fs.watch has different implementations on different OS

await once(child.stdout, 'data');
rmSync(dependency, { force: true });

await setTimeout(600); // throttle + 100
Copy link
Member

Choose a reason for hiding this comment

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

Can you use common.platformTimeout(600) for all values for setTimeout?

// This should be an assertion for the failure instead of an if block once
// the source of this flakyness gets resolved.
if (stdout.match(/Failed/g)?.length) {
assert.strictEqual(stdout.split('Failed')[1].match(/Restarting/g)?.length, 1, new Error('should have restarted after fail'));
Copy link
Member

Choose a reason for hiding this comment

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

Can you also add an assert for the length of split('Failed') operation?

it('should watch changes to previously loaded dependencies', async () => {
const dependencyContent = 'module.exports = {}';
const dependency = createTmpFile(dependencyContent);
const relativeDependencyPath = `./${path.basename(dependency)}`;
Copy link
Member

Choose a reason for hiding this comment

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

does path.relative(path.basename(dependency)) produces the same result for this line? if yes, can we use it?

const child = spawn(execPath, ['--watch', '--no-warnings', dependant], { encoding: 'utf8' });
child.stdout.on('data', (data) => { stdout += data; });
child.stderr.on('data', (data) => { stderr += data; });
child.on('error', (err) => { throw err; });
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
child.on('error', (err) => { throw err; });
child.on('error', common.mustNotCall());

Comment on lines +207 to +209
if (stdout.match(/Failed/g)?.length) {
assert.strictEqual(stdout.split('Failed')[1].match(/Restarting/g)?.length, 1, new Error('should have restarted after fail'));
assert.ok(stderr.match(/MODULE_NOT_FOUND/g)?.length, new Error('should have failed for not finding the removed file'));
Copy link
Member

@VoltrexKeyva VoltrexKeyva Nov 1, 2022

Choose a reason for hiding this comment

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

Suggested change
if (stdout.match(/Failed/g)?.length) {
assert.strictEqual(stdout.split('Failed')[1].match(/Restarting/g)?.length, 1, new Error('should have restarted after fail'));
assert.ok(stderr.match(/MODULE_NOT_FOUND/g)?.length, new Error('should have failed for not finding the removed file'));
if (stdout.includes('Failed')) {
assert.ok(stdout.split('Failed')[1].includes('Restarting'), new Error('should have restarted after fail'));
assert.ok(stderr.includes('MODULE_NOT_FOUND'), new Error('should have failed for not finding the removed file'));

We should use %String.prototype.includes% here instead because we're only checking if those values are included, although for the first assert we can keep it as is if we really need to check that Restarting only appears once.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or assert.match, to get a helpful string comparison between what was expected in case the test fails.

Copy link
Member

@MoLow MoLow left a comment

Choose a reason for hiding this comment

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

this change can lead to undesired (or at least debatable) behaviors,
for example when running

const storage = require('./in-memory');

and then changing to

const storage = require('./hdd');

I would expect changes to ./in-memory to be ignored since they are not part of the dependency tree.
as I mentioned this expectation of mine is debatable, but I think the real issue this PR should face (and that could simplify tests), is to watch modules that do not exist yet but that are required.

meaning a better implementation would be to send a watch:require / watch:import message over IPC whenever we throw MODULE_NOT_FOUND or ERR_MODULE_NOT_FOUND, respectively, WDYT?

@MoLow
Copy link
Member

MoLow commented Nov 1, 2022

I am not blocking this PR since already blocked

@MoLow MoLow added the watch-mode Issues and PRs related to watch mode label Nov 1, 2022
@ruyadorno ruyadorno changed the title watch: fix reloading for built/compiled files watch: fix reloading for rebuild/compiled files Nov 3, 2022
@ruyadorno
Copy link
Member Author

@MoLow that's a great point! That was precisely my initial approach (sending an extra watch:require on MODULE_NOT_FOUND) but it turns out that I couldn't find a sensible way to have a proper filename (similar to how we get it to send it over to reportModuleToWatchMode()) in modules/cjs/loader.js. Thinking more about it, even if we were to put together an algo to try to reconstruct that full filename it also brings other implications/considerations since there would be no way to stat that file like a regular required file would.

Digging up more I realized that by having filenames be sticky to that list of filteredFiles would be enough to solve my use case when rebuilding files such as in a TypeScript transpilation so I went with that as a very actionable plan B instead.

With that in mind, if anyone wants to take a stab at hooking up to the failure paths in both cjs/esm I agree that would be the best approach but otherwise if that turns out to be a dead end then I'd like to consider this as a fix since it unlocks the watcher for a bunch of very useful cases that I'd particularly like to see solved.

@MoLow
Copy link
Member

MoLow commented Nov 3, 2022

@ruyadorno what do you think about watching the entire parent directory of the entry module in case of MODULE_NOT_FOUND, that sounds like a better solution than just continuing to watch anything that was ever part of the dependency graph?

@ruyadorno
Copy link
Member Author

@MoLow I get the intent of trying to be as accurate as possible and I can relate to the sentiment that continuously watch previously seen files sounds wrong in theory.

With that said, when I think in practical terms I still see it as the best option at hand, or at least the option with least inconveniences. From my understanding the built in --watch is meant for an improved developer experience which means short-lived sessions, minimizing enormously the problem of having these references of previously seen files remain in the filteredFiles list. While the proposed alternative: "...watching the entire parent directory of the entry module in case of MODULE_NOT_FOUND" sounds more risky depending on where that file lives (node_modules? shared folders?), watching its parent directory might mean that now thousands of additional files may be watch unnecessarily.

I don't really have a strong opinion here 🙃 just trying to explain my current line of thought and I can be easily convinced otherwise. The one thing I have a strong opinion about is that I would prefer a solution that is easier to test, since I'm still very unhappy about the current test included in this PR (which means I'm also very open to different ideas on how to test the current impl).

@MoLow
Copy link
Member

MoLow commented Nov 5, 2022

@ruyadorno I assume we can try and make something more accurate than watching the entire directory, but I agree with you that this should be fixed.

@ruyadorno
Copy link
Member Author

thanks for following up with this @MoLow! I'll close this PR in favor of #45348 👍

@ruyadorno ruyadorno closed this Nov 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-ci PRs that need a full CI run. watch-mode Issues and PRs related to watch mode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants