Skip to content

Commit

Permalink
feat(cli): add interactive shell for automatic patch
Browse files Browse the repository at this point in the history
  • Loading branch information
vicary committed Mar 13, 2024
1 parent aea45f3 commit 8226a44
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 31 deletions.
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
200 changes: 200 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<boolean> {
// 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;
}
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vicary/fresh-graphql",
"version": "0.1.5",
"version": "0.2.6",
"exports": "./mod.ts",
"lock": false,
"nodeModulesDir": false
Expand Down
20 changes: 11 additions & 9 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
1 change: 1 addition & 0 deletions dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions diff.json
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 1 addition & 1 deletion examples/graphql-yoga/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"$fresh/": "https://deno.land/x/[email protected]/",
"@preact/signals": "https://esm.sh/*@preact/[email protected]",
"@preact/signals-core": "https://esm.sh/*@preact/[email protected]",
"@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/[email protected]",
"preact/": "https://esm.sh/[email protected]/",
Expand Down
14 changes: 12 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -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");
}
6 changes: 6 additions & 0 deletions schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -38,6 +40,7 @@ export type GraphQLTypeModule = {
| GraphQLFieldResolver<any, any, any, any>;
};

/** A directive definition with a schema mapper */
export type GraphQLDirectiveModule<TSchema extends string = string> = {
schema: TSchema;

Expand Down Expand Up @@ -89,6 +92,9 @@ export type SchemaMapper<TSchema extends string> = TSchema extends
)
: never;

/**
* The generated GraphQL manifest file content.
*/
export type Manifest = {
// modules: Record<string, GraphQLModule>;
modules: {
Expand Down

0 comments on commit 8226a44

Please sign in to comment.