Skip to content
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(lib): allow multiple entries #7047

Merged
merged 1 commit into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/config/build-options.md
Expand Up @@ -111,10 +111,10 @@ Options to pass on to [@rollup/plugin-dynamic-import-vars](https://github.com/ro

## build.lib

- **Type:** `{ entry: string, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat) => string) }`
- **Type:** `{ entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string) }`
- **Related:** [Library Mode](/guide/build#library-mode)

Build as a library. `entry` is required since the library cannot use HTML as entry. `name` is the exposed global variable and is required when `formats` includes `'umd'` or `'iife'`. Default `formats` are `['es', 'umd']`. `fileName` is the name of the package file output, default `fileName` is the name option of package.json, it can also be defined as function taking the `format` as an argument.
Build as a library. `entry` is required since the library cannot use HTML as entry. `name` is the exposed global variable and is required when `formats` includes `'umd'` or `'iife'`. Default `formats` are `['es', 'umd']`. `fileName` is the name of the package file output, default `fileName` is the name option of package.json, it can also be defined as function taking the `format` and `entryAlias` as arguments.

## build.manifest

Expand Down
23 changes: 23 additions & 0 deletions docs/guide/build.md
Expand Up @@ -128,6 +128,7 @@ import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, 'lib/main.js'),
name: 'MyLib',
// the proper extensions will be added
Expand Down Expand Up @@ -185,6 +186,28 @@ Recommended `package.json` for your lib:
}
```

Or, if exposing multiple entry points:

```json
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.cjs",
"module": "./dist/my-lib.mjs",
"exports": {
".": {
"import": "./dist/my-lib.mjs",
"require": "./dist/my-lib.cjs"
},
"./secondary": {
"import": "./dist/secondary.mjs",
"require": "./dist/secondary.cjs"
}
}
}
```

::: tip Note
If the `package.json` does not contain `"type": "module"`, Vite will generate different file extensions for Node.js compatibility. `.js` will become `.mjs` and `.cjs` will become `.js`.
:::
Expand Down
130 changes: 130 additions & 0 deletions packages/vite/src/node/__tests__/build.spec.ts
Expand Up @@ -20,6 +20,7 @@ describe('resolveLibFilename', () => {
entry: 'mylib.js'
},
'es',
'myLib',
resolve(__dirname, 'packages/name')
)

Expand All @@ -33,6 +34,7 @@ describe('resolveLibFilename', () => {
entry: 'mylib.js'
},
'es',
'myLib',
resolve(__dirname, 'packages/name')
)

Expand All @@ -45,6 +47,7 @@ describe('resolveLibFilename', () => {
entry: 'mylib.js'
},
'es',
'myLib',
resolve(__dirname, 'packages/name')
)

Expand All @@ -58,6 +61,7 @@ describe('resolveLibFilename', () => {
entry: 'mylib.js'
},
'es',
'myLib',
resolve(__dirname, 'packages/noname')
)

Expand All @@ -71,6 +75,7 @@ describe('resolveLibFilename', () => {
entry: 'mylib.js'
},
'es',
'myLib',
resolve(__dirname, 'packages/noname')
)
}).toThrow()
Expand All @@ -88,6 +93,7 @@ describe('resolveLibFilename', () => {
const filename = resolveLibFilename(
baseLibOptions,
format,
'myLib',
resolve(__dirname, 'packages/noname')
)

Expand All @@ -107,10 +113,134 @@ describe('resolveLibFilename', () => {
const filename = resolveLibFilename(
baseLibOptions,
format,
'myLib',
resolve(__dirname, 'packages/module')
)

expect(expectedFilename).toBe(filename)
}
})

test('multiple entries with aliases', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js',
entryB: 'entryB.js'
}
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('entryA.mjs')
expect(fileName2).toBe('entryB.mjs')
})

test('multiple entries with aliases: custom filename function', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
fileName: (format, entryAlias) =>
`custom-filename-function.${entryAlias}.${format}.js`
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('custom-filename-function.entryA.es.js')
expect(fileName2).toBe('custom-filename-function.entryB.es.js')
})

test('multiple entries with aliases: custom filename string', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
fileName: 'custom-filename'
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('custom-filename.mjs')
expect(fileName2).toBe('custom-filename.mjs')
})

test('multiple entries as array', () => {
const libOptions: LibraryOptions = {
entry: ['entryA.js', 'entryB.js']
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('entryA.mjs')
expect(fileName2).toBe('entryB.mjs')
})

test('multiple entries as array: custom filename function', () => {
const libOptions: LibraryOptions = {
entry: ['entryA.js', 'entryB.js'],
fileName: (format, entryAlias) =>
`custom-filename-function.${entryAlias}.${format}.js`
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('custom-filename-function.entryA.es.js')
expect(fileName2).toBe('custom-filename-function.entryB.es.js')
})

test('multiple entries as array: custom filename string', () => {
const libOptions: LibraryOptions = {
entry: ['entryA.js', 'entryB.js'],
fileName: 'custom-filename'
}

const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) =>
resolveLibFilename(
libOptions,
'es',
entryAlias,
resolve(__dirname, 'packages/name')
)
)

expect(fileName1).toBe('custom-filename.mjs')
expect(fileName2).toBe('custom-filename.mjs')
})
})
53 changes: 39 additions & 14 deletions packages/vite/src/node/build.ts
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path'
import colors from 'picocolors'
import type {
ExternalOption,
InputOption,
InternalModuleFormat,
ModuleFormat,
OutputOptions,
Expand Down Expand Up @@ -208,7 +209,7 @@ export interface LibraryOptions {
/**
* Path of library entry
*/
entry: string
entry: InputOption
/**
* The name of the exposed global variable. Required when the `formats` option includes
* `umd` or `iife`
Expand All @@ -224,7 +225,7 @@ export interface LibraryOptions {
* of the project package.json. It can also be defined as a function taking the
* format as an argument.
*/
fileName?: string | ((format: ModuleFormat) => string)
fileName?: string | ((format: ModuleFormat, entryName: string) => string)
}

export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife'
Expand Down Expand Up @@ -373,7 +374,17 @@ async function doBuild(

const resolve = (p: string) => path.resolve(config.root, p)
const input = libOptions
? options.rollupOptions?.input || resolve(libOptions.entry)
? options.rollupOptions?.input ||
(typeof libOptions.entry === 'string'
? resolve(libOptions.entry)
: Array.isArray(libOptions.entry)
? libOptions.entry.map(resolve)
: Object.fromEntries(
Object.entries(libOptions.entry).map(([alias, file]) => [
alias,
resolve(file)
])
))
: typeof options.ssr === 'string'
? resolve(options.ssr)
: options.rollupOptions?.input || resolve('index.html')
Expand Down Expand Up @@ -470,7 +481,8 @@ async function doBuild(
entryFileNames: ssr
? `[name].${jsExt}`
: libOptions
? resolveLibFilename(libOptions, format, config.root, jsExt)
? ({ name }) =>
resolveLibFilename(libOptions, format, name, config.root, jsExt)
: path.posix.join(options.assetsDir, `[name].[hash].${jsExt}`),
chunkFileNames: libOptions
? `[name].[hash].${jsExt}`
Expand Down Expand Up @@ -620,15 +632,20 @@ function resolveOutputJsExtension(
export function resolveLibFilename(
libOptions: LibraryOptions,
format: ModuleFormat,
entryName: string,
root: string,
extension?: JsExt
): string {
if (typeof libOptions.fileName === 'function') {
return libOptions.fileName(format)
return libOptions.fileName(format, entryName)
}

const packageJson = getPkgJson(root)
const name = libOptions.fileName || getPkgName(packageJson.name)
const name =
libOptions.fileName ||
(typeof libOptions.entry === 'string'
? getPkgName(packageJson.name)
: entryName)

if (!name)
throw new Error(
Expand All @@ -651,14 +668,22 @@ function resolveBuildOutputs(
): OutputOptions | OutputOptions[] | undefined {
if (libOptions) {
const formats = libOptions.formats || ['es', 'umd']
if (
(formats.includes('umd') || formats.includes('iife')) &&
!libOptions.name
) {
throw new Error(
`Option "build.lib.name" is required when output formats ` +
`include "umd" or "iife".`
)
if (formats.includes('umd') || formats.includes('iife')) {
if (
typeof libOptions.entry !== 'string' &&
Object.values(libOptions.entry).length > 1
) {
throw new Error(
`Multiple entry points are not supported when output formats include "umd" or "iife".`
)
}

if (!libOptions.name) {
throw new Error(
`Option "build.lib.name" is required when output formats ` +
`include "umd" or "iife".`
)
}
}
if (!outputs) {
return formats.map((format) => ({ format }))
Expand Down