Skip to content

Catch-all "*": ["./src/*"] path (the official TS7 baseUrl replacement) is never rewritten #265

Description

@TrevorSayre

Summary

With baseUrl removed and a single catch-all paths entry (e.g. "*": ["./src/*"]),
tsc-alias leaves all matching imports unchanged. This is the exact configuration
the TypeScript team recommends as the replacement for baseUrl in TypeScript 7
(microsoft/TypeScript#62207), so it is likely to become a common setup.

Related: #255 (closed) and #134 describe the broader no-baseUrl basePath
miscomputation. This report is narrower and, I think, more actionable: it pins
down why the catch-all specifically produces no output, with source references.

Environment

  • tsc-alias: 1.9.2 (latest)
  • TypeScript: 7.0.2 (confirmed; tsc itself compiles the catch-all config without
    error, so TS7 resolves "*": ["./src/*"] fine and only tsc-alias fails to
    rewrite the emitted import). Also reproduces on TypeScript 6.x.
  • Config: module/moduleResolution: NodeNext, rootDir: src, outDir: dist,
    no baseUrl.

Minimal reproduction

tsconfig.json:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ESNext",
    "rootDir": "src",
    "outDir": "dist",
    "paths": {
      "*": ["./src/*"],
    },
  },
  "include": ["src/**/*"],
}

src/config.ts:

export const NAME = "example";

src/index.ts:

import { NAME } from "config.js";
console.log(NAME);

Run:

tsc && tsc-alias -p tsconfig.json

Expected dist/index.js:

import { NAME } from "./config.js";

Actual dist/index.js (unchanged, fails at runtime under Node):

import { NAME } from "config.js";

Root cause

Two things combine in 1.9.2:

  1. The catch-all alias is dropped from the trie. In dist/utils/trie.js,
    buildAliasTrie computes prefix = alias.replace(/\*$/, ""), which is ""
    for a bare "*", and then only adds the alias when if (alias.prefix) is
    truthy. An empty prefix is falsy, so "*" is silently skipped and the default
    replacer never has anything to match.

  2. The implicit-baseUrl branch never sets baseUrl. In
    dist/helpers/config.js, when baseUrl is absent but paths exist, the
    else if branch rewrites each path relative to rootDir ("./src/*" becomes
    "*") but does not assign config.baseUrl = rootDir. baseUrl therefore
    defaults to "", so the built-in base-url replacer stays disabled
    (enabled: !!config.baseUrl in dist/helpers/replacers.js) and cannot pick up
    the bare specifier either.

Net effect: neither the default (trie) replacer nor the base-url replacer handles
the catch-all, so nothing is rewritten.

Suggested fix direction

Either (or both):

  • Treat an empty-prefix "*" as a valid catch-all in buildAliasTrie (match any
    non-relative specifier, resolving the target against outDir), rather than
    discarding it.
  • In the implicit-baseUrl branch of loadConfig, set config.baseUrl = rootDir
    (as PR fix: support ts 6 implicit baseURL #259 originally did) so the existing base-url replacer activates and
    resolves bare specifiers against outDir by file existence.

Happy to open a PR with a projects/projectNN test case mirroring the TS7
catch-all config if a maintainer confirms this direction fits the current rework
in #262.

Workaround for anyone stuck today

A tiny custom replacer reproduces the base-url behavior without needing baseUrl.
It rewrites any bare specifier that resolves to an emitted file under outDir, and
leaves node_modules imports alone:

// tsc-alias.replacer.cjs
"use strict";
const { statSync } = require("node:fs");
const { dirname, relative, resolve } = require("node:path");

const specifierRegex = /(?<quoted>["'](?<path>[^"'\r\n]+)["'])/;
const isFile = (t) => {
  try {
    return statSync(t).isFile();
  } catch {
    return false;
  }
};

module.exports.default = function localImportsReplacer({ orig, file, config }) {
  const match = orig.match(specifierRegex);
  const specifier = match && match.groups.path;
  if (!specifier || specifier.startsWith(".")) return orig;

  const target = resolve(config.outPath, specifier);
  if (!isFile(target)) return orig;

  let rel = relative(dirname(file), target).replace(/\\/g, "/");
  if (!rel.startsWith(".")) rel = "./" + rel;

  return orig.replace(match.groups.quoted, match.groups.quoted.replace(specifier, rel));
};

Wire it in: tsc-alias -p tsconfig.json --replacer ./tsc-alias.replacer.cjs. Note
this relies on specifiers carrying explicit extensions (config.js,
utils/index.js), which is the norm under NodeNext.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions