Skip to content

feat: support loading TypeScript files #399

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

Merged
merged 2 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion dist/get-env-vars.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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'];
export async function getEnvVars(options = {}) {
Expand Down Expand Up @@ -28,7 +29,10 @@ export async function getEnvFile({ filePath, fallback, verbose }) {
}
return env;
}
catch {
catch (error) {
if (isLoaderError(error)) {
throw error;
}
if (verbose === true) {
console.info(`Failed to find .env file at path: ${filePath}`);
}
Expand Down
1 change: 1 addition & 0 deletions dist/loaders/typescript.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function checkIfTypescriptSupported(): void;
8 changes: 8 additions & 0 deletions dist/loaders/typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function checkIfTypescriptSupported() {
if (!process.features.typescript) {
const error = new Error('To load typescript files with env-cmd, you need to 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;
}
}
3 changes: 3 additions & 0 deletions dist/parse-env-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
import { extname } from 'node:path';
import { pathToFileURL } from 'node:url';
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
import { checkIfTypescriptSupported } from './loaders/typescript.js';
/**
* Gets the environment vars from an env file
*/
Expand All @@ -16,6 +17,8 @@ export async function getEnvFileVars(envFilePath) {
const ext = extname(absolutePath).toLowerCase();
let env;
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') {
Expand Down
3 changes: 3 additions & 0 deletions dist/parse-rc-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { promisify } from 'node:util';
import { extname } from 'node:path';
import { pathToFileURL } from 'node:url';
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
import { checkIfTypescriptSupported } from './loaders/typescript.js';
const statAsync = promisify(stat);
const readFileAsync = promisify(readFile);
/**
Expand All @@ -23,6 +24,8 @@ export async function getRCFileVars({ environments, filePath }) {
let parsedData = {};
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') {
Expand Down
2 changes: 2 additions & 0 deletions dist/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export declare function parseArgList(list: string): string[];
* A simple function to test if the value is a promise/thenable
*/
export declare function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T>;
/** @returns true if the error is `ERR_UNKNOWN_FILE_EXTENSION` */
export declare function isLoaderError(error: unknown): error is Error;
17 changes: 16 additions & 1 deletion dist/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { resolve } from 'node:path';
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
*/
Expand All @@ -29,3 +38,9 @@ export function isPromise(value) {
&& 'then' in value
&& typeof value.then === 'function';
}
/** @returns true if the error is `ERR_UNKNOWN_FILE_EXTENSION` */
export function isLoaderError(error) {
return (error instanceof Error &&
'code' in error &&
error.code === 'ERR_UNKNOWN_FILE_EXTENSION');
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"Eric Lanehart <[email protected]>",
"Jon Scheiding <[email protected]>",
"serapath (Alexander Praetorius) <[email protected]>",
"Kyle Hensel <[email protected]>",
"Anton Versal <[email protected]>"
],
"license": "MIT",
Expand Down
7 changes: 6 additions & 1 deletion src/get-env-vars.ts
Original file line number Diff line number Diff line change
@@ -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']
Expand Down Expand Up @@ -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}`)
}
Expand Down
10 changes: 10 additions & 0 deletions src/loaders/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function checkIfTypescriptSupported() {
if (!process.features.typescript) {
const error = new Error(
'To load typescript files with env-cmd, you need to 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;
}
}
3 changes: 3 additions & 0 deletions src/parse-env-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { extname } from 'node:path'
import { pathToFileURL } from 'node:url'
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import type { Environment } from './types.ts'
import { checkIfTypescriptSupported } from './loaders/typescript.js'

/**
* Gets the environment vars from an env file
Expand All @@ -19,6 +20,8 @@ export async function getEnvFileVars(envFilePath: string): Promise<Environment>
const ext = extname(absolutePath).toLowerCase()
let env: unknown;
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') {
Expand Down
3 changes: 3 additions & 0 deletions src/parse-rc-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { extname } from 'node:path'
import { pathToFileURL } from 'node:url'
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import type { Environment, RCEnvironment } from './types.ts'
import { checkIfTypescriptSupported } from './loaders/typescript.js'

const statAsync = promisify(stat)
const readFileAsync = promisify(readFile)
Expand All @@ -30,6 +31,8 @@ export async function getRCFileVars(
let parsedData: Partial<RCEnvironment> = {}
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') {
Expand Down
20 changes: 19 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -32,3 +41,12 @@ export function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T
&& 'then' in value
&& typeof value.then === 'function'
}

/** @returns true if the error is `ERR_UNKNOWN_FILE_EXTENSION` */
export function isLoaderError(error: unknown): error is Error {
return (
error instanceof Error &&
'code' in error &&
error.code === 'ERR_UNKNOWN_FILE_EXTENSION'
);
}
26 changes: 26 additions & 0 deletions test/parse-env-file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,32 @@ describe('getEnvFileVars', (): void => {
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<void> => {
Expand Down
5 changes: 5 additions & 0 deletions test/test-files/cts-test.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const env: unknown = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
};
export default env;
7 changes: 7 additions & 0 deletions test/test-files/ts-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Environment } from '../../src/types.js';

const env: Environment = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 1,
};
export default env;
8 changes: 8 additions & 0 deletions test/test-files/tsx-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Environment } from '../../src/types.js';

const env: Environment = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 2,
};

export default env;
Loading