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

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Phil Pluckthun and committed by
GitHub
8f72977e 0e917637

+64 -24
+5
.changeset/selfish-adults-heal.md
··· 1 + --- 2 + '@0no-co/graphqlsp': patch 3 + --- 4 + 5 + Swap-write introspection file instead of overwriting it directly
+59 -24
packages/graphqlsp/src/graphql/getSchema.ts
··· 1 + import type { Stats, PathLike } from 'node:fs'; 2 + import fs from 'node:fs/promises'; 1 3 import path from 'path'; 2 - import fs from 'fs'; 3 4 4 5 import type { GraphQLSchema, IntrospectionQuery } from 'graphql'; 5 6 ··· 15 16 import { ts } from '../ts'; 16 17 import { Logger } from '../index'; 17 18 19 + const statFile = ( 20 + file: PathLike, 21 + predicate: (stat: Stats) => boolean 22 + ): Promise<boolean> => { 23 + return fs 24 + .stat(file) 25 + .then(predicate) 26 + .catch(() => false); 27 + }; 28 + 29 + const touchFile = async (file: PathLike): Promise<void> => { 30 + try { 31 + const now = new Date(); 32 + await fs.utimes(file, now, now); 33 + } catch (_error) {} 34 + }; 35 + 36 + /** Writes a file to a swapfile then moves it into place to prevent excess change events. */ 37 + export const swapWrite = async ( 38 + target: PathLike, 39 + contents: string 40 + ): Promise<void> => { 41 + if (!(await statFile(target, stat => stat.isFile()))) { 42 + // If the file doesn't exist, we can write directly, and not 43 + // try-catch so the error falls through 44 + await fs.writeFile(target, contents); 45 + } else { 46 + // If the file exists, we write to a swap-file, then rename (i.e. move) 47 + // the file into place. No try-catch around `writeFile` for proper 48 + // directory/permission errors 49 + const tempTarget = target + '.tmp'; 50 + await fs.writeFile(tempTarget, contents); 51 + try { 52 + await fs.rename(tempTarget, target); 53 + } catch (error) { 54 + await fs.unlink(tempTarget); 55 + throw error; 56 + } finally { 57 + // When we move the file into place, we also update its access and 58 + // modification time manually, in case the rename doesn't trigger 59 + // a change event 60 + await touchFile(target); 61 + } 62 + } 63 + }; 64 + 18 65 async function saveTadaIntrospection( 19 66 introspection: IntrospectionQuery, 20 67 tadaOutputLocation: string, ··· 28 75 }); 29 76 30 77 let output = tadaOutputLocation; 31 - let stat: fs.Stats | undefined; 32 78 33 - try { 34 - stat = await fs.promises.stat(output); 35 - } catch (error) { 36 - logger(`Failed to resolve path @ ${output}`); 37 - } 38 - 39 - if (!stat) { 40 - try { 41 - stat = await fs.promises.stat(path.dirname(output)); 42 - if (!stat.isDirectory()) { 43 - logger(`Output file is not inside a directory @ ${output}`); 44 - return; 45 - } 46 - } catch (error) { 47 - logger(`Directory does not exist @ ${output}`); 48 - return; 49 - } 50 - } else if (stat.isDirectory()) { 79 + if (await statFile(output, stat => stat.isDirectory())) { 51 80 output = path.join(output, 'introspection.d.ts'); 52 - } else if (!stat.isFile()) { 53 - logger(`No file or directory found on path @ ${output}`); 81 + } else if ( 82 + !(await statFile(path.dirname(output), stat => stat.isDirectory())) 83 + ) { 84 + logger(`Output file is not inside a directory @ ${output}`); 54 85 return; 55 86 } 56 87 57 - await fs.promises.writeFile(output, contents); 58 - logger(`Introspection saved to path @ ${output}`); 88 + try { 89 + await swapWrite(output, contents); 90 + logger(`Introspection saved to path @ ${output}`); 91 + } catch (error) { 92 + logger(`Failed to write introspection @ ${error}`); 93 + } 59 94 } 60 95 61 96 export interface SchemaRef {