Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Env / config package #1136

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a9f9cdb
init env pkg
goastler Apr 4, 2024
09636fe
basic loadConfig fns
goastler Apr 4, 2024
966b616
logging and loading from default env/config.ts paths
goastler Apr 4, 2024
19c39c9
adjusted default path handling
goastler Apr 4, 2024
3160a9a
parse config using schema
goastler Apr 4, 2024
32e2588
load from env using dotenv
goastler Apr 4, 2024
a6b5880
load config from js
goastler Apr 4, 2024
ef82125
js should export fn to build config
goastler Apr 4, 2024
9a9f706
make a test config
goastler Apr 4, 2024
6b14672
specifying config.ts converts to config.js
goastler Apr 4, 2024
faded39
check for config file existence
goastler Apr 4, 2024
acfb34d
missing dep
goastler Apr 4, 2024
4b637fc
Merge branch 'main' into env
goastler Apr 4, 2024
c304d1f
fix deps
goastler Apr 4, 2024
58ff03e
move config type to own file
goastler Apr 4, 2024
a14821d
compile ts
goastler Apr 4, 2024
06d8cb1
fix generic typing
goastler Apr 4, 2024
b378beb
JSON stringify env variables when loading from config
goastler Apr 4, 2024
b9731ab
make a config schema
goastler Apr 4, 2024
e11ca36
example of loading config
goastler Apr 4, 2024
707f0a6
example config
goastler Apr 4, 2024
3ded955
fix tests
goastler Apr 4, 2024
274aa1e
load config using json
goastler Apr 4, 2024
616ead6
json config
goastler Apr 4, 2024
da6a4af
json config tests
goastler Apr 4, 2024
98b0015
rename example env
goastler Apr 4, 2024
52e3300
move config pkg to configure
goastler Apr 4, 2024
1b8cff3
disable ts/js/json type configs
goastler Apr 4, 2024
048973f
lint
goastler Apr 4, 2024
ac2a117
lint
goastler Apr 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,481 changes: 1,463 additions & 1,018 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions packages/configure/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/node_modules/
/src/
/tests/
/artifacts/
tsconfig.json
tsconfig.*.json
tsconfig.tsbuildinfo
env.production
env.development
env.test
.env.*
webpack.*
*.ipynb
captchas_*.json
data.json
stl10/*.json
stl10
1 change: 1 addition & 0 deletions packages/configure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# i18next localization for Prosopo
54 changes: 54 additions & 0 deletions packages/configure/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@prosopo/configure",
"version": "0.3.5",
"description": "Prosopo config library",
"main": "./dist/index.js",
"type": "module",
"engines": {
"node": ">=18",
"npm": ">=9"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/cjs/index.cjs"
}
},
"types": "./dist/index.d.ts",
"scripts": {
"clean": "tsc --build --clean",
"build": "tsc --build --verbose",
"build:cjs": "npx vite --config vite.cjs.config.ts build",
"test": "NODE_ENV=test vitest --run",
"eslint": "npx eslint . --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"eslint:fix": "npm run eslint -- --fix",
"prettier": "npx prettier . --check --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"prettier:fix": "npm run prettier -- --write",
"lint": "npm run eslint && npm run prettier",
"lint:fix": "npm run eslint:fix && npm run prettier:fix"
},
"author": "Prosopo Limited",
"license": "Apache-2.0",
"dependencies": {
"@prosopo/common": "0.3.5",
"dotenv": "^16.4.5",
"zod": "^3.22.3"
},
"devDependencies": {
"tslib": "2.6.2",
"typescript": "5.1.6",
"vitest": "^1.3.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/prosopo/captcha.git"
},
"bugs": {
"url": "https://github.com/prosopo/captcha/issues"
},
"homepage": "https://github.com/prosopo/captcha#readme",
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"sideEffects": false
}
22 changes: 22 additions & 0 deletions packages/configure/src/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ConfigSchema } from './tests/config.js'
import { loadConfig } from './index.js'

const main = async () => {
const config = await loadConfig({ path: 'example.config.ts', schema: ConfigSchema })
console.log(config)
}

main().catch(console.error)
180 changes: 180 additions & 0 deletions packages/configure/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Loading from json/js/ts has been disabled for now on the basis that it causes issues with loading config in docker/flux. Native env var support makes it super easy to load config from env vars, so this is the recommended approach atm. Unfortunately, this means no dynamic config via js/ts, and no complex config types via js/ts. This is a tradeoff for compatibility. See https://github.com/prosopo/captcha/pull/1136 for reasoning.
*
* Old code for loading json/js/ts will remain here, in case we need it in the future.
*/

