Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/haydn/use-record-set. A React hook for quickly prototyping data models. 09qzj.csb.app
data graphql prototyping
Something went wrong. Try again.
use-record-set cli.ts
5.8 kB · 204 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205#!/usr/bin/env node
import { fork } from "child_process";import { watch, writeFileSync } from "fs";import { basename, dirname, resolve } from "path";import { pathToFileURL } from "url";import { Schema, printRecordSetSchema } from "./index";
const USAGE = `Usage: use-record-set print-schema <schema-file> [options]
Prints the GraphQL schema (SDL) generated from a useRecordSet schema.
Options: -o, --output <file> Write the schema to a file instead of stdout -e, --export <name> Name of the export containing the schema (default: the default export) -w, --watch Regenerate the schema whenever the schema file changes (requires --output) -h, --help Show this message`;
type Options = { file: string; output: string | null; exportName: string | null; watch: boolean;};
class UsageError extends Error {}
const HEADER = "# This file was automatically generated by use-record-set. Do not edit it manually.\n\n";
// TypeScript compiles `import()` to `require()` when targeting CommonJS, which can't load ES// modules. Creating the function at runtime keeps it as a native dynamic import.const importModule = new Function("specifier", "return import(specifier)") as ( specifier: string,) => Promise<{ [key: string]: unknown }>;
const parseArgs = (args: Array<string>): Options | null => { const positionals: Array<string> = []; let output: string | null = null; let exportName: string | null = null; let watch = false;
for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case "-h": case "--help": return null; case "-o": case "--output": case "-e": case "--export": { const value = args[++i]; if (value === undefined) { throw new UsageError(`Missing value for ${arg}`); } if (arg === "-o" || arg === "--output") { output = value; } else { exportName = value; } break; } case "-w": case "--watch": watch = true; break; default: if (arg.startsWith("-")) { throw new UsageError(`Unknown option: ${arg}`); } positionals.push(arg); } }
const [command, file, ...rest] = positionals;
if (command !== "print-schema") { throw new UsageError(command ? `Unknown command: ${command}` : "Missing command"); } if (!file) { throw new UsageError("Missing schema file"); } if (rest.length > 0) { throw new UsageError(`Unexpected argument: ${rest[0]}`); } if (watch && !output) { throw new UsageError("--watch requires --output"); }
return { file, output, exportName, watch };};
const loadSchema = async (file: string, exportName: string | null): Promise<Schema> => { const mod = await importModule(pathToFileURL(resolve(file)).href);
let schema = mod[exportName || "default"];
// CommonJS modules compiled from ES modules (e.g. by TypeScript or Babel) put their default // export on `exports.default`, which ends up nested one level deeper. if (!exportName && isObject(schema) && schema.__esModule === true) { schema = schema.default; }
const description = exportName ? `export named "${exportName}"` : "default export";
if (schema === undefined) { const available = Object.keys(mod).join(", ") || "none"; throw new Error(`${file} has no ${description} (available exports: ${available})`); } if (!isObject(schema)) { throw new Error(`The ${description} of ${file} is not a schema object`); }
return schema as Schema;};
const isObject = (value: unknown): value is { [key: string]: unknown } => typeof value === "object" && value !== null;
const printSchemaFile = async ({ file, output, exportName }: Options) => { const sdl = HEADER + printRecordSetSchema(await loadSchema(file, exportName)) + "\n"; if (output) { writeFileSync(output, sdl); } else { process.stdout.write(sdl); }};
// Each regeneration runs in a fresh process because Node caches imported modules (and anything// they import) for the life of the process.const watchSchemaFile = ({ file, output, exportName }: Options) => { const args = ["print-schema", file, "--output", output as string]; if (exportName) { args.push("--export", exportName); }
let running = false; let pending = false; let timeout: ReturnType<typeof setTimeout> | null = null;
const regenerate = () => { if (running) { pending = true; return; } running = true; fork(__filename, args).on("exit", (code) => { running = false; if (code === 0) { console.error(`Wrote ${output}`); } if (pending) { pending = false; regenerate(); } }); };
regenerate();
// Watching the directory rather than the file itself catches editors that save by replacing // the file, which would otherwise end the watch. watch(dirname(resolve(file)), (_, filename) => { if (filename !== basename(file)) { return; } // Editors often emit several events for a single save. if (timeout) { clearTimeout(timeout); } timeout = setTimeout(regenerate, 100); });
console.error(`Watching ${file} for changes…`);};
const main = async () => { let options: Options | null;
try { options = parseArgs(process.argv.slice(2)); } catch (error) { if (error instanceof UsageError) { console.error(`${error.message}\n\n${USAGE}`); process.exitCode = 1; return; } throw error; }
if (options === null) { console.log(USAGE); } else if (options.watch) { watchSchemaFile(options); } else { await printSchemaFile(options); }};
main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1;});