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

Fix watch mode to listen to changes below the "longest common directory prefix" of relevant files, rather than only files below process.cwd(), while keeping event filtering intact #9267

Merged
merged 2 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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