import { getLogger } from '@prosopo/common'
import dotenv from 'dotenv'
import fs from 'fs'
import z from 'zod'
// import ts from 'typescript'

const logger = getLogger('warn', import.meta.url)

/**
* The arguments for loading environment variables.
* @param populateProcessEnv - Whether to populate `process.env` with the loaded environment variables. Defaults to `false`.
* @param path - The path to the source file containing the environment variables. If unspecified, falls back to `config.ts` then `.env`.
*/
export type Args<T extends object> = {
populateProcessEnv?: boolean
path?: string
schema: z.ZodType<T>
}

const loadConfigFromEnv = async (
path: string
): Promise<{
[key: string]: string
}> => {
logger.debug(`Loading env-based config from: ${path}`)

if (!fs.existsSync(path)) {
throw new Error(`Config file not found at '${path}'`)
}

const result = {}
dotenv.config({
path,
processEnv: result, // make dotenv load into the result object
})

return result
}

// const loadConfigFromJson = async (path: string): Promise<{
// [key: string]: string
// }> => {
// logger.debug(`Loading json-based config from: ${path}`)

// if (!fs.existsSync(path)) {
// throw new Error(`Config file not found at '${path}'`)
// }

// const result = JSON.parse(fs.readFileSync(path, 'utf-8'))

// return result
// }

// const loadConfigFromTs = async (path: string): Promise<{
// [key: string]: string
// }> => {
// logger.debug(`Loading ts-based config from: ${path}`);

// if (!fs.existsSync(path)) {
// throw new Error(`Config file not found at '${path}'`)
// }

// // read the ts file
// const tsCode = fs.readFileSync(path, 'utf-8')
// // compile the ts file
// const jsCode = ts.transpileModule(tsCode, {}).outputText
// // write the js code to a temporary file
// const jsPath = path.slice(0, -3) + '.js.tmp'
// fs.writeFileSync(jsPath, jsCode)

// // load the config from the js file
// const config = await loadConfigFromJs(jsPath)

// // delete the temporary js file
// fs.unlinkSync(jsPath)

// return config
// }

// const loadConfigFromJs = async (path: string): Promise<{
// [key: string]: string
// }> => {
// logger.debug(`Loading js-based config from: ${path}`);

// if (!fs.existsSync(path)) {
// throw new Error(`Config file not found at '${path}'. Have you compiled your typescript config file? e.g. \`npx tsc config.ts\``)
// }

// // dynamic import the js file
// // this will have no typing!
// const buildConfig = (await import(`${path}`)).default
// console.log('buildConfig', buildConfig)

// // ensure the config is a function
// if (typeof buildConfig !== 'function') {
// throw new Error(`Config at '${path}' must export a function to build the config object.`)
// }

// const config = buildConfig()

// return config
// }

