From 02dbcc53a1d9906382ab9ce0588dc154394519e3 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Fri, 10 Jan 2025 23:06:57 +1100 Subject: [PATCH 1/2] feat: support loading TypeScript files --- src/get-env-vars.ts | 7 ++++++- src/loaders/typescript.ts | 11 +++++++++++ src/parse-env-file.ts | 3 +++ src/parse-rc-file.ts | 3 +++ src/utils.ts | 20 +++++++++++++++++++- test/parse-env-file.spec.ts | 26 ++++++++++++++++++++++++++ test/test-files/cts-test.cts | 5 +++++ test/test-files/ts-test.ts | 7 +++++++ test/test-files/tsx-test.tsx | 8 ++++++++ 9 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/loaders/typescript.ts create mode 100644 test/test-files/cts-test.cts create mode 100644 test/test-files/ts-test.ts create mode 100644 test/test-files/tsx-test.tsx diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index 89f2f47..ac5cf4f 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -1,6 +1,7 @@ import type { GetEnvVarOptions, Environment } from './types.ts' import { getRCFileVars } from './parse-rc-file.js' import { getEnvFileVars } from './parse-env-file.js' +import { isLoaderError } from './utils.js' const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'] const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'] @@ -34,7 +35,11 @@ export async function getEnvFile( } return env } - catch { + catch (error) { + if (isLoaderError(error)) { + throw error + } + if (verbose === true) { console.info(`Failed to find .env file at path: ${filePath}`) } diff --git a/src/loaders/typescript.ts b/src/loaders/typescript.ts new file mode 100644 index 0000000..65cd99c --- /dev/null +++ b/src/loaders/typescript.ts @@ -0,0 +1,11 @@ +export function checkIfTypescriptSupported() { + if (!process.features.typescript) { + const error = new Error( + 'To load typescript files with env-cmd, you need to enable ' + + 'node’s --experimental-strip-types option, or upgrade to node ' + + 'v23.6 or later. See https://nodejs.org/en/learn/typescript/run-natively', + ); + Object.assign(error, { code: 'ERR_UNKNOWN_FILE_EXTENSION' }); + throw error; + } +} diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index 7e44d88..7031b60 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -3,6 +3,7 @@ import { extname } from 'node:path' import { pathToFileURL } from 'node:url' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise, importAttributesKeyword } from './utils.js' import type { Environment } from './types.ts' +import { checkIfTypescriptSupported } from './loaders/typescript.js' /** * Gets the environment vars from an env file @@ -19,6 +20,8 @@ export async function getEnvFileVars(envFilePath: string): Promise const ext = extname(absolutePath).toLowerCase() let env: Environment = {} if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + if (/tsx?$/.test(ext)) checkIfTypescriptSupported(); + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them let attributeTypes = {} if (ext === '.json') { diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index f4bfa14..2e21b89 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -4,6 +4,7 @@ import { extname } from 'node:path' import { pathToFileURL } from 'node:url' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise, importAttributesKeyword } from './utils.js' import type { Environment, RCEnvironment } from './types.ts' +import { checkIfTypescriptSupported } from './loaders/typescript.js' const statAsync = promisify(stat) const readFileAsync = promisify(readFile) @@ -30,6 +31,8 @@ export async function getRCFileVars( let parsedData: Partial = {} try { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + if (/tsx?$/.test(ext)) checkIfTypescriptSupported() + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them let attributeTypes = {} if (ext === '.json') { diff --git a/src/utils.ts b/src/utils.ts index 2b005c1..49bc2ea 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,16 @@ import { homedir } from 'node:os' import { cwd } from 'node:process' // Special file extensions that node can natively import -export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs'] +export const IMPORT_HOOK_EXTENSIONS = [ + '.json', + '.js', + '.cjs', + '.mjs', + '.ts', + '.mts', + '.cts', + '.tsx', +]; /** * A simple function for resolving the path the user entered @@ -33,6 +42,15 @@ export function isPromise(value?: T | PromiseLike): value is PromiseLike { THANKS: 'FOR ALL THE FISH', ANSWER: 0, }) + }); + + (process.features.typescript ? describe : describe.skip)('TS', () => { + it('should parse a .ts file', async () => { + const env = await getEnvFileVars('./test/test-files/ts-test.ts'); + assert.deepEqual(env, { + THANKS: 'FOR ALL THE FISH', + ANSWER: 1, + }); + }); + + it('should parse a .cts file', async () => { + const env = await getEnvFileVars('./test/test-files/cts-test.cts'); + assert.deepEqual(env, { + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, + }); + }); + + it('should parse a .tsx file', async () => { + const env = await getEnvFileVars('./test/test-files/tsx-test.tsx'); + assert.deepEqual(env, { + THANKS: 'FOR ALL THE FISH', + ANSWER: 2, + }); + }); }) it('should parse an env file', async (): Promise => { diff --git a/test/test-files/cts-test.cts b/test/test-files/cts-test.cts new file mode 100644 index 0000000..32b57be --- /dev/null +++ b/test/test-files/cts-test.cts @@ -0,0 +1,5 @@ +const env: unknown = { + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, +}; +export default env; diff --git a/test/test-files/ts-test.ts b/test/test-files/ts-test.ts new file mode 100644 index 0000000..5d8e40c --- /dev/null +++ b/test/test-files/ts-test.ts @@ -0,0 +1,7 @@ +import type { Environment } from '../../src/types.js'; + +const env: Environment = { + THANKS: 'FOR ALL THE FISH', + ANSWER: 1, +}; +export default env; diff --git a/test/test-files/tsx-test.tsx b/test/test-files/tsx-test.tsx new file mode 100644 index 0000000..ecb6549 --- /dev/null +++ b/test/test-files/tsx-test.tsx @@ -0,0 +1,8 @@ +import type { Environment } from '../../src/types.js'; + +const env: Environment = { + THANKS: 'FOR ALL THE FISH', + ANSWER: 2, +}; + +export default env; From 5a0264fe147b177a046e0ba8aa411a4e83a02c9d Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Fri, 10 Jan 2025 23:20:52 +1100 Subject: [PATCH 2/2] chore: run tests on node v23 --- .github/workflows/linux-tests.yml | 2 +- .github/workflows/windows-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 5a5c6d0..f5dfce9 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 23.x] steps: - name: Checkout Project diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index 574e919..38c1345 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 23.x] steps: - name: Checkout Project