fuse
Version:
The magical GraphQL framework
391 lines (382 loc) • 12 kB
JavaScript
// src/cli.ts
import sade from "sade";
import path3 from "path";
import { existsSync as existsSync3 } from "fs";
import fs3 from "fs/promises";
import { createServer, build } from "vite";
import { VitePluginNode } from "vite-plugin-node";
// src/utils/gql-tada.ts
import { promises as fs, watch, existsSync } from "fs";
import path from "path";
import { buildSchema, introspectionFromSchema } from "graphql";
import { minifyIntrospectionQuery } from "@urql/introspection";
async function isUsingGraphQLTada(cwd) {
const [pkgJson, tsConfig] = await Promise.allSettled([
fs.readFile(path.resolve(cwd, "package.json"), "utf-8"),
fs.readFile(path.resolve(cwd, "tsconfig.json"), "utf-8")
]);
if (pkgJson.status === "rejected" || tsConfig.status === "rejected") {
return false;
}
try {
const parsed = JSON.parse(pkgJson.value);
const merged = Object.keys({
...parsed.dependencies,
...parsed.devDependencies
});
if (!merged.find((x) => x.includes("gql.tada"))) {
return false;
}
if (!merged.find((x) => x.includes("@0no-co/graphqlsp"))) {
return false;
}
} catch (e) {
return false;
}
try {
const parsed = JSON.parse(tsConfig.value);
const lspPlugin = parsed.compilerOptions.plugins.find(
(plugin) => plugin.name === "@0no-co/graphqlsp"
);
if (!lspPlugin) {
return false;
}
if (!lspPlugin.tadaOutputLocation) {
return false;
}
} catch (e) {
return tsConfig.value.includes("@0no-co/graphqlsp") && tsConfig.value.includes("tadaOutputLocation");
}
return true;
}
var tadaGqlContents = `import { initGraphQLTada } from 'gql.tada';
import type { introspection } from './introspection';
export const graphql = initGraphQLTada<{
introspection: typeof introspection;
}>();
export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
export type { FragmentOf as FragmentType } from 'gql.tada';
export { readFragment } from 'gql.tada';
export { readFragment as useFragment } from 'gql.tada';
`;
async function ensureTadaIntrospection(location, shouldWatch) {
const schemaLocation = path.resolve(location, "schema.graphql");
const writeTada = async () => {
try {
const content = await fs.readFile(schemaLocation, "utf-8");
const schema = buildSchema(content);
const introspection = introspectionFromSchema(schema, {
descriptions: false
});
const minified = minifyIntrospectionQuery(introspection, {
includeDirectives: false,
includeEnums: true,
includeInputs: true,
includeScalars: true
});
const json = JSON.stringify(minified, null, 2);
const hasSrcDir = existsSync(path.resolve(location, "src"));
const base = hasSrcDir ? path.resolve(location, "src") : location;
const outputLocation = path.resolve(base, "fuse", "introspection.ts");
const contents = [
preambleComments,
tsAnnotationComment,
`const introspection = ${json} as const;
`,
"export { introspection };"
].join("\n");
await fs.writeFile(outputLocation, contents);
} catch (e) {
}
};
await writeTada();
if (shouldWatch) {
watch(schemaLocation, async () => {
await writeTada();
});
}
}
var preambleComments = ["/* eslint-disable */", "/* prettier-ignore */"].join("\n") + "\n";
var tsAnnotationComment = [
"/** An IntrospectionQuery representation of your schema.",
" *",
" * @remarks",
" * This is an introspection of your schema saved as a file by GraphQLSP.",
" * You may import it to create a `graphql()` tag function with `gql.tada`",
" * by importing it and passing it to `initGraphQLTada<>()`.",
" *",
" * @example",
" * ```",
" * import { initGraphQLTada } from 'gql.tada';",
" * import type { introspection } from './introspection';",
" *",
" * export const graphql = initGraphQLTada<{",
" * introspection: typeof introspection;",
" * scalars: {",
" * DateTime: string;",
" * Json: any;",
" * };",
" * }>();",
" * ```",
" */"
].join("\n");
// src/utils/codegen.ts
import { generate, CodegenContext } from "@graphql-codegen/cli";
import { DateTimeResolver, JSONResolver } from "graphql-scalars";
import { existsSync as existsSync2, promises as fs2 } from "fs";
import path2 from "path";
async function boostrapCodegen(location, watch2) {
const baseDirectory = process.cwd();
const hasSrcDir = existsSync2(path2.resolve(baseDirectory, "src"));
const contents = `export * from "./fragment-masking";
export * from "./gql";
export * from "fuse/client";
`;
const ctx = new CodegenContext({
filepath: "codgen.yml",
config: {
ignoreNoDocuments: true,
errorsOnly: true,
noSilentErrors: true,
hooks: {
afterOneFileWrite: async () => {
await fs2.writeFile(
hasSrcDir ? baseDirectory + "/src/fuse/index.ts" : baseDirectory + "/fuse/index.ts",
contents
);
}
},
watch: watch2 ? [
hasSrcDir ? baseDirectory + "/src/**/*.{ts,tsx}" : baseDirectory + "/**/*.{ts,tsx}",
"!./{node_modules,.next,.git}/**/*",
hasSrcDir ? "!./src/fuse/*.{ts,tsx}" : "!./fuse/*.{ts,tsx}"
] : false,
schema: location,
generates: {
[hasSrcDir ? baseDirectory + "/src/fuse/" : baseDirectory + "/fuse/"]: {
documents: [
hasSrcDir ? "./src/**/*.{ts,tsx}" : "./**/*.{ts,tsx}",
"!./{node_modules,.next,.git}/**/*",
hasSrcDir ? "!./src/fuse/*.{ts,tsx}" : "!./fuse/*.{ts,tsx}"
],
preset: "client",
// presetConfig: {
// persistedDocuments: true,
// },
config: {
scalars: {
ID: {
input: "string",
output: "string"
},
DateTime: DateTimeResolver.extensions.codegenScalarType,
JSON: JSONResolver.extensions.codegenScalarType
},
avoidOptionals: false,
enumsAsTypes: true,
nonOptionalTypename: true,
skipTypename: false
}
}
}
}
});
await generate(ctx, true);
}
// src/cli.ts
var prog = sade("fuse");
prog.version("0.12.1");
prog.command("build").describe("Creates the build output for server and client.").option(
"--adapter",
"Which adapter to use for building, options are lambda, cloudflare, bun and node (default)",
"node"
).option(
"--server",
'Whether to look for the "types/" directory and create a server build output.'
).option(
"--client",
"Whether to look for GraphQL documents and generate types."
).option(
"--schema",
'Where to find the schema, either a "*.graphql" file or an endpoint that can be introspected.',
"./schema.graphql"
).action(async (opts) => {
if (!opts.server && !opts.client) {
opts.server = true;
opts.client = true;
}
if (opts.server) {
const baseDirectory2 = process.cwd();
let entryPoint = "node.mjs";
switch (opts.adapter) {
case "lambda": {
entryPoint = "lambda.mjs";
break;
}
case "bun": {
entryPoint = "bun.mjs";
break;
}
case "cloudflare": {
entryPoint = "cloudflare.mjs";
break;
}
default: {
entryPoint = "node.mjs";
break;
}
}
await build({
build: {
outDir: path3.resolve(baseDirectory2, "build"),
rollupOptions: {
logLevel: "silent"
}
},
plugins: [
...VitePluginNode({
async adapter() {
},
appName: "fuse",
appPath: path3.resolve(
baseDirectory2,
"node_modules",
"fuse",
"dist",
"adapters",
entryPoint
),
exportName: "main"
})
]
});
console.log("Server build output created in ./build");
}
const baseDirectory = process.cwd();
if (opts.client) {
if (!await isUsingGraphQLTada(baseDirectory)) {
await boostrapCodegen(opts.schema, false);
} else {
const hasSrcDir = existsSync3(path3.resolve(baseDirectory, "src"));
const base = hasSrcDir ? path3.resolve(baseDirectory, "src") : baseDirectory;
if (!existsSync3(path3.resolve(base, "fuse"))) {
await fs3.mkdir(path3.resolve(base, "fuse"));
}
await Promise.allSettled([
fs3.writeFile(
path3.resolve(base, "fuse/index.ts"),
`// This is a generated file!
export * from './tada';
export * from 'fuse/client';
`
),
fs3.writeFile(path3.resolve(base, "fuse/tada.ts"), tadaGqlContents)
]);
}
}
}).command("dev").describe("Runs the dev-server for the client and server by default.").option(
"--port",
"Which port to use for the dev-server (default: 4000)",
4e3
).option(
"--server",
'Whether to look for the "types/" directory and create a server build output.'
).option(
"--client",
"Whether to look for GraphQL documents and generate types."
).option(
"--schema",
'Where to find the schema, either a "*.graphql" file or an endpoint that can be introspected.',
"./schema.graphql"
).action(async (opts) => {
if (!opts.server && !opts.client) {
opts.server = true;
opts.client = true;
}
const baseDirectory = process.cwd();
const isUsingTada = opts.client && await isUsingGraphQLTada(baseDirectory);
let hasTadaWatcherRunning = false;
if (opts.server) {
let yoga;
const server = await createServer({
plugins: [
...VitePluginNode({
initAppOnBoot: true,
async adapter({ app, req, res }) {
yoga = await app(opts).then((yo) => {
fs3.writeFile(
path3.resolve(baseDirectory, "schema.graphql"),
yo.stringifiedSchema,
"utf-8"
).then(() => {
if (isUsingTada && !hasTadaWatcherRunning) {
hasTadaWatcherRunning = true;
ensureTadaIntrospection(baseDirectory, true);
}
});
return yo;
});
await yoga.handle(req, res);
},
appPath: path3.resolve(
baseDirectory,
"node_modules",
"fuse",
"dist",
"dev.mjs"
),
exportName: "main"
})
]
});
server.watcher.on("change", async (file) => {
if (file.includes("types/")) {
if (isUsingTada) {
setTimeout(() => {
fetch(
`http://localhost:${opts.port}/graphql?query={__typename}`
).catch(() => {
});
}, 500);
}
server.restart();
}
});
await server.listen(opts.port);
console.log(`Server listening on http://localhost:${opts.port}/graphql`);
}
if (opts.client) {
if (!isUsingTada) {
setTimeout(() => {
fetch(`http://localhost:${opts.port}/graphql?query={__typename}`).then(() => {
boostrapCodegen(opts.schema, true);
}).catch(() => {
});
}, 1e3);
} else {
setTimeout(() => {
fetch(
`http://localhost:${opts.port}/graphql?query={__typename}`
).catch(() => {
});
}, 1e3);
const hasSrcDir = existsSync3(path3.resolve(baseDirectory, "src"));
const base = hasSrcDir ? path3.resolve(baseDirectory, "src") : baseDirectory;
if (!existsSync3(path3.resolve(base, "fuse"))) {
await fs3.mkdir(path3.resolve(base, "fuse"));
}
await Promise.allSettled([
fs3.writeFile(
path3.resolve(base, "fuse/index.ts"),
`// This is a generated file!
export * from './tada';
export * from 'fuse/client';
`
),
fs3.writeFile(path3.resolve(base, "fuse/tada.ts"), tadaGqlContents)
]);
}
}
});
prog.parse(process.argv);