/**
* Loads the config from env or a config file.
* @param args - The arguments for loading environment variables.
*/
export async function loadConfig<T extends object>(args: Args<T>): Promise<T> {
if (args.path === undefined) {
// check whether it has been set in the process.env
if (process.env.ENV) {
args.path = process.env.ENV
} else {
// const defaultConfigTsPath = './config.ts';
const defaultEnvPath = './.env'
// if (fs.existsSync(defaultConfigTsPath)) {
// // try to load ${cwd}/config.ts
// args.path = defaultConfigTsPath;
// } else
if (fs.existsSync(defaultEnvPath)) {
// try to load ${cwd}/.env
args.path = defaultEnvPath
} else {
throw new Error(`No config file found at default location of '${defaultEnvPath}'`)
}
}
}
// load the config from the specified path
const config: {
[key: string]: string
} = await loadConfigFromEnv(args.path)
// const tsExtension = '.ts'
// if (args.path?.endsWith(tsExtension)) {
// config = await loadConfigFromTs(args.path);
// } else if (args.path?.endsWith('.js')) {
// config = await loadConfigFromJs(args.path);
// } else if (args.path?.endsWith('.json')) {
// config = await loadConfigFromJson(args.path);
// } else {
// config = await loadConfigFromEnv(args.path);
// }
logger.info(`Loaded config from '${args.path}': ${JSON.stringify(config)}`)

// parse the config to ensure it meets the expected format
let parsedConfig: T
try {
parsedConfig = args.schema.parse(config)
} catch (err) {
throw new Error(`Failed to parse config at '${args.path}': ${err}`)
}

// populate process.env if requested
if (args.populateProcessEnv) {
for (const key in parsedConfig) {
// convert all values into strings via json encoding. this is to ensure that all values are strings. Any non-string objects will need to be JSON parsed before use. E.g. { a: { b: 1 } } will be stored as "{ "a": { "b": "1" } }", i.e. you'd need to JSON.parse(process.env.a).b to get the number 1 - but that would be a string, also.
process.env[key] = JSON.stringify(parsedConfig[key])
}
}

return parsedConfig
}
61 changes: 61 additions & 0 deletions packages/configure/src/tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ConfigSchema } from './config.js'
import { describe, expect, it } from 'vitest'
import { loadConfig } from '../index.js'

describe('config', () => {
it('should load config from env', async () => {
expect(await loadConfig({ path: `${__dirname}/example.config.env`, schema: ConfigSchema })).to.deep.equal({
a: true,
b: 1,
c: 'hello',
})
})

// it('should load config from json', async () => {
// expect(await loadConfig({ path: `${__dirname}/example.config.json`, schema: ConfigSchema })).to.deep.equal({
// a: true,
// b: 1,
// c: 'hello',
// })
// })

// it('should load config from js', async () => {
// expect(await loadConfig({ path: `${__dirname}/example.config.js`, schema: ConfigSchema })).to.deep.equal({
// a: true,
// b: 1,
// c: 'hello',
// })
// })

// it('should load config from ts', async () => {
// expect(await loadConfig({ path: `${__dirname}/example.config.ts`, schema: ConfigSchema })).to.deep.equal({
// a: true,
// b: 1,
// c: 'hello',
// })
// })

it('should load into process.env', async () => {
const config = await loadConfig({
path: `${__dirname}/example.config.env`,
schema: ConfigSchema,
populateProcessEnv: true,
})
for (const key of Object.keys(config)) {
expect(process.env[key]).to.equal(JSON.stringify(config[key as keyof typeof config]))
}
})
})
22 changes: 22 additions & 0 deletions packages/configure/src/tests/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { z } from 'zod'

export const ConfigSchema = z.object({
a: z.coerce.boolean(),
b: z.coerce.number(),
c: z.string(),
})

export type Config = z.infer<typeof ConfigSchema>
3 changes: 3 additions & 0 deletions packages/configure/src/tests/example.config.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a=true
b=1
c=hello
23 changes: 23 additions & 0 deletions packages/configure/src/tests/example.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'
Object.defineProperty(exports, '__esModule', { value: true })
function buildConfig() {
return {
a: true,
b: 1,
c: 'hello',
}
}
exports.default = buildConfig
5 changes: 5 additions & 0 deletions packages/configure/src/tests/example.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"a": "true",
"b": "1",
"c": "hello"
}
Loading
Loading