Skip to content

Commit

Permalink
Fix watch mode to listen to changes below the "longest common directo…
Browse files Browse the repository at this point in the history
…ry prefix" of relevant files, rather than only files below `process.cwd()`, while keeping event filtering intact (#9267)

* fix: Listen for file changes in watch mode in longest common directory

Prior to #9009, which moved from Chokidar to `@parcel/watcher`, the behavior
was to watch for all relevant files. However, since switching to
`@parcel/watcher`, the new behavior has been to watch all files below
`process.cwd()`, and then filter the change events for only the relevant files.
This approach works fine, except that it's possible for a valid config to
reference paths outside of the current working directory (e.g. `documents:
"../some-other/*.graphql`), and these paths are included in build mode, but
were not being included in watch mode because they're outside of
`process.cwd()`.

This commit adds logic, after parsing all relevant file paths, to find the
"longest common directory prefix" of all those file paths, i.e. the "highest"
(closest to `/`) directory that contains all the relevant file paths. Then,
when subscribing to the Parcel watcher, this directory is used instead of
`process.cwd()`. For example, the longest common directory of the paths
`/foo/bar/*.graphql` and `/foo/fizz/*.graphql` would be `/foo`.

Note that the filtering behavior is left unchanged, and this only affects the
root path given to the Parcel watcher. When an event is received, the filtering
can still filter out irrelevant paths, including those filtered by
`config.watchPattern` if it's defined.

* chore: Add `yarn watch:examples` and `dev-test-outer-dir` for watch mode testing

This adds a directory "outside" of `dev-test`, and also adds a new script
command `yarn watch:examples` which is the "watch mode" equivalent of
`yarn generate:examples`. It adds a dependency on a `githunt` document in
`dev-test-outer-dir`, and if all is working as expected, you should be able to
edit this file while in watch mode to trigger a rebuild, and it should also be
included in build mode. That is, if you run `yarn generate:examples`, or
`yarn watch:examples`, there should be no changes to the Git repo.
  • Loading branch information
milesrichardson authored Apr 4, 2023
1 parent 6e3fd38 commit 1837493
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-candles-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/cli': patch
---

