Skip to content

Commit

Permalink
feat(lib): allow multiple entries (#7047)
Browse files Browse the repository at this point in the history
  • Loading branch information
schummar committed Sep 30, 2022
1 parent ee3231c commit 65a0fad
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 16 deletions.
4 changes: 2 additions & 2 deletions docs/config/build-options.md
Expand Up @@ -145,10 +145,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 @@ -214,7 +215,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 @@ -230,7 +231,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 @@ -439,7 +440,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 @@ -536,7 +547,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 @@ -705,15 +717,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 @@ -736,14 +753,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

0 comments on commit 65a0fad

Please sign in to comment.