From 8226a44c20919a8c775647dab6a3569d544a4dbd Mon Sep 17 00:00:00 2001 From: Vicary A Date: Wed, 13 Mar 2024 13:27:35 +0800 Subject: [PATCH] feat(cli): add interactive shell for automatic patch --- README.md | 49 +++++--- cli.ts | 200 ++++++++++++++++++++++++++++++++ deno.json | 2 +- deps.ts | 20 ++-- dev.ts | 1 + diff.json | 3 + examples/graphql-yoga/deno.json | 2 +- mod.ts | 14 ++- schema.ts | 6 + 9 files changed, 266 insertions(+), 31 deletions(-) create mode 100644 cli.ts create mode 100644 diff.json diff --git a/README.md b/README.md index 06bee2f..9638c04 100644 --- a/README.md +++ b/README.md @@ -15,29 +15,42 @@ A simple GraphQL server for Deno Fresh. ## Installation -1. [Create a fresh project](https://fresh.deno.dev/docs/getting-started/create-a-project) - or checkout your existing Fresh project. +```bash +# Create a Fresh project +> deno run -A -r https://fresh.deno.dev -1. Add the following lines to your `dev.ts`: +# Add fresh-graphql to your project +> deno run -A jsr:@vicary/fresh-graphql +``` + +### Manual Installation - ```diff - import "https://deno.land/x/dotenv/load.ts"; +You may also patch the files manually if you have modified `dev.ts` or +`deno.json` for an existing Fresh project. - import dev from "$fresh/dev.ts"; - +import { dev as graphql } from "@vicary/fresh-graphql"; +#### dev.ts - +await graphql(import.meta.url); - await dev(import.meta.url, "./main.ts"); - ``` +```diff +-#!/usr/bin/env -S deno run -A --watch=static/,routes/ ++#!/usr/bin/env -S deno run -A --watch=static/,routes/,graphql/ -1. Include the `graphql/` directory in your deno.json. +import "https://deno.land/x/dotenv/load.ts"; - ```diff - "tasks": { - - "start": "deno run -A --watch=static/,routes/ dev.ts", - + "start": "deno run -A --watch=static/,routes/,graphql/ dev.ts", - } - ``` +import dev from "$fresh/dev.ts"; ++import { dev as graphql } from "@vicary/fresh-graphql"; + ++await graphql(import.meta.url); +await dev(import.meta.url, "./main.ts"); +``` + +#### deno.json + +```diff +"tasks": { +- "start": "deno run -A --watch=static/,routes/ dev.ts", ++ "start": "deno run -A --watch=static/,routes/,graphql/ dev.ts", +} +``` ## Usage @@ -135,7 +148,7 @@ export const resolver = async function* (_, { from }) { Supported, documentations coming. -You may read `dev.ts` if you need it now. +If you need it now, you may read our source code. ### Side notes diff --git a/cli.ts b/cli.ts new file mode 100644 index 0000000..6c2cb38 --- /dev/null +++ b/cli.ts @@ -0,0 +1,200 @@ +import { assert, colors, log } from "./deps.ts"; +import diff from "./diff.json" with { type: "json" }; + +log.setup({ + handlers: { + console: new log.ConsoleHandler("DEBUG", { + formatter: ((): log.FormatterFunction => { + const levelFormatters: Record< + string, + (record: log.LogRecord) => string + > = { + CRITICAL: (record) => colors.red(colors.bold(`! ${record.msg}`)), + ERROR: (record) => `${colors.red(colors.bold("✖"))} ${record.msg}`, + WARN: (record) => `${colors.yellow(colors.bold("!"))} ${record.msg}`, + INFO: (record) => `${colors.blue(colors.bold("ℹ"))} ${record.msg}`, + DEBUG: (record) => `• ${record.msg}`, + }; + + const defaultFormatter = (record: log.LogRecord) => + `${levelFormatters[record.levelName]} ${record.msg}`; + + return (record) => + (levelFormatters[record.levelName] ?? defaultFormatter)(record); + })(), + }), + }, + loggers: { + default: { + level: "DEBUG", + handlers: ["console"], + }, + }, +}); + +async function main() { + if (!await installPackage()) { + log.error("Unable to install fresh-graphql package."); + return; + } + + if (!await ensurePatchExecutable()) { + log.warn("`patch` not found in your environment."); + return; + } + + if (promptYesNo("Do you want fresh-graphql to patch your `dev.ts`?", true)) { + if (!await patchDev()) { + log.error("Unable to patch dev.ts, please try to do it manually."); + log.info( + `Our patch works with an unmodified dev.ts generated after Fresh 1.6.5.`, + ); + } + } + + if (promptYesNo("Do you want fresh-graphql to patch to `deno.json`?", true)) { + if (!await patchDenoJson()) { + log.error("Unable to patch deno.json, please try to do it manually."); + } + } +} + +await main(); + +async function installPackage() { + const { success } = await new Deno.Command( + "deno", + { args: ["add", "jsr:@vicary/fresh-graphql"] }, + ).spawn().status; + + return success; +} + +async function ensurePatchExecutable() { + try { + const { success } = await new Deno.Command( + "patch", + { + args: ["--version"], + stdout: "null", + stderr: "null", + }, + ).spawn().status; + + assert(success); + + return true; + } catch (e) { + if (!(e instanceof Deno.errors.NotFound)) throw e; + + return false; + } +} + +function promptYesNo(question: string, defaultYes = false) { + const defaultAnswer = defaultYes ? "Y/n" : "y/N"; + const answer = prompt(`${question} [${defaultAnswer}]`); + + if (!answer) { + return defaultYes; + } + + return answer.trim().toLowerCase() === "y"; +} + +async function patchDev() { + // Ensure file exists + await Deno.stat("dev.ts"); + + return await attemptPatch(); + + async function attemptPatch(): Promise { + const en = new TextEncoder(); + const de = new TextDecoder(); + + const runPatch = async (...args: string[]) => { + const proc = new Deno.Command("patch", { + args, + stdin: "piped", + stdout: "piped", + stderr: "piped", + }).spawn(); + + const stdin = proc.stdin.getWriter(); + stdin.write(en.encode(diff.dev)); + stdin.close(); + + return await proc.output(); + }; + + // Dry-run + { + const out = await runPatch("-NCs", "./dev.ts"); + if (!out.success) { + const stdout = de.decode(out.stdout); + + if (stdout.includes("previously applied")) { + log.info("Your dev.ts is already patched, skipping."); + + return true; + } + + return false; + } + } + + // Actual run + const out = await runPatch("-Ns", "./dev.ts"); + if (!out.success) { + return false; + } + + log.info(`dev.ts patched successfully.`); + + return true; + } +} + +async function patchDenoJson(): Promise { + // Ensure file exists + await Deno.stat("deno.json"); + + // Apply the patch + try { + // Read the JSON + const json = JSON.parse( + new TextDecoder().decode(await Deno.readFile("deno.json")), + ); + + const task = json.tasks.start; + + if (!task.includes("--watch=static/,routes/ dev.ts")) { + if ( + task.includes("--watch=static/,routes/,graphql/ dev.ts") + ) { + log.info(`Your deno.json is already patched, skipping.`); + + return true; + } + + return false; + } + + json.tasks.start = task.replace( + "--watch=static/,routes/ dev.ts", + "--watch=static/,routes/,graphql/ dev.ts", + ); + + // Write the JSON back + await Deno.writeFile( + "deno.json", + new TextEncoder().encode(JSON.stringify(json, null, 2) + "\n"), + ); + } catch { + return false; + } + + log.info(`deno.json patched successfully.`); + + return true; +} diff --git a/deno.json b/deno.json index 8105402..e267bd4 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@vicary/fresh-graphql", - "version": "0.1.5", + "version": "0.2.6", "exports": "./mod.ts", "lock": false, "nodeModulesDir": false diff --git a/deps.ts b/deps.ts index 99ebdee..d1ec7d2 100644 --- a/deps.ts +++ b/deps.ts @@ -1,12 +1,14 @@ -export { assert } from "jsr:@std/assert@^0.219.1"; -export { ensureDir, walk } from "jsr:@std/fs@^0.219.1"; -export { - dirname, - fromFileUrl, - join, - parse as parsePath, - toFileUrl, -} from "jsr:@std/path@^0.219.1"; +export { assert } from "jsr:@std/assert@^0.219.1/assert"; +export * as colors from "jsr:@std/fmt@^0.219.1/colors"; +export { ensureDir } from "jsr:@std/fs@^0.219.1/ensure_dir"; +export { walk } from "jsr:@std/fs@^0.219.1/walk"; +export * as log from "jsr:@std/log@^0.219.1"; +export { dirname } from "jsr:@std/path@^0.219.1/dirname"; +export { fromFileUrl } from "jsr:@std/path@^0.219.1/from_file_url"; +export { join } from "jsr:@std/path@^0.219.1/join"; +export { parse as parsePath } from "jsr:@std/path@^0.219.1/parse"; +export { resolve as resolvePath } from "jsr:@std/path@^0.219.1/resolve"; +export { toFileUrl } from "jsr:@std/path@^0.219.1/to_file_url"; export { type IExecutableSchemaDefinition, makeExecutableSchema, diff --git a/dev.ts b/dev.ts index 6ae0b6f..9399daa 100644 --- a/dev.ts +++ b/dev.ts @@ -10,6 +10,7 @@ import { walk, } from "./deps.ts"; +/** Options for the GraphQL dev server. */ export type DevOptions = { /** Path to generated schema manifest. */ entrypoint?: string; diff --git a/diff.json b/diff.json new file mode 100644 index 0000000..54eac4e --- /dev/null +++ b/diff.json @@ -0,0 +1,3 @@ +{ + "dev": "@@ -1,6 +1,8 @@\n-#!/usr/bin/env -S deno run -A --watch=static/,routes/\n+#!/usr/bin/env -S deno run -A --watch=static/,routes/,graphql/\n\n import dev from \"$fresh/dev.ts\";\n+import { dev as graphql } from \"@vicary/fresh-graphql\";\n import config from \"./fresh.config.ts\";\n\n+await graphql(import.meta.url);\n await dev(import.meta.url, \"./main.ts\", config);\n" +} diff --git a/examples/graphql-yoga/deno.json b/examples/graphql-yoga/deno.json index 3f01b07..360cb8b 100644 --- a/examples/graphql-yoga/deno.json +++ b/examples/graphql-yoga/deno.json @@ -7,7 +7,7 @@ "$fresh/": "https://deno.land/x/fresh@1.6.5/", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", - "@vicary/fresh-graphql": "jsr:@vicary/fresh-graphql@^0.1.0", + "@vicary/fresh-graphql": "jsr:@vicary/fresh-graphql@^0.2.0", "graphql-yoga": "npm:graphql-yoga@^5.1.1", "preact": "https://esm.sh/preact@10.19.2", "preact/": "https://esm.sh/preact@10.19.2/", diff --git a/mod.ts b/mod.ts index e359bf7..a3db004 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,12 @@ -export { dev } from "./dev.ts"; -export { fromManifest } from "./schema.ts"; +export { dev, type DevOptions } from "./dev.ts"; +export { + fromManifest, + type GraphQLDirectiveModule, + type GraphQLTypeModule, + type Manifest, +} from "./schema.ts"; + +// Start interactive shell that automatically patches the fresh project. +if (import.meta.main) { + await import("./cli.ts"); +} diff --git a/schema.ts b/schema.ts index 9374ca7..177ae05 100644 --- a/schema.ts +++ b/schema.ts @@ -28,7 +28,9 @@ import { export type Callable = (...args: any[]) => any; +/** An object type definition with an optional resolver. */ export type GraphQLTypeModule = { + /** GraphQL SDL */ schema: string; /** Resolver of the schema, optional. */ @@ -38,6 +40,7 @@ export type GraphQLTypeModule = { | GraphQLFieldResolver; }; +/** A directive definition with a schema mapper */ export type GraphQLDirectiveModule = { schema: TSchema; @@ -89,6 +92,9 @@ export type SchemaMapper = TSchema extends ) : never; +/** + * The generated GraphQL manifest file content. + */ export type Manifest = { // modules: Record; modules: {