ui icon indicating copy to clipboard operation
ui copied to clipboard

[feat]: CLI support for package.json subpath imports

Open jeremy-code opened this issue 1 year ago • 15 comments
trafficstars

Feature description

In TypeScript 5.4, subpath imports are now supported. In components.json, if the subpath import is added as an alias like:

"aliases": {
    "components": "#components"
}

It will create a new folder called #components, and put all the components in there. Conversely, if the alias is set to the actual components directory (e.g. ./src/components/ui), an error will be thrown as an array of errors like:

{
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": [
      "resolvedPaths",
      "components"
    ],
    "message": "Required"
},

Not setting any alias at all results in error "Invalid configuration found in ../components.json".

In the meantime, I have set it with the subpath imports as the alias, and added bash commands to move it to the correctly place, but this isn't ideal.

"ui:add": "yarn dlx shadcn-ui@latest add && mv ./#components/ui/* ./src/components/ui/ && rm -rf ./#components",

Affected component/components

No response

Additional Context

No response

Before submitting

  • [X] I've made research efforts and searched the documentation
  • [X] I've searched for existing issues and PRs

jeremy-code avatar Mar 09 '24 03:03 jeremy-code

This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.

shadcn avatar Jun 10 '24 23:06 shadcn

Turborepo now officially recommends node subpath imports for internal packages in their docs:

A library that is being transpiled by its consumer cannot use the compilerOptions.paths configuration because TypeScript assumes that source code is being transpiled in the package where it is written. If you're using Typescript 5.4 or later, we recommend using Node.js subpath imports.

Thanks @jeremy-code for the workaround, but would be amazing if shadcn supported this at some point, since I imagine this will become much more prevalent in the near future.

jam-fran avatar Jul 18 '24 18:07 jam-fran

I'm facing a similar issue, it would be cool that the CLI get fixed on the path alias. Currently, it seems like if the CLI detects that in the tsconfig there is a "wrong" alias (such as #utils), it fallbacks to #/lib/utils.

Node subpath can only begin with a # so it doesn't get confused with public scoped packages (that start with @) The problem is that node subpath doesn't allow an alias that starts with #/. And shadcn CLI doesn't seem to work with anything that don't start with <character>/. This trailing slash makes the problem, basically

The only way to fix it is whenever you add a new component with the CLI, you need to fix the path alias by hand (unless someone else finds another solution?)

sqmasep avatar Oct 03 '24 19:10 sqmasep

Reactivate this please!

SanderCokart avatar Nov 27 '24 11:11 SanderCokart

A workaround that involves not using aliases is using Node.js self-referencing.

For instance, see the following code:

// package.json
{
  "name": "@repo/ui",
  "exports": {
    "./globals.css": "./globals.css",
    "./postcss.config": "./postcss.config.js",
    "./tailwind.config": "./tailwind.config.ts",
    "./utils": "./utils.ts",
    "./components/*": "./components/*.tsx",
    "./components/ui/*": "./components/ui/*.tsx"
  },
}

// components.json
{
  "aliases": {
    "utils": "@repo/ui/utils",
    "components": "@repo/ui/components"
  }
}

Then, internally (in @repo/ui), imports will look like import { cn } from "@repo/ui/utils"; and in external apps like import { Switch } from "@repo/ui/components/ui/switch";. If you're not using a module bundler, you'll have to update the export map for .js.

IMO, due to the package structure (few imports), this is more useful than using aliasing since it just bases everything based on what you export. The only issue is if you have some internal tool in the library that you don't want to export, but at least for shadcn/ui CLI, there isn't any instances of this.


EDIT:

shadcn/ui has released experimental monorepo support using the internal packages (@workspace/ui) structure.

jeremy-code avatar Dec 02 '24 21:12 jeremy-code

After playing around with things for a while, I was able to get subpath imports working with shadcn/ui components nearly perfectly. My package is a Compiled Package, thus the dist output path being referenced, but I believe this should work if yours is a Just-In-Time Package if you update the imports path to point to your TS files instead of the compiled files.

// package.json
{
  ...
  "imports": {
    "#*": "./dist/*.js"
  },
  ...
}
// tsconfig.json
{
  ...
  "compilerOptions": {
    "paths": {
      "#*": ["./src/*"]
    },
  }
  ...
}
// components.json
{
  ...
  "aliases": {
    "components": "#components",
    "hooks": "#hooks",
    "lib": "#lib",
    "ui": "#components/ui",
    "utils": "#lib/utils"
  }
  ...
}

