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): multiple entries for umd/iife #10609

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion docs/config/build-options.md
Expand Up @@ -148,7 +148,7 @@ Options to pass on to [@rollup/plugin-dynamic-import-vars](https://github.com/ro
- **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']`, or `['es', 'cjs']`, if multiple entries are used. `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 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'` and entry is not defined as object (in which case the object keys define names for each entry). Default `formats` are `['es', 'umd']`, or `['es', 'cjs']`, if multiple entries are used. `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
263 changes: 204 additions & 59 deletions packages/vite/src/node/__tests__/build.spec.ts
Expand Up @@ -5,7 +5,12 @@ import type { Logger } from 'vite'
import { describe, expect, test, vi } from 'vitest'
import type { OutputOptions } from 'rollup'
import type { LibraryFormats, LibraryOptions } from '../build'
import { resolveBuildOutputs, resolveLibFilename } from '../build'
import {
resolveBuildOutputs,
resolveBuilds,
resolveLibFilename,
resolveLibName
} from '../build'
import { createLogger } from '../logger'

const __dirname = resolve(fileURLToPath(import.meta.url), '..')
Expand All @@ -21,7 +26,12 @@ describe('resolveBuildOutputs', () => {
const logger = createLogger()
const libOptions: LibraryOptions = { ...baseLibOptions }
const outputs: OutputOptions[] = [{ format: 'es' }]
const resolvedOutputs = resolveBuildOutputs(outputs, libOptions, logger)
const resolvedOutputs = resolveBuildOutputs(
libOptions.entry,
outputs,
libOptions,
logger
)

expect(resolvedOutputs).toEqual([
{
Expand All @@ -33,7 +43,12 @@ describe('resolveBuildOutputs', () => {
test('resolves outputs from lib options', () => {
const logger = createLogger()
const libOptions: LibraryOptions = { ...baseLibOptions, name: 'lib' }
const resolvedOutputs = resolveBuildOutputs(void 0, libOptions, logger)
const resolvedOutputs = resolveBuildOutputs(
libOptions.entry,
void 0,
libOptions,
logger
)

expect(resolvedOutputs).toEqual([
{
Expand All @@ -48,7 +63,7 @@ describe('resolveBuildOutputs', () => {
test('does not change outputs when lib options are missing', () => {
const logger = createLogger()
const outputs: OutputOptions[] = [{ format: 'es' }]
const resolvedOutputs = resolveBuildOutputs(outputs, false, logger)
const resolvedOutputs = resolveBuildOutputs('', outputs, false, logger)

expect(resolvedOutputs).toEqual(outputs)
})
Expand All @@ -62,7 +77,7 @@ describe('resolveBuildOutputs', () => {
}
const outputs: OutputOptions[] = [{ format: 'es' }]

resolveBuildOutputs(outputs, libOptions, logger)
resolveBuildOutputs(libOptions.entry, outputs, libOptions, logger)

expect(loggerSpy).toHaveBeenCalledWith(
expect.stringContaining('"build.lib.formats" will be ignored because')
Expand All @@ -75,38 +90,50 @@ describe('resolveBuildOutputs', () => {
...baseLibOptions,
formats: ['iife']
}
const resolveBuild = () => resolveBuildOutputs(void 0, libOptions, logger)
const resolveBuild = () =>
resolveBuildOutputs(libOptions.entry, void 0, libOptions, logger)

expect(resolveBuild).toThrowError(/Option "build\.lib\.name" is required/)
expect(resolveBuild).toThrowError(
`"build.lib.entry"/"build.rollupOptions.input" must be defined as object or option ` +
`"build.lib.name"/"build.rollupOptions.output.name" must be provided when output formats include "umd" or "iife".`
)
})

test('throws an error when lib.name is missing on umd format', () => {
const logger = createLogger()
const libOptions: LibraryOptions = { ...baseLibOptions, formats: ['umd'] }
const resolveBuild = () => resolveBuildOutputs(void 0, libOptions, logger)
const resolveBuild = () =>
resolveBuildOutputs(libOptions.entry, void 0, libOptions, logger)

expect(resolveBuild).toThrowError(/Option "build\.lib\.name" is required/)
expect(resolveBuild).toThrowError(
`"build.lib.entry"/"build.rollupOptions.input" must be defined as object or option ` +
`"build.lib.name"/"build.rollupOptions.output.name" must be provided when output formats include "umd" or "iife".`
)
})

test('throws an error when output.name is missing on iife format', () => {
const logger = createLogger()
const libOptions: LibraryOptions = { ...baseLibOptions }
const outputs: OutputOptions[] = [{ format: 'iife' }]
const resolveBuild = () => resolveBuildOutputs(outputs, libOptions, logger)
const resolveBuild = () =>
resolveBuildOutputs(libOptions.entry, outputs, libOptions, logger)

expect(resolveBuild).toThrowError(
/Entries in "build\.rollupOptions\.output" must specify "name"/
`"build.lib.entry"/"build.rollupOptions.input" must be defined as object or option ` +
`"build.lib.name"/"build.rollupOptions.output.name" must be provided when output formats include "umd" or "iife".`
)
})

test('throws an error when output.name is missing on umd format', () => {
const logger = createLogger()
const libOptions: LibraryOptions = { ...baseLibOptions }
const outputs: OutputOptions[] = [{ format: 'umd' }]
const resolveBuild = () => resolveBuildOutputs(outputs, libOptions, logger)
const resolveBuild = () =>
resolveBuildOutputs(libOptions.entry, outputs, libOptions, logger)

expect(resolveBuild).toThrowError(
/Entries in "build\.rollupOptions\.output" must specify "name"/
`"build.lib.entry"/"build.rollupOptions.input" must be defined as object or option ` +
`"build.lib.name"/"build.rollupOptions.output.name" must be provided when output formats include "umd" or "iife".`
)
})
})
Expand Down Expand Up @@ -351,18 +378,27 @@ describe('resolveBuildOutputs', () => {
name: 'entryA'
}

expect(resolveBuildOutputs(undefined, libOptions, {} as Logger)).toEqual([
{ format: 'es' },
{ format: 'umd' }
])
expect(
resolveBuildOutputs({ name: 'A' }, libOptions, {} as Logger)
resolveBuildOutputs(libOptions.entry, undefined, libOptions, {} as Logger)
).toEqual([{ format: 'es' }, { format: 'umd' }])
expect(
resolveBuildOutputs(
libOptions.entry,
{ name: 'A' },
libOptions,
{} as Logger
)
).toEqual([
{ format: 'es', name: 'A' },
{ format: 'umd', name: 'A' }
])
expect(
resolveBuildOutputs([{ name: 'A' }], libOptions, {} as Logger)
resolveBuildOutputs(
libOptions.entry,
[{ name: 'A' }],
libOptions,
{} as Logger
)
).toEqual([{ name: 'A' }])
})

Expand All @@ -371,60 +407,36 @@ describe('resolveBuildOutputs', () => {
entry: ['entryA.js', 'entryB.js']
}

expect(resolveBuildOutputs(undefined, libOptions, {} as Logger)).toEqual([
{ format: 'es' },
{ format: 'cjs' }
])
expect(
resolveBuildOutputs({ name: 'A' }, libOptions, {} as Logger)
resolveBuildOutputs(libOptions.entry, undefined, libOptions, {} as Logger)
).toEqual([{ format: 'es' }, { format: 'cjs' }])
expect(
resolveBuildOutputs(
libOptions.entry,
{ name: 'A' },
libOptions,
{} as Logger
)
).toEqual([
{ format: 'es', name: 'A' },
{ format: 'cjs', name: 'A' }
])
expect(
resolveBuildOutputs([{ name: 'A' }], libOptions, {} as Logger)
).toEqual([{ name: 'A' }])
})

test('umd or iife: should not support multiple entries', () => {
;['umd', 'iife'].forEach((format) => {
expect(() =>
resolveBuildOutputs(
undefined,
{
entry: ['entryA.js', 'entryB.js'],
formats: [format as LibraryFormats]
},
{} as Logger
)
).toThrow(
`Multiple entry points are not supported when output formats include "umd" or "iife".`
)
})
})

test('umd or iife: should define build.lib.name', () => {
;['umd', 'iife'].forEach((format) => {
expect(() =>
resolveBuildOutputs(
undefined,
{
entry: 'entryA.js',
formats: [format as LibraryFormats]
},
{} as Logger
)
).toThrow(
`Option "build.lib.name" is required when output formats include "umd" or "iife".`
resolveBuildOutputs(
libOptions.entry,
[{ name: 'A' }],
libOptions,
{} as Logger
)
})
).toEqual([{ name: 'A' }])
})

test('array outputs: should ignore build.lib.formats', () => {
// @ts-expect-error mock Logger
const log = { warn: vi.fn() } as Logger
expect(
resolveBuildOutputs(
'entryA.js',
[{ name: 'A' }],
{
entry: 'entryA.js',
Expand All @@ -439,4 +451,137 @@ describe('resolveBuildOutputs', () => {
)
)
})

test('error on missing names for multiple entries', () => {
const libOptions: LibraryOptions = {
entry: ['entryA.js', 'entryB.js'],
formats: ['umd']
}

expect(() =>
resolveBuildOutputs(libOptions.entry, undefined, libOptions, {} as Logger)
).toThrow(
`"build.lib.entry"/"build.rollupOptions.input" must be defined as object when there are multiple ` +
`inputs and output formats include "umd" or "iife".`
)
})
})

describe('resolveBuilds', () => {
test('one entry, one build', () => {
const libOptions: LibraryOptions = {
entry: 'entryA.js',
name: 'entryA',
formats: ['es', 'cjs', 'umd', 'iife']
}

const builds = resolveBuilds(
libOptions.entry,
undefined,
libOptions,
(_input) => (output) => output,
{} as Logger
)

expect(builds).toEqual([
{
input: 'entryA.js',
output: [
{ format: 'es' },
{ format: 'cjs' },
{ format: 'umd' },
{ format: 'iife' }
]
}
])
})

test('multiple entries, one build', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
formats: ['es', 'cjs']
}

const builds = resolveBuilds(
libOptions.entry,
undefined,
libOptions,
(_input) => (output) => output,
{} as Logger
)

expect(builds).toEqual([
{
input: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
output: [{ format: 'es' }, { format: 'cjs' }]
}
])
})

test('multiple builds', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
formats: ['es', 'cjs', 'umd', 'iife']
}

const builds = resolveBuilds(
libOptions.entry,
undefined,
libOptions,
(_input) => (output) => output,
{} as Logger
)

expect(builds).toEqual([
{
input: {
entryA: 'entryA.js',
entryB: 'entryB.js'
},
output: [{ format: 'es' }, { format: 'cjs' }]
},
{
input: { entryA: 'entryA.js' },
output: [{ format: 'umd' }, { format: 'iife' }],
label: 'Extra non code splitting build for entry: entryA'
},
{
input: { entryB: 'entryB.js' },
output: [{ format: 'umd' }, { format: 'iife' }],
label: 'Extra non code splitting build for entry: entryB'
}
])
})
})

describe('resolveLibName', () => {
test('returns name if provided', () => {
const libOptions: LibraryOptions = {
entry: '',
name: 'lib'
}

const name = resolveLibName(libOptions, '')
expect(name).toBe('lib')
})

test('returns name from entries object', () => {
const libOptions: LibraryOptions = {
entry: {
entryA: 'entryA.js'
}
}

const name = resolveLibName(libOptions, libOptions.entry)
expect(name).toBe('entryA')
})
})