-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
915 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,6 @@ | |
"generated.ts", | ||
"vectors/*.json", | ||
"vectors/**/*.test.ts", | ||
"wagmi", | ||
"site/dist" | ||
] | ||
}, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
#!/usr/bin/env node | ||
import { cac } from 'cac' | ||
|
||
import { version } from '../version.js' | ||
import { type Generate, generate } from './Commands/Generate.js' | ||
import { type Init, init } from './Commands/Init.js' | ||
import * as Logger from './Logger.js' | ||
|
||
const cli = cac('fhub') | ||
|
||
cli | ||
.command('generate', 'generate code based on configuration') | ||
.option('-c, --config <path>', '[string] path to config file') | ||
.option('-r, --root <path>', '[string] root path to resolve config from') | ||
// .option('-w, --watch', '[boolean] watch for changes') | ||
.example((name) => `${name} generate`) | ||
.action(async (options: Generate) => await generate(options)) | ||
|
||
cli | ||
.command('init', 'create configuration file') | ||
.option('-c, --config <path>', '[string] path to config file') | ||
.option('-r, --root <path>', '[string] root path to resolve config from') | ||
.example((name) => `${name} init`) | ||
.action(async (options: Init) => await init(options)) | ||
|
||
cli.help() | ||
cli.version(version) | ||
|
||
void (async () => { | ||
try { | ||
process.title = 'node (fhub)' | ||
} catch {} | ||
|
||
try { | ||
// Parse CLI args without running command | ||
cli.parse(process.argv, { run: false }) | ||
if (!cli.matchedCommand) { | ||
if (cli.args.length === 0) { | ||
if (!cli.options.help && !cli.options.version) cli.outputHelp() | ||
} else throw new Error(`Unknown command: ${cli.args.join(' ')}`) | ||
} | ||
await cli.runMatchedCommand() | ||
process.exit(0) | ||
} catch (error) { | ||
Logger.error(`\n${(error as Error).message}`) | ||
process.exit(1) | ||
} | ||
})() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
import { default as dedent } from 'dedent' | ||
import { default as fs } from 'fs-extra' | ||
import { basename, dirname, resolve } from 'pathe' | ||
import pc from 'picocolors' | ||
import { z } from 'zod' | ||
|
||
import { fileURLToPath } from 'node:url' | ||
import { fromZodError } from '../Errors.js' | ||
import * as logger from '../Logger.js' | ||
import { findConfig } from '../Utils/findConfig.js' | ||
import { format } from '../Utils/format.js' | ||
import { getIsUsingTypeScript } from '../Utils/getIsUsingTypeScript.js' | ||
import { resolveConfig } from '../Utils/resolveConfig.js' | ||
|
||
const Generate = z.object({ | ||
/** Path to config file */ | ||
config: z.string().optional(), | ||
/** Directory to search for config file */ | ||
root: z.string().optional(), | ||
}) | ||
export type Generate = z.infer<typeof Generate> | ||
|
||
export async function generate(options: Generate = {}) { | ||
// Validate command line options | ||
try { | ||
await Generate.parseAsync(options) | ||
} catch (error) { | ||
if (error instanceof z.ZodError) | ||
throw fromZodError(error, { prefix: 'Invalid option' }) | ||
throw error | ||
} | ||
|
||
// Get cli config file | ||
const configPath = await findConfig(options) | ||
if (!configPath) { | ||
if (options.config) | ||
throw new Error(`Config not found at ${pc.gray(options.config)}`) | ||
throw new Error('Config not found') | ||
} | ||
|
||
const resolvedConfigs = await resolveConfig({ configPath }) | ||
const isTypeScript = await getIsUsingTypeScript() | ||
|
||
const outNames = new Set<string>() | ||
const isArrayConfig = Array.isArray(resolvedConfigs) | ||
const configs = isArrayConfig ? resolvedConfigs : [resolvedConfigs] | ||
for (const config of configs) { | ||
if (isArrayConfig) | ||
logger.log(`Using config ${pc.gray(basename(configPath))}`) | ||
if (!config.out) throw new Error('out is required.') | ||
if (outNames.has(config.out)) | ||
throw new Error(`out "${config.out}" must be unique.`) | ||
outNames.add(config.out) | ||
|
||
const spinner = logger.spinner() | ||
|
||
// Generate client file | ||
spinner.start('Generating client file') | ||
await writeClientFile({ | ||
isTypeScript, | ||
rpcUrl: config.rpcUrl, | ||
out: config.out, | ||
}) | ||
spinner.succeed() | ||
|
||
// Get the filenames of available top level actions | ||
spinner.start('Generating fhub Action Hooks') | ||
const actionsPath = resolve( | ||
dirname(fileURLToPath(import.meta.url)), | ||
'../..', | ||
'Actions', | ||
) | ||
const files = await fs.readdir(actionsPath) | ||
|
||
await Promise.all( | ||
files.map(async (file) => { | ||
// Reading function names | ||
const fileContents = await fs.readFile( | ||
resolve(actionsPath, file), | ||
'utf8', | ||
) | ||
|
||
// cutting of .ts or .js | ||
const namespaceName = file.slice(0, -3) | ||
|
||
// Idk how to generate server actions that return an async iterator – not sure this is possible | ||
if (namespaceName === 'Watch') return | ||
|
||
const functionNames = (() => { | ||
const matches = [ | ||
...fileContents.matchAll(/as (?<functionName>.*) }/gm), | ||
] | ||
return matches.map((match) => { | ||
const functionName = match.groups?.functionName | ||
if (!functionName) | ||
throw new Error('Unexpected – cant retrieve function name') | ||
return functionName | ||
}) | ||
})() | ||
|
||
// Creating sub directories with server actions | ||
|
||
await Promise.all( | ||
functionNames.map(async (functionName) => { | ||
const hookType = functionName === 'create' ? 'Mutation' : 'Query' | ||
const outputHook = `use${namespaceName}${functionName.slice(0, 1).toUpperCase()}${functionName.slice(1)}${hookType}` | ||
await fs.ensureDir(resolve(config.out, outputHook)) | ||
|
||
// Creating action.ts file | ||
const actionFileContent = await format(dedent` | ||
'use server' | ||
import { fhubClient } from '../client.${isTypeScript ? 'ts' : 'js'}' | ||
import { Actions } from 'fhub' | ||
export function action(parameters${isTypeScript ? `: Actions.${namespaceName}.${functionName}.ParametersType` : ''}): ${isTypeScript ? `Promise<Actions.${namespaceName}.${functionName}.ReturnType>` : ''} { | ||
return Actions.${namespaceName}.${functionName}(fhubClient, parameters) | ||
} | ||
`) | ||
await fs.writeFile( | ||
resolve( | ||
config.out, | ||
outputHook, | ||
`action.${isTypeScript ? 'ts' : 'js'}`, | ||
), | ||
actionFileContent, | ||
) | ||
|
||
// Creating hook file | ||
const hookContents = await (async () => { | ||
if (hookType === 'Mutation') { | ||
return await format(dedent` | ||
import { action } from './action.${isTypeScript ? 'ts' : 'js'}' | ||
${ | ||
isTypeScript | ||
? dedent` | ||
import type { Actions } from 'fhub' | ||
import { useMutation, type MutationOptions } from '@tanstack/react-query' | ||
` | ||
: "import { useMutation } from '@tanstack/react-query'" | ||
} | ||
export function ${outputHook}${hookType}({mutation = {}}${ | ||
isTypeScript | ||
? `: { | ||
mutation?: MutationOptions<Actions.${namespaceName}.${functionName}.ReturnType, Actions.${namespaceName}.${functionName}.ErrorType, Actions.${namespaceName}.${functionName}.ParametersType> | undefined | ||
}` | ||
: '' | ||
}) { | ||
return useMutation({ ...mutation, mutationKey: ['${namespaceName}.${functionName}'], mutationFn: (args)=> action(args) }) | ||
} | ||
`) | ||
} | ||
|
||
return format(dedent` | ||
import { action } from './action.${isTypeScript ? 'ts' : 'js'}' | ||
${ | ||
isTypeScript | ||
? dedent` | ||
import type { Actions } from 'fhub' | ||
import { useQuery, type QueryOptions } from '@tanstack/react-query' | ||
` | ||
: "import { useQuery } from '@tanstack/react-query'" | ||
} | ||
${isTypeScript ? `type QueryKey = ['${namespaceName}.${functionName}', Actions.${namespaceName}.${functionName}.ParametersType | undefined]` : ''} | ||
function queryKey(parameters${isTypeScript ? `: Actions.${namespaceName}.${functionName}.ParametersType | undefined` : ''})${isTypeScript ? ': QueryKey' : ''} { | ||
return ['${namespaceName}.${functionName}', parameters] as const | ||
} | ||
export function ${outputHook}${hookType}({query = {}, args}${ | ||
isTypeScript | ||
? `: { | ||
query?: QueryOptions<Actions.${namespaceName}.${functionName}.ReturnType, Actions.${namespaceName}.${functionName}.ErrorType, Actions.${namespaceName}.${functionName}.ReturnType, QueryKey> | undefined | ||
args?: Actions.${namespaceName}.${functionName}.ParametersType | undefined | ||
}` | ||
: '' | ||
}) { | ||
const enabled = Boolean(args && (query.enabled ?? true)) | ||
return useQuery({ ...query, queryKey: queryKey(args), queryFn: ({queryKey:[_, args]})=> action(args), enabled }) | ||
} | ||
`) | ||
})() | ||
|
||
await fs.writeFile( | ||
resolve( | ||
config.out, | ||
outputHook, | ||
`index.${isTypeScript ? 'ts' : 'js'}`, | ||
), | ||
hookContents, | ||
) | ||
}), | ||
) | ||
}), | ||
) | ||
|
||
spinner.succeed() | ||
spinner.stop() | ||
} | ||
} | ||
|
||
async function writeClientFile({ | ||
isTypeScript, | ||
rpcUrl, | ||
out, | ||
}: { | ||
isTypeScript: boolean | ||
rpcUrl: string | ||
out: string | ||
}) { | ||
// Format and write output | ||
const cwd = process.cwd() | ||
const outPath = resolve(cwd, out, `client.${isTypeScript ? 'ts' : 'js'}`) | ||
await fs.ensureDir(dirname(outPath)) | ||
const formatted = await format(dedent` | ||
import { Client, Transport } from "fhub"; | ||
export const fhubClient = Client.create(Transport.grpcNode({ baseUrl: '${rpcUrl}', httpVersion: '2' })) | ||
`) | ||
await fs.writeFile(outPath, formatted) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import dedent from 'dedent' | ||
import { default as fs } from 'fs-extra' | ||
import { relative, resolve } from 'pathe' | ||
import pc from 'picocolors' | ||
import { z } from 'zod' | ||
|
||
import { type Config, defaultConfig } from '../Config.js' | ||
import { fromZodError } from '../Errors.js' | ||
import * as logger from '../Logger.js' | ||
import { findConfig } from '../Utils/findConfig.js' | ||
import { format } from '../Utils/format.js' | ||
import { getIsUsingTypeScript } from '../Utils/getIsUsingTypeScript.js' | ||
|
||
export type Init = { | ||
/** Path to config file */ | ||
config?: string | ||
/** Watch for file system changes to config and plugins */ | ||
content?: Config | ||
/** Directory to init config file */ | ||
root?: string | ||
} | ||
|
||
const Init = z.object({ | ||
config: z.string().optional(), | ||
content: z.object({}).optional(), | ||
root: z.string().optional(), | ||
}) | ||
|
||
export async function init(options: Init = {}) { | ||
// Validate command line options | ||
try { | ||
await Init.parseAsync(options) | ||
} catch (error) { | ||
if (error instanceof z.ZodError) | ||
throw fromZodError(error, { prefix: 'Invalid option' }) | ||
throw error | ||
} | ||
|
||
// Check for existing config file | ||
const configPath = await findConfig(options) | ||
if (configPath) { | ||
logger.info( | ||
`Config already exists at ${pc.gray( | ||
relative(process.cwd(), configPath), | ||
)}`, | ||
) | ||
return configPath | ||
} | ||
|
||
const spinner = logger.spinner() | ||
spinner.start('Creating config') | ||
// Check if project is using TypeScript | ||
const isUsingTypeScript = await getIsUsingTypeScript() | ||
const rootDir = resolve(options.root || process.cwd()) | ||
let outPath: string | ||
if (options.config) { | ||
outPath = resolve(rootDir, options.config) | ||
} else { | ||
const extension = isUsingTypeScript ? 'ts' : 'js' | ||
outPath = resolve(rootDir, `fhub.config.${extension}`) | ||
} | ||
|
||
let content: string | ||
if (isUsingTypeScript) { | ||
const config = options.content ?? defaultConfig | ||
content = dedent(` | ||
import { defineConfig } from 'fhub/Cli' | ||
export default defineConfig(${JSON.stringify(config)}) | ||
`) | ||
} else { | ||
const config = options.content ?? { | ||
...defaultConfig, | ||
out: defaultConfig.out.replace('.ts', '.js'), | ||
} | ||
content = dedent(` | ||
// @ts-check | ||
/** @type {import('fhub/Cli').Config} */ | ||
export default ${JSON.stringify(config, null, 2).replace( | ||
/"(\d*)":/gm, | ||
'$1:', | ||
)} | ||
`) | ||
} | ||
|
||
const formatted = await format(content) | ||
await fs.writeFile(outPath, formatted) | ||
spinner.succeed() | ||
logger.success( | ||
`Config created at ${pc.gray(relative(process.cwd(), outPath))}`, | ||
) | ||
|
||
return outPath | ||
} |
Oops, something went wrong.