Fix watch mode to listen to longest common directory prefix of relevant files, rather than only files below the current working directory (fixes #9266).
8 changes: 8 additions & 0 deletions dev-test-outer-dir/githunt/current-user.query.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# When running `yarn watch:examples`, updating this file should trigger rebuild,
# even though it's "outside" of the CWD of `dev-test/codegen.ts`
query CurrentUserForProfileFromOutsideDirectory {
currentUser {
login
avatar_url
}
}
2 changes: 1 addition & 1 deletion dev-test/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const config: CodegenConfig = {
},
'./dev-test/githunt/graphql-declared-modules.d.ts': {
schema: './dev-test/githunt/schema.json',
documents: ['./dev-test/githunt/**/*.graphql'],
documents: ['./dev-test/githunt/**/*.graphql', './dev-test-outer-dir/githunt/**/*.graphql'],
plugins: ['typescript-graphql-files-modules'],
},
'./dev-test/githunt/typed-document-nodes.ts': {
Expand Down
17 changes: 9 additions & 8 deletions dev-test/githunt/graphql-declared-modules.d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
declare module '*/comment-added.subscription.graphql' {
declare module '*/current-user.query.graphql' {
import { DocumentNode } from 'graphql';
const defaultDocument: DocumentNode;
export const onCommentAdded: DocumentNode;
export const CurrentUserForProfileFromOutsideDirectory: DocumentNode;
export const CurrentUserForProfile: DocumentNode;

export default defaultDocument;
}

declare module '*/comment.query.graphql' {
declare module '*/comment-added.subscription.graphql' {
import { DocumentNode } from 'graphql';
const defaultDocument: DocumentNode;
export const Comment: DocumentNode;
export const onCommentAdded: DocumentNode;

export default defaultDocument;
}

declare module '*/comments-page-comment.fragment.graphql' {
declare module '*/comment.query.graphql' {
import { DocumentNode } from 'graphql';
const defaultDocument: DocumentNode;
export const CommentsPageComment: DocumentNode;
export const Comment: DocumentNode;

export default defaultDocument;
}

declare module '*/current-user.query.graphql' {
declare module '*/comments-page-comment.fragment.graphql' {
import { DocumentNode } from 'graphql';
const defaultDocument: DocumentNode;
export const CurrentUserForProfile: DocumentNode;
export const CommentsPageComment: DocumentNode;

export default defaultDocument;
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"generate:examples:esm": "node packages/graphql-codegen-cli/dist/esm/bin.js --require dotenv/config --config ./dev-test/codegen.ts dotenv_config_path=dev-test/.env",
"generate:examples:cjs": "node packages/graphql-codegen-cli/dist/cjs/bin.js --require dotenv/config --config ./dev-test/codegen.ts dotenv_config_path=dev-test/.env",
"generate:examples": "yarn generate:examples:cjs",
"watch:examples:esm": "node packages/graphql-codegen-cli/dist/esm/bin.js --require dotenv/config --watch --config ./dev-test/codegen.ts dotenv_config_path=dev-test/.env",
"watch:examples:cjs": "node packages/graphql-codegen-cli/dist/cjs/bin.js --require dotenv/config --watch --config ./dev-test/codegen.ts dotenv_config_path=dev-test/.env",
"watch:examples": "yarn watch:examples:cjs",
"examples:codegen": "set -o xtrace && eval $(node scripts/print-example-ci-command.js codegen)",
"examples:build": "set -o xtrace && eval $(node scripts/print-example-ci-command.js build)",
"examples:test:end2end": "set -o xtrace && eval $(node scripts/print-example-ci-command.js test:end2end)"
Expand Down
74 changes: 67 additions & 7 deletions packages/graphql-codegen-cli/src/utils/watcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { join } from 'path';
import { access } from 'node:fs/promises';
import { join, isAbsolute, resolve, sep } from 'path';
import { normalizeInstanceOrArray, normalizeOutputParam, Types } from '@graphql-codegen/plugin-helpers';
import { isValidPath } from '@graphql-tools/utils';
import type { subscribe } from '@parcel/watcher';
Expand All @@ -17,8 +18,8 @@ function log(msg: string) {
getLogger().info(` ${msg}`);
}

function emitWatching() {
log(`${logSymbols.info} Watching for changes...`);
function emitWatching(watchDir: string) {
log(`${logSymbols.info} Watching for changes in ${watchDir}...`);
}

export const createWatcher = (
Expand Down Expand Up @@ -62,6 +63,8 @@ export const createWatcher = (
let watcherSubscription: Awaited<ReturnType<typeof subscribe>>;

const runWatcher = async () => {
const watchDirectory = await findHighestCommonDirectory(files);

const parcelWatcher = await import('@parcel/watcher');
debugLog(`[Watcher] Parcel watcher loaded...`);

Expand All @@ -71,10 +74,10 @@ export const createWatcher = (
if (!isShutdown) {
executeCodegen(initalContext)
.then(onNext, () => Promise.resolve())
.then(() => emitWatching());
.then(() => emitWatching(watchDirectory));
}
}, 100);
emitWatching();
emitWatching(watchDirectory);

const ignored: string[] = [];
for (const entry of Object.keys(config.generates).map(filename => ({
Expand All @@ -92,7 +95,7 @@ export const createWatcher = (
}

watcherSubscription = await parcelWatcher.subscribe(
process.cwd(),
watchDirectory,
async (_, events) => {
// it doesn't matter what has changed, need to run whole process anyway
await Promise.all(
Expand All @@ -105,7 +108,7 @@ export const createWatcher = (

lifecycleHooks(config.hooks).onWatchTriggered(eventName, path);
debugLog(`[Watcher] triggered due to a file ${eventName} event: ${path}`);
const fullPath = join(process.cwd(), path);
const fullPath = join(watchDirectory, path);
// In ESM require is not defined
try {
delete require.cache[fullPath];
Expand Down Expand Up @@ -156,3 +159,60 @@ export const createWatcher = (
});
});
};

/**
* Given a list of file paths (each of which may be absolute, or relative to
* `process.cwd()`), find absolute path of the "highest" common directory,
* i.e. the directory that contains all the files in the list.
*
* @param files List of relative and/or absolute file paths (or micromatch patterns)
*/
const findHighestCommonDirectory = async (files: string[]): Promise<string> => {
// Map files to a list of basePaths, where "base" is the result of mm.scan(pathOrPattern)
// e.g. mm.scan("/**/foo/bar").base -> "/" ; mm.scan("/foo/bar/**/fizz/*.graphql") -> /foo/bar
const dirPaths = files
.map(filePath => (isAbsolute(filePath) ? filePath : resolve(filePath)))
.map(patterned => mm.scan(patterned).base);

// Return longest common prefix if it's accessible, otherwise process.cwd()
return (async (maybeValidPath: string) => {
debugLog(`[Watcher] Longest common prefix of all files: ${maybeValidPath}...`);
try {
await access(maybeValidPath);
return maybeValidPath;
} catch {
log(`[Watcher] Longest common prefix (${maybeValidPath}) is not accessible`);
log(`[Watcher] Watching current working directory (${process.cwd()}) instead`);
return process.cwd();
}
})(longestCommonPrefix(dirPaths.map(path => path.split(sep))).join(sep));
};

/**
* Find the longest common prefix of an array of paths, where each item in
* the array an array of path segments which comprise an absolute path when
* joined together by a path separator
*
* Adapted from:
* https://duncan-mcardle.medium.com/leetcode-problem-14-longest-common-prefix-javascript-3bc6a2f777c4
*
* @param splitPaths An array of arrays, where each item is a path split by its separator
* @returns An array of path segments representing the longest common prefix of splitPaths
*/
const longestCommonPrefix = (splitPaths: string[][]): string[] => {
// Return early on empty input
if (!splitPaths.length) {
return [];
}

// Loop through the segments of the first path
for (let i = 0; i <= splitPaths[0].length; i++) {
// Check if this path segment is present in the same position of every path
if (!splitPaths.every(string => string[i] === splitPaths[0][i])) {
// If not, return the path segments up to and including the previous segment
return splitPaths[0].slice(0, i);
}
}

return splitPaths[0];
};
2 changes: 2 additions & 0 deletions website/src/pages/docs/custom-codegen/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ You can also test the integration of your plugin with the codegen core and cli,

To do that, make sure everything is built by using `yarn build` in the root directory, then you can use it in `./dev-test/codegen.ts`, and run `yarn generate:examples` in the project root directory to run it.

If you would like to test "watch mode" in the same way, you can run `yarn watch:examples`.

## 9. Documentation

GraphQL Code Generator website has API Reference for all our plugins. Most of the documentation is generated from code, and some of it is written manually.
Expand Down

0 comments on commit 1837493

Please sign in to comment.