feat: support loading TypeScript files

This commit is contained in:
Kyle Hensel 2025-01-10 23:06:57 +11:00
parent 642c451d08
commit b314da93c5
No known key found for this signature in database
GPG Key ID: A6481E3E38D544EC
16 changed files with 125 additions and 4 deletions

View File

@ -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 = {}) {
@ -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}`);
}

1
dist/loaders/typescript.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare function checkIfTypescriptSupported(): void;

8
dist/loaders/typescript.js vendored Normal file
View File

@ -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;
}
}

View File

@ -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
*/
@ -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') {

View File

@ -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);
/**
@ -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') {

2
dist/utils.d.ts vendored
View File

@ -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
dist/utils.js vendored
View File

@ -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
*/
@ -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');
}

View File

@ -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}`)
}

10
src/loaders/typescript.ts Normal file
View File

@ -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;
}
}

View File

@ -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
@ -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') {

View File

@ -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)
@ -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') {

View File

@ -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
@ -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'
);
}

View File

@ -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> => {

View File

@ -0,0 +1,5 @@
const env: unknown = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
};
export default env;

View File

@ -0,0 +1,7 @@
import type { Environment } from '../../src/types.js';
const env: Environment = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 1,
};
export default env;

View File

@ -0,0 +1,8 @@
import type { Environment } from '../../src/types.js';
const env: Environment = {
THANKS: 'FOR ALL THE FISH',
ANSWER: 2,
};
export default env;