-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support a vite
entry point (export condition / main field)
#15643
Changes from all commits
ebabdf2
7434427
e982306
ab01a0d
2f89104
244e46f
fa1ab73
fecb677
5354cd4
5f69bf2
4b82710
b18666f
6b8fcb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# Monorepo packages | ||
|
||
Vite has first-class support for bundling local packages in a monorepo without the need for a build step. We currently support 2 methods for bundling (A and B below), each with their own tradeoffs, so use the method that works best for you! | ||
|
||
## A) Add `vite` entry points (experimental) | ||
|
||
This method is based entirely around `package.json` entry points. For local packages using `exports`, add a `vite` condition that maps to a source file (condition must be listed first). For other packages, add a `vite` entry point similar to `main`. | ||
|
||
### Requirements | ||
|
||
- Requires entry points within the package's `package.json`. | ||
- Requires package (npm, pnpm, yarn) workspaces. | ||
- Requires the package to be symlinked in `node_modules`. | ||
|
||
Because this functionality is experimental, it must be enabled in your config. | ||
|
||
```js | ||
import { defineConfig } from 'vite' | ||
|
||
export default defineConfig({ | ||
experimental: { | ||
vitePackageEntryPoints: true, | ||
}, | ||
}) | ||
``` | ||
|
||
### Implementation | ||
|
||
For each local package that you want to bundle source files, add `vite` entry points (1 or 2, not both). | ||
|
||
```json | ||
// 1) Using exports (supports deep imports) | ||
{ | ||
"exports": { | ||
"." { | ||
"vite": "./src/index.ts", | ||
"module": "./lib/index.js" | ||
}, | ||
"./*": { | ||
"vite": ["./src/*.ts", "./src/*.tsx"], | ||
"module": "./lib/*.js" | ||
} | ||
} | ||
} | ||
|
||
// 2) Not using exports (default import only) | ||
{ | ||
"vite": "src/index.ts", | ||
"main": "lib/index.js" | ||
} | ||
``` | ||
|
||
:::info | ||
For packages that are depended on (not symlinked in `node_modules`), the `vite` entry points are ignored, and are safe to publish. | ||
::: | ||
|
||
### Caveats | ||
|
||
- If using `vite` export condition: | ||
- To support deep imports, the `./*` entry point must be defined, which maps 1:1 to the file system. | ||
- To support multiple source files with different extensions, use an array as the `vite` condition value (example above). | ||
- If using `vite` main entry point: | ||
- Only default/index imports are supported. | ||
- Deep imports are not supported. | ||
- Packages that are not symlinked into `node_modules` will _not_ use the `vite` entry points. For reference, `workspace:`, `portal:`, and `link:` are supported, while `file:`, `git:`, etc are not. | ||
|
||
## B) Define resolve aliases | ||
|
||
The tried-and-true method for bundling local packages is to define the [`resolve.alias`](/config/shared-options.html#resolve-alias) setting, and map each package name to their source folder. | ||
|
||
### Requirements | ||
|
||
- Ignores `main` and `exports` within the package's `package.json`. | ||
- Does not require package workspaces. | ||
- Does not require the package to exist in `node_modules`. | ||
|
||
### Implementation | ||
|
||
```js | ||
import path from 'node:path' | ||
import { defineConfig } from 'vite' | ||
|
||
export default defineConfig({ | ||
resolve: { | ||
alias: { | ||
'@brand/components': path.join(__dirname, '../packages/components'), | ||
'@brand/utils': path.join(__dirname, '../packages/utils'), | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
Once aliases have been defined, you can import from them as if they were installed from npm. Both default and deep imports are supported! | ||
|
||
```js | ||
import Button from '@brand/components/Button.vue' | ||
import { camelCase } from '@brand/utils/string' | ||
``` | ||
|
||
## TypeScript path aliases | ||
|
||
Regardless of the method you choose above, you'll most likely need to define `tsconfig.json` paths for TypeScript to resolve type information from the local package source files. | ||
|
||
```json | ||
{ | ||
"compilerOptions": { | ||
"paths": { | ||
// Default import only | ||
"@brand/components": ["../packages/components/src/index.ts"], | ||
// With deep imports | ||
"@brand/components/*": ["../packages/components/src/*"] | ||
} | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,6 +125,9 @@ export interface InternalResolveOptions extends Required<ResolveOptions> { | |
* @internal | ||
*/ | ||
idOnly?: boolean | ||
|
||
// Maps to the experiment of the same name | ||
resolveLocalPackageSources?: boolean | ||
} | ||
|
||
export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { | ||
|
@@ -479,7 +482,7 @@ function resolveSubpathImports( | |
if (!pkgData) return | ||
|
||
let importsPath = resolveExportsOrImports( | ||
pkgData.data, | ||
pkgData, | ||
id, | ||
options, | ||
targetWeb, | ||
|
@@ -761,13 +764,27 @@ export function tryNodeResolve( | |
const unresolvedId = deepMatch ? '.' + id.slice(pkgId.length) : id | ||
|
||
let resolved: string | undefined | ||
try { | ||
resolved = resolveId(unresolvedId, pkg, targetWeb, options) | ||
} catch (err) { | ||
if (!options.tryEsmOnly) { | ||
throw err | ||
|
||
// If a local package, attempt to resolve the original source file, | ||
// and avoid entry points or pre-built files | ||
if (pkg.inWorkspace && options.resolveLocalPackageSources) { | ||
try { | ||
resolved = resolveSourceFile(unresolvedId, pkg, targetWeb, options) | ||
} catch { | ||
// If nothing found, resolve with other methods | ||
} | ||
} | ||
|
||
if (!resolved) { | ||
try { | ||
resolved = resolveId(unresolvedId, pkg, targetWeb, options) | ||
} catch (err) { | ||
if (!options.tryEsmOnly) { | ||
throw err | ||
} | ||
} | ||
} | ||
|
||
if (!resolved && options.tryEsmOnly) { | ||
resolved = resolveId(unresolvedId, pkg, targetWeb, { | ||
...options, | ||
|
@@ -959,11 +976,12 @@ export async function tryOptimizedResolve( | |
|
||
export function resolvePackageEntry( | ||
id: string, | ||
{ dir, data, setResolvedCache, getResolvedCache }: PackageData, | ||
pkg: PackageData, | ||
targetWeb: boolean, | ||
options: InternalResolveOptions, | ||
): string | undefined { | ||
const { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id) | ||
const { dir, data, setResolvedCache, getResolvedCache } = pkg | ||
|
||
const cached = getResolvedCache('.', targetWeb) | ||
if (cached) { | ||
|
@@ -977,7 +995,7 @@ export function resolvePackageEntry( | |
// using https://github.com/lukeed/resolve.exports | ||
if (data.exports) { | ||
entryPoint = resolveExportsOrImports( | ||
data, | ||
pkg, | ||
'.', | ||
options, | ||
targetWeb, | ||
|
@@ -1054,6 +1072,27 @@ export function resolvePackageEntry( | |
packageEntryFailure(id) | ||
} | ||
|
||
export function resolveSourceFile( | ||
id: string, | ||
pkg: PackageData, | ||
targetWeb: boolean, | ||
options: InternalResolveOptions, | ||
): string | undefined { | ||
const srcDir = pkg.srcDir ?? pkg.dir | ||
|
||
return tryFsResolve( | ||
id.startsWith('.') | ||
? // File relative from package source directory | ||
path.join(srcDir, id) | ||
: // Is the package name, so return an index entry point | ||
srcDir, | ||
options, | ||
true, | ||
targetWeb, | ||
true, | ||
) | ||
} | ||
|
||
function packageEntryFailure(id: string, details?: string) { | ||
const err: any = new Error( | ||
`Failed to resolve entry for package "${id}". ` + | ||
|
@@ -1065,7 +1104,7 @@ function packageEntryFailure(id: string, details?: string) { | |
} | ||
|
||
function resolveExportsOrImports( | ||
pkg: PackageData['data'], | ||
pkg: PackageData, | ||
key: string, | ||
options: InternalResolveOptionsWithOverrideConditions, | ||
targetWeb: boolean, | ||
|
@@ -1091,27 +1130,40 @@ function resolveExportsOrImports( | |
}) | ||
|
||
const fn = type === 'imports' ? imports : exports | ||
const result = fn(pkg, key, { | ||
const result = fn(pkg.data, key, { | ||
browser: targetWeb && !additionalConditions.has('node'), | ||
require: options.isRequire && !additionalConditions.has('import'), | ||
conditions, | ||
}) | ||
|
||
return result ? result[0] : undefined | ||
if (!result) { | ||
return undefined | ||
} | ||
|
||
// We need to support array conditions like `["./*.js", "./*.mjs"]` | ||
// So loop through each result and find one that actually exists | ||
if (result.length > 1) { | ||
for (const entry of result) { | ||
if (fs.existsSync(path.join(pkg.dir, entry))) { | ||
return entry | ||
} | ||
} | ||
} | ||
Comment on lines
+1143
to
+1151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We currently intentionally don't support this as it's not following the spec. Node.js only specifies to use the next element if the specifier is invalid, not if it don't exist on the fs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to know, I can remove it. |
||
|
||
// If nothing exists, return the 1st entry and let the other layers | ||
// handle the appropriate error | ||
return result[0] | ||
} | ||
|
||
function resolveDeepImport( | ||
id: string, | ||
{ | ||
webResolvedImports, | ||
setResolvedCache, | ||
getResolvedCache, | ||
dir, | ||
data, | ||
}: PackageData, | ||
pkg: PackageData, | ||
targetWeb: boolean, | ||
options: InternalResolveOptions, | ||
): string | undefined { | ||
const { webResolvedImports, setResolvedCache, getResolvedCache, dir, data } = | ||
pkg | ||
|
||
const cache = getResolvedCache(id, targetWeb) | ||
if (cache) { | ||
return cache | ||
|
@@ -1126,7 +1178,7 @@ function resolveDeepImport( | |
// resolve without postfix (see #7098) | ||
const { file, postfix } = splitFileAndPostfix(relativeId) | ||
const exportsId = resolveExportsOrImports( | ||
data, | ||
pkg, | ||
file, | ||
options, | ||
targetWeb, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think Vite should be hardcoding these directories. The users should be free to arrange the output however they want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I originally started adding this behind a config setting, similar to file extensions, but pushed it off to get the POC working.