Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Nov 1, 2024
1 parent 51e3d13 commit e96e888
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 1 deletion.
15 changes: 14 additions & 1 deletion packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
} from './baseEnvironment'
import type { MinimalPluginContext, Plugin, PluginContext } from './plugin'
import type { RollupPluginHooks } from './typeUtils'
import { licensePlugin } from './plugins/license'

export interface BuildEnvironmentOptions {
/**
Expand Down Expand Up @@ -199,6 +200,12 @@ export interface BuildEnvironmentOptions {
* @default true
*/
copyPublicDir?: boolean
/**
* Whether to emit a .vite/license.md that includes all bundled dependencies'
* licenses. Specify a path that ends with `.json` to generate a raw JSON.entry.
* @default false
*/
license?: boolean | string
/**
* Whether to emit a .vite/manifest.json under assets dir to map hash-less filenames
* to their hashed versions. Useful when you want to generate your own HTML
Expand Down Expand Up @@ -383,6 +390,7 @@ export function resolveBuildEnvironmentOptions(
write: true,
emptyOutDir: null,
copyPublicDir: true,
license: false,
manifest: false,
lib: false,
ssr: consumer === 'server',
Expand Down Expand Up @@ -501,7 +509,12 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{
...(config.esbuild !== false ? [buildEsbuildPlugin(config)] : []),
terserPlugin(config),
...(!config.isWorker
? [manifestPlugin(), ssrManifestPlugin(), buildReporterPlugin(config)]
? [
licensePlugin(),
manifestPlugin(),
ssrManifestPlugin(),
buildReporterPlugin(config),
]
: []),
buildLoadFallbackPlugin(),
],
Expand Down
150 changes: 150 additions & 0 deletions packages/vite/src/node/plugins/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import fs from 'node:fs'
import path from 'node:path'
import type { Plugin } from '../plugin'
import { isInNodeModules, sortObjectKeys } from '../utils'
import type { PackageCache } from '../packages'
import { findNearestMainPackageData } from '../packages'

export interface LicenseEntry {
/**
* Package name
*/
name: string
/**
* Package version
*/
version: string
/**
* SPDX license identifier (from package.json "license" field)
*/
identifier?: string
/**
* License file text
*/
text?: string
}

// https://github.com/npm/npm-packlist/blob/53b2a4f42b7fef0f63e8f26a3ea4692e23a58fed/lib/index.js#L284-L286
const licenseFiles = [/^license/i, /^licence/i, /^copying/i]

export interface ManifestChunk {
src?: string
file: string
css?: string[]
assets?: string[]
isEntry?: boolean
name?: string
isDynamicEntry?: boolean
imports?: string[]
dynamicImports?: string[]
}

export function licensePlugin(): Plugin {
return {
name: 'vite:manifest',

async generateBundle(_, bundle) {
const licenseOption = this.environment.config.build.license
if (licenseOption === false) return

const packageCache: PackageCache = new Map()
// Track license via a key to its license entry.
// A key consists of "name@version" of a package.
const licenses: Record<string, LicenseEntry> = {}

for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'asset') continue

for (const moduleId of chunk.moduleIds) {
if (moduleId.startsWith('\0') || !isInNodeModules(moduleId)) continue

// Find the dependency package.json
const pkgData = findNearestMainPackageData(
path.dirname(moduleId),
packageCache,
)
if (!pkgData) continue

// Grab the package.json keys and check if already exists in the licenses
const { name, version = '0.0.0', license } = pkgData.data
const key = `${name}@${version}`
if (licenses[key]) continue

// If not, create a new license entry
const entry: LicenseEntry = { name, version }
if (license) {
entry.identifier = license.trim()
}
const licenseFile = findLicenseFile(pkgData.dir)
if (licenseFile) {
entry.text = fs.readFileSync(licenseFile, 'utf-8').trim()
}
licenses[key] = entry
}
}

const licenseEntries = Object.values(sortObjectKeys(licenses))

// Emit as a JSON file
if (
typeof licenseOption === 'string' &&
licenseOption.endsWith('.json')
) {
this.emitFile({
fileName: licenseOption,
type: 'asset',
source: JSON.stringify(licenseEntries, null, 2),
})
return
}

// Emit a license file as markdown
const markdown = licenseEntryToMarkdown(licenseEntries)
this.emitFile({
fileName:
typeof licenseOption === 'string'
? licenseOption
: '.vite/license.md',
type: 'asset',
source: markdown,
})
},
}
}

function licenseEntryToMarkdown(licenses: LicenseEntry[]) {
if (licenses.length === 0) {
return `\
# Licenses
The app does not bundle any dependencies with licenses.
`
}

let text = `\
# Licenses
The app bundles dependencies which contains the following licenses:
`
for (const license of licenses) {
const nameAndVersionText = `${license.name} - ${license.version}`
const identifierText = license.identifier ? ` (${license.identifier})` : ''

text += `\n## ${nameAndVersionText}${identifierText}\n`
if (license.text) {
text += `\n${license.text}\n`
}
}
return text
}

function findLicenseFile(pkgDir: string) {
const files = fs.readdirSync(pkgDir)
const matchedFile = files.find((file) =>
licenseFiles.some((re) => re.test(file)),
)
if (matchedFile) {
return path.join(pkgDir, matchedFile)
}
}
1 change: 1 addition & 0 deletions playground/optimize-deps/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default defineConfig({
build: {
// to make tests faster
minify: false,
license: true,
rollupOptions: {
onwarn(msg, warn) {
// filter `"Buffer" is not exported by "__vite-browser-external"` warning
Expand Down

0 comments on commit e96e888

Please sign in to comment.