From 2765862764ecf3b9ce1bf57070cf1598264c27f0 Mon Sep 17 00:00:00 2001 From: Marco Schumacher Date: Tue, 16 Aug 2022 23:26:23 +0200 Subject: [PATCH] feat(lib): allow multiple entries --- docs/config/build-options.md | 4 +- docs/guide/build.md | 23 ++++ .../vite/src/node/__tests__/build.spec.ts | 130 ++++++++++++++++++ packages/vite/src/node/build.ts | 52 +++++-- 4 files changed, 193 insertions(+), 16 deletions(-) diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 807b0f37640f4b..c72dda685b1470 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -109,10 +109,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 diff --git a/docs/guide/build.md b/docs/guide/build.md index 30dadbbb2473df..41376b46e5f2f8 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -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 @@ -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`. ::: diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index a692041b0ba04c..e12187b1ce6dfc 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -20,6 +20,7 @@ describe('resolveLibFilename', () => { entry: 'mylib.js' }, 'es', + 'myLib', resolve(__dirname, 'packages/name') ) @@ -33,6 +34,7 @@ describe('resolveLibFilename', () => { entry: 'mylib.js' }, 'es', + 'myLib', resolve(__dirname, 'packages/name') ) @@ -45,6 +47,7 @@ describe('resolveLibFilename', () => { entry: 'mylib.js' }, 'es', + 'myLib', resolve(__dirname, 'packages/name') ) @@ -58,6 +61,7 @@ describe('resolveLibFilename', () => { entry: 'mylib.js' }, 'es', + 'myLib', resolve(__dirname, 'packages/noname') ) @@ -71,6 +75,7 @@ describe('resolveLibFilename', () => { entry: 'mylib.js' }, 'es', + 'myLib', resolve(__dirname, 'packages/noname') ) }).toThrow() @@ -88,6 +93,7 @@ describe('resolveLibFilename', () => { const filename = resolveLibFilename( baseLibOptions, format, + 'myLib', resolve(__dirname, 'packages/noname') ) @@ -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') + }) }) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 174e5a33d9169a..22a2e1d5054570 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -3,6 +3,7 @@ import path from 'node:path' import colors from 'picocolors' import type { ExternalOption, + InputOption, InternalModuleFormat, ModuleFormat, OutputOptions, @@ -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` @@ -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' @@ -372,7 +373,16 @@ async function doBuild( const resolve = (p: string) => path.resolve(config.root, p) const input = libOptions - ? resolve(libOptions.entry) + ? 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') @@ -469,7 +479,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}` @@ -618,15 +629,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( @@ -649,14 +665,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 }))