esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

[Feature]: handle ${configDir} TS template var in compilerOptions.paths

Open mister-good-deal opened this issue 1 year ago • 1 comments

🚀 Feature Proposal

In TS v5.5, there is a new template var ${configDir} in compilerOptions.paths definition see official doc

Esbuild should handle it and replace this var with the correct path value.

Motivation

Keep being updated with TS standard

Example

The following project monorepo structure

monorepo/
├── tsconfig.base.json    # Base TypeScript config
├── jest.config.base.ts   # Base Jest config
└── packages/
    ├── <package_1>/
    │     ├── package.json    # Extends package-base.json
    │     ├── tsconfig.json   # Extends tsconfig.base.json
    │     └── jest.config.ts  # Extends jest.config.base.ts
    └── <package_2>/
            ├── package.json    # Extends package-base.json
            ├── tsconfig.json   # Extends tsconfig.base.json
            └── jest.config.ts  # Extends jest.config.base.ts

with a defined tsconfig.base.json

{
    "compilerOptions": {
        "rootDir": "./",
        "paths": {
            "@monorepo/*": ["./packages/*"],
            "@database/*": ["${configDir}/prisma/*"]
        },
    "include": [ "jest.config.base.ts" ]
}

would resolve @database as '^@database/(.*)$': '/path/to/monorepo/packages/package_1/prisma/$1' when loading from package_1/tsconfig.json.

I know it would be much more easier to just defined @database at tsconfig.json package level but unfortunately TS does not let extend the compilerOptions.paths property with the root one, it overwrites it...

mister-good-deal avatar Jan 25 '25 16:01 mister-good-deal

As a workaround, if someone encounter this issue, here is a build script i'm using to replace ${configDir} using esbuild tsconfigRaw property with custom made tsconfig.json.

build.mjs

import esbuild from "esbuild";
import path from "node:path";
import { fileURLToPath } from "node:url";

import baseTsConfig from "../../../tsconfig.base.json" with { type: "json" };
import projectTsConfig from "../tsconfig.json" with { type: "json" };

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(__dirname, "..");
const MONOREPO_ROOT = path.resolve(PROJECT_ROOT, "../..");

// Process paths to use absolute paths
const processedPaths = Object.fromEntries(
    Object.entries(baseTsConfig.compilerOptions.paths).map(([key, value]) => [
        key,
        value.map(configPath => {
            // Handle ${configDir} replacement
            if (configPath.includes("${configDir}")) return configPath.replace("${configDir}", PROJECT_ROOT);
            // Handle @monorepo/* paths
            if (key.startsWith("@monorepo/")) return path.resolve(MONOREPO_ROOT, configPath);

            return configPath;
        })
    ])
);

// Merge configurations
const mergedConfig = {
    ...baseTsConfig,
    ...projectTsConfig,
    compilerOptions: {
        ...baseTsConfig.compilerOptions,
        ...projectTsConfig.compilerOptions,
        paths: processedPaths,
        baseUrl: MONOREPO_ROOT
    },
    extends: undefined // Remove the extends field as it's not needed in the final configuration
};

await esbuild.build({
    entryPoints: [`${PROJECT_ROOT}/src/index.ts`],
    bundle: true,
    platform: "node",
    target: "node23",
    format: "esm",
    outdir: `${PROJECT_ROOT}/dist`,
    tsconfigRaw: JSON.stringify(mergedConfig),
    resolveExtensions: [".ts", ".js", ".mjs", ".json"],
    nodePaths: [MONOREPO_ROOT],
    banner: {
        js: `
            import { createRequire } from 'module';
            import { fileURLToPath } from 'url';
            import { dirname } from 'path';
            const require = createRequire(import.meta.url);
            const __filename = fileURLToPath(import.meta.url);
            const __dirname = dirname(__filename);
        `
    }
});

mister-good-deal avatar Jan 25 '25 18:01 mister-good-deal

In theory this should already be working as the ${configDir} feature was implemented last year (see #3782). Here's a live example of it working: link.

If it's not working for you, then please provide a way to reproduce the issue. The "monorepo structure" described in the initial post is not a reproduction because it's an incomplete code sample.

evanw avatar Jul 08 '25 00:07 evanw

Closing due to lack of a reproduction.

evanw avatar Jul 18 '25 20:07 evanw