Skip to content

Commit

Permalink
feat: support loading TypeScript files
Browse files Browse the repository at this point in the history
  • Loading branch information
k-yle committed Jan 10, 2025
1 parent 8c55ce6 commit 02dbcc5
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 2 deletions.
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
11 changes: 11 additions & 0 deletions src/loaders/typescript.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
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, importAttributesKeyword } 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: 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') {
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, 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)
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 @@ -33,6 +42,15 @@ export function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T
&& 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'
);
}


// "Import Attributes" are only supported since node v18.20 and v20.10.
// For older node versions, we have to use "Import Assertions".
Expand Down
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;

0 comments on commit 02dbcc5

Please sign in to comment.