mirror of
https://github.com/toddbluhm/env-cmd.git
synced 2025-12-08 18:23:33 +00:00
chore: don't convert strings to numbers/booleans in the API
also added test cases for invalid files
This commit is contained in:
parent
8c55ce619d
commit
99774970f4
@ -56,7 +56,7 @@ export async function EnvCmd(
|
|||||||
const proc = spawn(command, commandArgs, {
|
const proc = spawn(command, commandArgs, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
shell: options.useShell,
|
shell: options.useShell,
|
||||||
env: env as Record<string, string>,
|
env,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle any termination signals for parent and child proceses
|
// Handle any termination signals for parent and child proceses
|
||||||
|
|||||||
@ -17,16 +17,16 @@ export async function getEnvFileVars(envFilePath: string): Promise<Environment>
|
|||||||
|
|
||||||
// Get the file extension
|
// Get the file extension
|
||||||
const ext = extname(absolutePath).toLowerCase()
|
const ext = extname(absolutePath).toLowerCase()
|
||||||
let env: Environment = {}
|
let env: unknown;
|
||||||
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
|
||||||
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
// For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
|
||||||
let attributeTypes = {}
|
let attributeTypes = {}
|
||||||
if (ext === '.json') {
|
if (ext === '.json') {
|
||||||
attributeTypes = { [importAttributesKeyword]: { type: 'json' } }
|
attributeTypes = { [importAttributesKeyword]: { type: 'json' } }
|
||||||
}
|
}
|
||||||
const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as Environment | { default: Environment }
|
const res: unknown = await import(pathToFileURL(absolutePath).href, attributeTypes)
|
||||||
if ('default' in res) {
|
if (typeof res === 'object' && res && 'default' in res) {
|
||||||
env = res.default as Environment
|
env = res.default
|
||||||
} else {
|
} else {
|
||||||
env = res
|
env = res
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export async function getEnvFileVars(envFilePath: string): Promise<Environment>
|
|||||||
env = await env
|
env = await env
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return normalizeEnvObject(env, absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = readFileSync(absolutePath, { encoding: 'utf8' })
|
const file = readFileSync(absolutePath, { encoding: 'utf8' })
|
||||||
@ -86,19 +86,7 @@ export function parseEnvVars(envString: string): Environment {
|
|||||||
|
|
||||||
value = value.replace(/\\n/g, '\n');
|
value = value.replace(/\\n/g, '\n');
|
||||||
|
|
||||||
// Convert string to JS type if appropriate
|
matches[key] = value
|
||||||
if (value !== '' && !isNaN(+value)) {
|
|
||||||
matches[key] = +value
|
|
||||||
}
|
|
||||||
else if (value === 'true') {
|
|
||||||
matches[key] = true
|
|
||||||
}
|
|
||||||
else if (value === 'false') {
|
|
||||||
matches[key] = false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
matches[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return JSON.parse(JSON.stringify(matches)) as Environment
|
return JSON.parse(JSON.stringify(matches)) as Environment
|
||||||
}
|
}
|
||||||
@ -118,3 +106,27 @@ export function stripEmptyLines(envString: string): string {
|
|||||||
const emptyLinesRegex = /(^\n)/gim
|
const emptyLinesRegex = /(^\n)/gim
|
||||||
return envString.replace(emptyLinesRegex, '')
|
return envString.replace(emptyLinesRegex, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we load data from a file like .js, the user
|
||||||
|
* might export something which is not an object.
|
||||||
|
*
|
||||||
|
* This function ensures that the input is valid,
|
||||||
|
* and converts the object's values to strings, for
|
||||||
|
* consistincy. See issue #125 for details.
|
||||||
|
*/
|
||||||
|
export function normalizeEnvObject(input: unknown, absolutePath: string): Environment {
|
||||||
|
if (typeof input !== 'object' || !input) {
|
||||||
|
throw new Error(`env-cmd cannot load “${absolutePath}” because it does not export an object.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const env: Environment = {};
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
// we're intentionally stringifying the value here, to
|
||||||
|
// match what `child_process.spawn` does when loading
|
||||||
|
// env variables.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
env[key] = `${value}`
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Command } from '@commander-js/extra-typings'
|
import type { Command } from '@commander-js/extra-typings'
|
||||||
|
|
||||||
// Define an export type
|
// Define an export type
|
||||||
export type Environment = Partial<Record<string, string | number | boolean>>
|
export type Environment = Partial<Record<string, string>>
|
||||||
|
|
||||||
export type RCEnvironment = Partial<Record<string, Environment>>
|
export type RCEnvironment = Partial<Record<string, Environment>>
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,8 @@ describe('parseEnvVars', (): void => {
|
|||||||
assert(envVars.BOB === 'COOL')
|
assert(envVars.BOB === 'COOL')
|
||||||
assert(envVars.NODE_ENV === 'dev')
|
assert(envVars.NODE_ENV === 'dev')
|
||||||
assert(envVars.ANSWER === '42 AND COUNTING')
|
assert(envVars.ANSWER === '42 AND COUNTING')
|
||||||
assert(envVars.NUMBER === 42)
|
assert(envVars.NUMBER === '42')
|
||||||
assert(envVars.BOOLEAN === true)
|
assert(envVars.BOOLEAN === 'true')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse out all env vars in string with format \'key=value\'', (): void => {
|
it('should parse out all env vars in string with format \'key=value\'', (): void => {
|
||||||
@ -133,7 +133,7 @@ describe('parseEnvString', (): void => {
|
|||||||
const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
|
const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
|
||||||
assert(env.BOB === 'COOL')
|
assert(env.BOB === 'COOL')
|
||||||
assert(env.NODE_ENV === 'dev')
|
assert(env.NODE_ENV === 'dev')
|
||||||
assert(env.ANSWER === 42)
|
assert(env.ANSWER === '42')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -142,10 +142,10 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test.json')
|
const env = await getEnvFileVars('./test/test-files/test.json')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR WHAT?!',
|
THANKS: 'FOR WHAT?!',
|
||||||
ANSWER: 42,
|
ANSWER: '42',
|
||||||
ONLY: 'IN PRODUCTION',
|
ONLY: 'IN PRODUCTION',
|
||||||
GALAXY: 'hitch\nhiking',
|
GALAXY: 'hitch\nhiking',
|
||||||
BRINGATOWEL: true,
|
BRINGATOWEL: 'true',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -153,10 +153,10 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test-newlines.json')
|
const env = await getEnvFileVars('./test/test-files/test-newlines.json')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR WHAT?!',
|
THANKS: 'FOR WHAT?!',
|
||||||
ANSWER: 42,
|
ANSWER: '42',
|
||||||
ONLY: 'IN\n PRODUCTION',
|
ONLY: 'IN\n PRODUCTION',
|
||||||
GALAXY: 'hitch\nhiking\n\n',
|
GALAXY: 'hitch\nhiking\n\n',
|
||||||
BRINGATOWEL: true,
|
BRINGATOWEL: 'true',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test.cjs')
|
const env = await getEnvFileVars('./test/test-files/test.cjs')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR ALL THE FISH',
|
THANKS: 'FOR ALL THE FISH',
|
||||||
ANSWER: 0,
|
ANSWER: '0',
|
||||||
GALAXY: 'hitch\nhiking',
|
GALAXY: 'hitch\nhiking',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -173,7 +173,7 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test-async.cjs')
|
const env = await getEnvFileVars('./test/test-files/test-async.cjs')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR ALL THE FISH',
|
THANKS: 'FOR ALL THE FISH',
|
||||||
ANSWER: 0,
|
ANSWER: '0',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test.mjs')
|
const env = await getEnvFileVars('./test/test-files/test.mjs')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR ALL THE FISH',
|
THANKS: 'FOR ALL THE FISH',
|
||||||
ANSWER: 0,
|
ANSWER: '0',
|
||||||
GALAXY: 'hitch\nhiking',
|
GALAXY: 'hitch\nhiking',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -190,7 +190,7 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test-async.mjs')
|
const env = await getEnvFileVars('./test/test-files/test-async.mjs')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR ALL THE FISH',
|
THANKS: 'FOR ALL THE FISH',
|
||||||
ANSWER: 0,
|
ANSWER: '0',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -198,13 +198,13 @@ describe('getEnvFileVars', (): void => {
|
|||||||
const env = await getEnvFileVars('./test/test-files/test')
|
const env = await getEnvFileVars('./test/test-files/test')
|
||||||
assert.deepEqual(env, {
|
assert.deepEqual(env, {
|
||||||
THANKS: 'FOR WHAT?!',
|
THANKS: 'FOR WHAT?!',
|
||||||
ANSWER: 42,
|
ANSWER: '42',
|
||||||
ONLY: 'IN=PRODUCTION',
|
ONLY: 'IN=PRODUCTION',
|
||||||
GALAXY: 'hitch\nhiking',
|
GALAXY: 'hitch\nhiking',
|
||||||
BRINGATOWEL: true,
|
BRINGATOWEL: 'true',
|
||||||
a: 1,
|
a: '1',
|
||||||
b: 2,
|
b: '2',
|
||||||
c: 3,
|
c: '3',
|
||||||
d: "=",
|
d: "=",
|
||||||
e: "equals symbol = = ",
|
e: "equals symbol = = ",
|
||||||
json_no_quotes: "{\"foo\": \"bar\"}",
|
json_no_quotes: "{\"foo\": \"bar\"}",
|
||||||
@ -223,4 +223,16 @@ describe('getEnvFileVars', (): void => {
|
|||||||
assert.match(e.message, /file path/gi)
|
assert.match(e.message, /file path/gi)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const fileExt of ['cjs', 'mjs', 'json']) {
|
||||||
|
it(`should throw an error when importing a ${fileExt} file with an invalid export`, async () => {
|
||||||
|
try {
|
||||||
|
await getEnvFileVars(`./test/test-files/invalid.${fileExt}`)
|
||||||
|
assert.fail('Should not get here!')
|
||||||
|
} catch (e) {
|
||||||
|
assert.instanceOf(e, Error)
|
||||||
|
assert.match(e.message, /does not export an object/gi)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
1
test/test-files/invalid.cjs
Normal file
1
test/test-files/invalid.cjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = undefined;
|
||||||
1
test/test-files/invalid.json
Normal file
1
test/test-files/invalid.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
123
|
||||||
1
test/test-files/invalid.mjs
Normal file
1
test/test-files/invalid.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default "this is invalid; it's not an object";
|
||||||
Loading…
x
Reference in New Issue
Block a user