UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

281 lines (264 loc) 8.29 kB
import { CAC } from "cac"; import fs from "node:fs"; import path from "node:path"; import fetch from "cross-fetch"; import { codegenRequest } from "../../lib/edge"; import { CodegenFramework, CodegenPlatform, JsLanguage, prodEdgeCdnBaseUrl, uniqueId, } from "../../shared"; import { Schema, throwIfOptionIsUndefined, withOtherOptionSources, withValidation, } from "../helpers"; import defaultLocalLogger from "../../shared/helpers/defaultLocalLogger"; export type GenerateOptions = { token?: string; branchName?: string; framework?: string; platform?: string; queryFilePath?: string; outputFilePath?: string; outputDirectoryPath?: string; includeToken?: boolean; includeInitData?: boolean; includeToolbar?: boolean; language?: JsLanguage; edgeBaseUrl?: string; getHypertuneImportPath?: string; }; export const defaultOptions = { queryFilePath: "hypertune.graphql", outputDirectoryPath: "generated", includeToken: false, includeInitData: false, includeToolbar: false, language: "ts", }; export const generateOptionsSchema: Schema = { token: "string", branchName: "string", framework: "string", platform: "string", queryFilePath: "string", outputFilePath: "string", outputDirectoryPath: "string", includeToken: "boolean", includeInitData: "boolean", includeToolbar: "boolean", language: "string", edgeBaseUrl: "string", getHypertuneImportPath: "string", }; export function registerGenerateCommand(cli: CAC): void { cli // TODO: make generate a sub command, after https://github.com/cacjs/cac/pull/152 // .command("generate", "Generate Hypertune files for your project", { allowUnknownOptions: true }) // .alias("!") .command("", "Generate Hypertune files for your project", { allowUnknownOptions: true, ignoreOptionDefaultValue: true, }) .example((bin) => `${bin} --token U2FsdGVkX1abcdef`) .action(withOtherOptionSources(generate, generateOptionsSchema)) .option("--token [value]", "Project token") .option("--branchName [value]", "Project branch to use") .option( "--queryFilePath [value]", "File path to the GraphQL initialization query", { default: defaultOptions.queryFilePath } ) .option( "--outputFilePath [value]", "(Deprecated) The path to write the generated file to" ) .option( "--outputDirectoryPath [value]", "The directory to write the generated files to", { default: defaultOptions.outputDirectoryPath } ) .option( "--includeToken", "Include the project token in the generated code", { default: defaultOptions.includeToken, } ) .option( "--includeInitData", "Embed a static snapshot of your flag logic in the generated code so the SDK can reliably, locally and instantly initialize first, before fetching the latest logic from the server, and can function even if the server is unreachable", { default: defaultOptions.includeInitData, } ) .option( "--includeToolbar", "Generate code for Hypertune toolbar. Only available on paid plans.", { default: defaultOptions.includeToolbar, } ) .option( "--language [value]", "Target language (ts, js (uses ESM), mjs or cjs)", { default: defaultOptions.language, } ) .option( "--framework [value]", "Framework (nextApp, nextPages, react, vue, remix or gatsby)" ) .option("--platform [value]", "Platform (vercel)") .option( "--getHypertuneImportPath [value]", "Relative import path for a file with a default export of the `getHypertune` function, which takes a single object argument containing readonly `headers` and `cookies`; only used for the nextApp framework and vercel platform" ); } export const generate = withValidation( generateOptionsSchema, async (options: GenerateOptions) => { console.log("Starting Hypertune code generation..."); if (options.outputFilePath) { console.warn( "Warning: --outputFilePath option is deprecated, please use --outputDirectoryPath instead" ); } try { const language = options.language ?? (defaultOptions.language as JsLanguage); const outputFilePath = path.join( options.outputDirectoryPath ?? defaultOptions.outputDirectoryPath, `hypertune.${language}` ); await generateCode({ token: throwIfOptionIsUndefined("token", options.token), branchName: options.branchName ?? null, queryCode: getQueryCode( options.queryFilePath ?? defaultOptions.queryFilePath, /* throwIfNotExists */ !!options.queryFilePath ), outputFilePath: options.outputDirectoryPath ? outputFilePath : (options.outputFilePath ?? outputFilePath), includeToken: options.includeToken ?? defaultOptions.includeToken, includeFallback: options.includeInitData ?? defaultOptions.includeInitData, includeToolbar: options.includeToolbar ?? defaultOptions.includeToolbar, language, edgeBaseUrl: options.edgeBaseUrl ?? prodEdgeCdnBaseUrl, framework: options.framework, platform: options.platform, getHypertuneImportPath: options.getHypertuneImportPath, }); console.log("Done"); } catch (error) { console.error( error instanceof Error ? `Error: ${error.message}` : String(error) ); process.exit(1); } } ); async function generateCode({ token, branchName, queryCode, outputFilePath, includeToken, includeFallback, includeToolbar, edgeBaseUrl, language, framework, platform, getHypertuneImportPath, }: { token: string; branchName: string | null; queryCode: string | null; outputFilePath: string; includeToken: boolean; includeFallback: boolean; includeToolbar: boolean; edgeBaseUrl: string; language: JsLanguage; framework: string | undefined; platform: string | undefined; getHypertuneImportPath: string | undefined; }): Promise<void> { if ( framework === "nextApp" && platform === "vercel" && !getHypertuneImportPath ) { console.log( `Not generating Vercel flag definitions. To enable generation, set the "getHypertuneImportPath" option.` ); } if (framework && language !== "js" && language !== "ts") { console.warn( `Warning: framework option is not supported for language: ${language}` ); } const extension = path.extname(outputFilePath); const clientFileName = path.basename( outputFilePath.slice(0, outputFilePath.length - extension.length) ); const codegenResponse = await codegenRequest({ traceId: uniqueId(), token, branchName, body: { framework: framework as CodegenFramework | undefined, platform: platform as CodegenPlatform | undefined, clientFileName, query: queryCode, includeToken, includeFallback, includeToolbar, getHypertuneImportPath, }, language, edgeBaseUrl, tracedFetch: (_, url, init) => { return fetch(url, init); }, }); const outputDirectoryPath = path.dirname(outputFilePath); if (!fs.existsSync(outputDirectoryPath)) { fs.mkdirSync(outputDirectoryPath, { recursive: true }); } codegenResponse.messages.forEach(({ level, message, metadata }) => { defaultLocalLogger(level, message, metadata); }); codegenResponse.files.forEach(({ name, content }) => { writeFile(path.join(outputDirectoryPath, name), content); }); } function getQueryCode( filePath: string, throwIfNotExists: boolean ): string | null { const queryFilePath = path.join("./", filePath); if (!fs.existsSync(queryFilePath)) { if (throwIfNotExists) { throw new Error( `Query file does not exist at the provided path: "${queryFilePath}"` ); } return null; } const queryCode = fs.readFileSync(queryFilePath).toString(); console.log(`Loaded query code from file: "${queryFilePath}"`); return queryCode; } function writeFile(filePath: string, contents: string): void { fs.writeFileSync(filePath, contents, { flag: "w" }); console.log("Generated file:", filePath); }