From bb6b344bfacaee21dbfc16f119bd1b00ff0d8e3c Mon Sep 17 00:00:00 2001 From: Jonathan Holmes Date: Wed, 11 Jan 2023 14:01:27 +1300 Subject: [PATCH] Add required variables feature --- .prettierrc | 9 +++++++ example/src/shared/module.ts | 3 +++ index.d.ts | 12 +++++++++ package.json | 3 ++- src/class/environmentProvider.ts | 4 +++ src/class/transformState.ts | 3 +++ src/index.ts | 43 ++++++++++++++++++++++++++++++-- src/transform/macros/call/env.ts | 17 ++++++++----- 8 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9c32a5d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 120, + "tabWidth": 4, + "useTabs": true, + "arrowParens": "avoid" +} diff --git a/example/src/shared/module.ts b/example/src/shared/module.ts index 6cf5832..ba1cb5a 100644 --- a/example/src/shared/module.ts +++ b/example/src/shared/module.ts @@ -20,6 +20,9 @@ if ($env.boolean("ANALYTICS_API_URL")) { } export const DefaultValue = $env.number("DEFAULT_VALUE", 0.05); +export const DefaultString = $env.string("DEFAULT_STR"); + +// $env.expectString("TEST", "A 'TEST' variable is required in your environment."); const test: number = DefaultValue; diff --git a/index.d.ts b/index.d.ts index c0741f9..e621808 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,6 +10,12 @@ export namespace $env { export function string(name: string): string | undefined; export function string(name: string, defaultValue: string): string; + // /** + // * Attempts to fetch the given environment variable as a string - will throw a compiler error otherwise. + // * @param name The name of the environment variable to expect + // */ + // export function expectString<_TCompilerError extends string>(name: string, message?: _TCompilerError): string; + /** * Converts the given environment variable to a boolean - if not set will be set `defaultValue` or `false`. * @@ -41,6 +47,12 @@ export namespace $env { */ export function number(name: string): number | undefined; export function number(name: string, defaultValue: number): number; + + // /** + // * Attempts to fetch the given environment variable as a number - will throw a compiler error otherwise. + // * @param name The name of the environment variable to expect + // */ + // export function expectNumber(name: string): number; } /** diff --git a/package.json b/package.json index cd3c8fb..26b43de 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "rbxts-transform-env", - "version": "2.0.4", + "version": "2.1.0-beta.0", "description": "Transformer for Roblox TypeScript compiler that allows getting values of process.env as string literals", "main": "out/index.js", "scripts": { "build": "tsc", + "yalc": "tsc && yalc push", "prepublish": "npm run build" }, "repository": { diff --git a/src/class/environmentProvider.ts b/src/class/environmentProvider.ts index 7a84b27..9b4883d 100644 --- a/src/class/environmentProvider.ts +++ b/src/class/environmentProvider.ts @@ -30,6 +30,10 @@ export class EnvironmentProvider { return this.variables.get(name); } + public has(name: string): boolean { + return this.variables.has(name); + } + public getAsNumber(name: string): number | undefined { const value = this.get(name); if (value && value.match(/\d+/gi)) { diff --git a/src/class/transformState.ts b/src/class/transformState.ts index af96ff4..51bb29c 100644 --- a/src/class/transformState.ts +++ b/src/class/transformState.ts @@ -8,10 +8,13 @@ import { EnvironmentProvider } from "./environmentProvider"; import { LoggerProvider } from "./logProvider"; import { SymbolProvider } from "./symbolProvider"; +type Handler = "warn" | "error" | "errorOnProduction"; + export interface TransformConfiguration { verbose?: boolean; defaultEnvironment: string; shortCircuitNodeEnv: boolean; + expectedVariables: Record | undefined; } export class TransformState { diff --git a/src/index.ts b/src/index.ts index 29cb00f..a768d66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,17 +4,19 @@ import ts from "typescript"; import { TransformConfiguration, TransformState } from "./class/transformState"; import { transformFile } from "./transform/transformFile"; -import fs from "fs"; import { LoggerProvider } from "./class/logProvider"; +import { EnvironmentProvider } from "./class/environmentProvider"; const DEFAULTS: TransformConfiguration = { verbose: false, defaultEnvironment: "production", shortCircuitNodeEnv: true, + expectedVariables: undefined, }; export default function transform(program: ts.Program, userConfiguration: TransformConfiguration) { userConfiguration = { ...DEFAULTS, ...userConfiguration }; + const isProduction = (process.env.NODE_ENV ?? userConfiguration.defaultEnvironment) === "production"; if (process.argv.includes("--verbose")) { userConfiguration.verbose = true; @@ -26,11 +28,48 @@ export default function transform(program: ts.Program, userConfiguration: Transf logger.write("\n"); } logger.infoIfVerbose("Loaded environment transformer"); + let performedVariableCheck = false; + return (context: ts.TransformationContext): ((file: ts.SourceFile) => ts.Node) => { const state = new TransformState(program, context, userConfiguration, logger); if (state.symbolProvider.moduleFile === undefined) { - return (file) => file; + return file => file; + } + + if (userConfiguration.expectedVariables && !performedVariableCheck) { + let hasDiagnostic = false; + + for (const [variable, variableRequireConfig] of Object.entries(userConfiguration.expectedVariables)) { + const hasVariable = state.environmentProvider.has(variable); + if (!hasVariable) { + if (typeof variableRequireConfig === "string") { + if ( + variableRequireConfig === "error" || + (isProduction && variableRequireConfig === "errorOnProduction") + ) { + logger.error(`Expected enviroment variable: '${variable}'`); + hasDiagnostic = true; + } else if (variableRequireConfig === "warn") { + logger.warnIfVerbose(`Missing environment variable '${variable}'`); + } + } else { + const [handler, message] = variableRequireConfig; + if (handler === "error" || (isProduction && handler === "errorOnProduction")) { + logger.error(message); + hasDiagnostic = true; + } else if (handler === "warn") { + logger.warnIfVerbose(message); + } + } + } + } + + if (hasDiagnostic) { + throw new Error(`Required environment variable(s) have not been configured - see above`); + } + + performedVariableCheck = true; } return (file: ts.SourceFile) => { diff --git a/src/transform/macros/call/env.ts b/src/transform/macros/call/env.ts index c615cf6..29695b2 100644 --- a/src/transform/macros/call/env.ts +++ b/src/transform/macros/call/env.ts @@ -16,6 +16,11 @@ export function getEnvDefaultValue(expression: ts.CallExpression): ts.Expression } } +export function isUnsafeToPrint(variable: string): boolean { + const valueLower = variable.toLowerCase(); + return valueLower.includes("token") || valueLower.includes("api") || valueLower.includes("key"); +} + export const EnvCallAsStringMacro: CallMacro = { getSymbol(state: TransformState) { const envSymbol = state.symbolProvider.moduleFile?.envNamespace; @@ -35,13 +40,13 @@ export const EnvCallAsStringMacro: CallMacro = { (variableValue !== undefined ? toExpression(variableValue) : getEnvDefaultValue(callExpression)) ?? factory.createIdentifier("undefined"); - if (state.config.verbose) { + if (state.config.verbose && !variableName.toLowerCase().includes("token")) { state.logger.infoIfVerbose( - `Transform variable ${variableName} to ${printer.printNode( - ts.EmitHint.Expression, - expression, - callExpression.getSourceFile(), - )}`, + `Transform variable ${variableName} to ${ + isUnsafeToPrint(variableName) + ? "***" + : printer.printNode(ts.EmitHint.Expression, expression, callExpression.getSourceFile()) + }`, ); console.log("\t", callExpression.getSourceFile().fileName); }