It is unfortunate to need the TS path alias in addition to the subpath import, but this gets you 99% there. The only thing that does not seem to work with this setup is the generated cn import, which for some reason gets imported as #/lib/utils instead of #lib/utils. As @sqmasep pointed out above, you'll have to fix this import by hand.

bdbergeron avatar Dec 07 '24 01:12 bdbergeron

A workaround that involves not using aliases is using Node.js self-referencing.

For instance, see the following code:

// package.json
{
  "name": "@repo/ui",
  "exports": {
    "./globals.css": "./globals.css",
    "./postcss.config": "./postcss.config.js",
    "./tailwind.config": "./tailwind.config.ts",
    "./utils": "./utils.ts",
    "./components/*": "./components/*.tsx",
    "./components/ui/*": "./components/ui/*.tsx"
  },
}

// components.json
{
  "aliases": {
    "utils": "@repo/ui/utils",
    "components": "@repo/ui/components"
  }
}

Then, internally (in @repo/ui), imports will look like import { cn } from "@repo/ui/utils"; and in external apps like import { Switch } from "@repo/ui/components/ui/switch";. If you're not using a module bundler, you'll have to update the export map for .js.

IMO, due to the package structure (few imports), this is more useful than using aliasing since it just bases everything based on what you export. The only issue is if you have some internal tool in the library that you don't want to export, but at least for shadcn/ui CLI, there isn't any instances of this.

I ended up going the self-referencing route, here's what the exports (in package.json) ended up looking like:

  "exports": {
    "./utils/*": {
      "types": "./src/lib/utils/*.ts",
      "default": "./src/lib/utils/*.ts"
    },
    "./lib/*": {
      "types": "./src/lib/*.ts",
      "default": "./src/lib/*.ts"
    },
    "./components/*": {
      "types": "./src/components/*.tsx",
      "default": "./src/components/*.tsx"
    },
    "./ui/*": {
      "types": "./src/components/ui/*.tsx",
      "default": "./src/components/ui/*.tsx"
    },
    "./hooks/*": {
      "types": "./src/hooks/*.tsx",
      "default": "./src/hooks/*.tsx"
    }
  }

And in tsconfig.json:

    "paths": {
      "@repo/ui/*": ["./src/*"]
    }

This helped me get rid of .ts/tsx extensions at the app level when importing code, allows bundlers and typescript to properly identify what code is relevant, and allows shadcn cli to work (for the most part, the bugs that exist around the utils import at the moment still affect us here). This also helped me keep this library configured as an "internal" typescript package as outlined in https://turbo.build/blog/you-might-not-need-typescript-project-references#internal-typescript-packages

I think I ran into issues with transpilePackages config in nextjs when I tried to use the "imports" setup in package.json and tried to build, not entirely sure as it's been a while since I tested that particular setup.

jdfm avatar Jan 02 '25 21:01 jdfm

This issue has been automatically marked as stale due to two years of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you.

shadcn avatar Mar 02 '25 11:03 shadcn

@shadcn I think this issue is worth keeping alive. subpath imports are a convenient option that bundlers and tools like tsc now understand. Supporting it potentially means we don't have to worry about aligning components.json, tsconfig.json and vite.config.js.

disintegrator avatar May 29 '25 15:05 disintegrator

Agreed - using the CLI is a big pain in our projects that use subpath imports

dpnova avatar Jun 10 '25 03:06 dpnova

This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)

shadcn avatar Jul 30 '25 23:07 shadcn

Reactivate this please!

valerii15298 avatar Oct 20 '25 22:10 valerii15298

We recently removed all path aliases from Epic Stack's default tsconfig (see epicweb-dev/epic-stack#1060 for the fallout) so new projects rely on TypeScript 5.4+ subpath imports like #app.

When we run npx shadcn@latest add dropdown-menu, the CLI resolves the alias literally and scaffolds into #app/components/ui instead of the intended ./app/components/ui.

Because the CLI doesn’t understand the # subpath, teams have to add manual shell steps to move the files after every install. Supporting package.json subpath aliases (or at least ignoring the leading #) would let us keep the cleaner compiler defaults without breaking the install workflow.

kentcdodds avatar Nov 12 '25 19:11 kentcdodds

I just got the ping on this. Reopening...

shadcn avatar Nov 13 '25 06:11 shadcn

I don't know if I can get to it this week. But I can review and merge a PR if anyone want to take a look. Thank you.

shadcn avatar Nov 13 '25 06:11 shadcn