Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

fix: Swap-write `gql.tada` output file instead of writing to it directly (#291)

Changed files
+64 -24
.changeset
packages
graphqlsp
src
graphql
+5
.changeset/selfish-adults-heal.md
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Swap-write introspection file instead of overwriting it directly
+59 -24
packages/graphqlsp/src/graphql/getSchema.ts
···
+
import type { Stats, PathLike } from 'node:fs';
+
import fs from 'node:fs/promises';
import path from 'path';
-
import fs from 'fs';
import type { GraphQLSchema, IntrospectionQuery } from 'graphql';
···
import { ts } from '../ts';
import { Logger } from '../index';
+
const statFile = (
+
file: PathLike,
+
predicate: (stat: Stats) => boolean
+
): Promise<boolean> => {
+
return fs
+
.stat(file)
+
.then(predicate)
+
.catch(() => false);
+
};
+
+
const touchFile = async (file: PathLike): Promise<void> => {
+
try {
+
const now = new Date();
+
await fs.utimes(file, now, now);
+
} catch (_error) {}
+
};
+
+
/** Writes a file to a swapfile then moves it into place to prevent excess change events. */
+
export const swapWrite = async (
+
target: PathLike,
+
contents: string
+
): Promise<void> => {
+
if (!(await statFile(target, stat => stat.isFile()))) {
+
// If the file doesn't exist, we can write directly, and not
+
// try-catch so the error falls through
+
await fs.writeFile(target, contents);
+
} else {
+
// If the file exists, we write to a swap-file, then rename (i.e. move)
+
// the file into place. No try-catch around `writeFile` for proper
+
// directory/permission errors
+
const tempTarget = target + '.tmp';
+
await fs.writeFile(tempTarget, contents);
+
try {
+
await fs.rename(tempTarget, target);
+
} catch (error) {
+
await fs.unlink(tempTarget);
+
throw error;
+
} finally {
+
// When we move the file into place, we also update its access and
+
// modification time manually, in case the rename doesn't trigger
+
// a change event
+
await touchFile(target);
+
}
+
}
+
};
+
async function saveTadaIntrospection(
introspection: IntrospectionQuery,
tadaOutputLocation: string,
···
});
let output = tadaOutputLocation;
-
let stat: fs.Stats | undefined;
-
try {
-
stat = await fs.promises.stat(output);
-
} catch (error) {
-
logger(`Failed to resolve path @ ${output}`);
-
}
-
-
if (!stat) {
-
try {
-
stat = await fs.promises.stat(path.dirname(output));
-
if (!stat.isDirectory()) {
-
logger(`Output file is not inside a directory @ ${output}`);
-
return;
-
}
-
} catch (error) {
-
logger(`Directory does not exist @ ${output}`);
-
return;
-
}
-
} else if (stat.isDirectory()) {
+
if (await statFile(output, stat => stat.isDirectory())) {
output = path.join(output, 'introspection.d.ts');
-
} else if (!stat.isFile()) {
-
logger(`No file or directory found on path @ ${output}`);
+
} else if (
+
!(await statFile(path.dirname(output), stat => stat.isDirectory()))
+
) {
+
logger(`Output file is not inside a directory @ ${output}`);
return;
}
-
await fs.promises.writeFile(output, contents);
-
logger(`Introspection saved to path @ ${output}`);
+
try {
+
await swapWrite(output, contents);
+
logger(`Introspection saved to path @ ${output}`);
+
} catch (error) {
+
logger(`Failed to write introspection @ ${error}`);
+
}
}
export interface SchemaRef {