Skip to content

Commit

Permalink
Add writing utility
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten committed Apr 14, 2024
1 parent a5d2ce3 commit 60238e8
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 10 deletions.
9 changes: 5 additions & 4 deletions packages/cli-utils/src/commands/generate-output/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from '@gql.tada/internal';

import type { TTY } from '../../term';
import type { WriteTarget } from '../shared';
import { writeOutput } from '../shared';
import * as logger from './logger';

interface Options {
Expand Down Expand Up @@ -57,10 +59,9 @@ export async function* run(tty: TTY, opts: Options) {
throw logger.externalError('Could not generate introspection output', error);
}

let destination: string;
let destination: WriteTarget;
if (!opts.output && tty.pipeTo) {
tty.pipeTo.write(contents);
return;
destination = tty.pipeTo;
} else if (opts.output) {
destination = path.resolve(process.cwd(), opts.output);
} else if (pluginConfig.tadaOutputLocation) {
Expand All @@ -80,7 +81,7 @@ export async function* run(tty: TTY, opts: Options) {
}

try {
await fs.writeFile(destination, contents);
await writeOutput(destination, contents);
} catch (error) {
throw logger.externalError('Something went wrong while writing the introspection file', error);
}
Expand Down
11 changes: 5 additions & 6 deletions packages/cli-utils/src/commands/generate-schema/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { GraphQLSPConfig, LoadConfigResult } from '@gql.tada/internal';
import { load, loadConfig, parseConfig } from '@gql.tada/internal';

import type { TTY } from '../../term';
import { getGraphQLSPConfig } from '../../lsp';
import { getTsConfig } from '../../tsconfig';
import type { WriteTarget } from '../shared';
import { writeOutput } from '../shared';
import * as logger from './logger';

interface Options {
Expand All @@ -32,10 +32,9 @@ export async function* run(tty: TTY, opts: Options) {
throw logger.errorMessage('Failed to load schema.');
}

let destination: string;
let destination: WriteTarget;
if (!opts.output && tty.pipeTo) {
tty.pipeTo.write(printSchema(schema));
return;
destination = tty.pipeTo;
} else if (opts.output) {
destination = path.resolve(process.cwd(), opts.output);
} else {
Expand Down Expand Up @@ -70,7 +69,7 @@ export async function* run(tty: TTY, opts: Options) {
}

try {
await fs.writeFile(destination, printSchema(schema));
await writeOutput(destination, printSchema(schema));
} catch (error) {
throw logger.externalError('Something went wrong while writing the introspection file', error);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-utils/src/commands/shared/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './logger';
export * from './utils';
57 changes: 57 additions & 0 deletions packages/cli-utils/src/commands/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { WriteStream } from 'node:tty';
import type { PathLike } from 'node:fs';
import * as fs from 'node:fs/promises';

/** Checks whether a file exists on disk */
export const fileExists = (file: PathLike): Promise<boolean> =>
fs
.stat(file)
.then((stat) => stat.isFile())
.catch(() => false);

const touchFile = async (file: PathLike): Promise<void> => {
try {
const now = new Date();
await fs.utimes(file, now, now);
} catch (_error) {}
};

export type WriteTarget = PathLike | WriteStream;

/** Writes a file to a swapfile then moves it into place to prevent excess change events. */
export const writeOutput = async (target: WriteTarget, contents: string): Promise<void> => {
if (target && typeof target === 'object' && 'writable' in target) {
// If we get a WritableStream (e.g. stdout), we write to that
// but we listen for errors and wait for it to flush fully
return await new Promise((resolve, reject) => {
target.write(contents, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
} else if (!(await fileExists(target))) {
// 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);
}
}
};

0 comments on commit 60238e8

Please sign in to comment.