diff --git a/src/auto.ts b/src/auto.ts index d6f5d251..5d3697f9 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -2,10 +2,10 @@ import { normalize, join } from 'pathe' import consola from 'consola' import chalk from 'chalk' import type { PackageJson } from 'pkg-types' -import { extractExportFilenames, listRecursively } from './utils' +import { extractExportFilenames, listRecursively, warn } from './utils' import { BuildEntry, definePreset, MkdistBuildEntry } from './types' -type InferEntriesResult = { entries: BuildEntry[], cjs?: boolean, dts?: boolean } +type InferEntriesResult = { entries: BuildEntry[], cjs?: boolean, dts?: boolean, warnings: string[] } export const autoPreset = definePreset(() => { return { @@ -17,6 +17,9 @@ export const autoPreset = definePreset(() => { } const sourceFiles = listRecursively(join(ctx.options.rootDir, 'src')) const res = inferEntries(ctx.pkg, sourceFiles) + for (const message of res.warnings) { + warn(ctx, message) + } ctx.options.entries.push(...res.entries) if (res.cjs) { ctx.options.rollup.emitCJS = true @@ -41,6 +44,8 @@ export const autoPreset = definePreset(() => { * - if an array of source files, these will be used directly instead of accessing fs. */ export function inferEntries (pkg: PackageJson, sourceFiles: string[]): InferEntriesResult { + const warnings = [] + // Come up with a list of all output files & their formats const outputs = extractExportFilenames(pkg.exports) @@ -93,7 +98,7 @@ export function inferEntries (pkg: PackageJson, sourceFiles: string[]): InferEnt }, undefined) if (!input) { - consola.warn(`could not infer entrypoint for \`${output.file}\``) + warnings.push(`Could not find entrypoint for ${output.file}`) continue } @@ -113,7 +118,7 @@ export function inferEntries (pkg: PackageJson, sourceFiles: string[]): InferEnt } } - return { entries, cjs, dts } + return { entries, cjs, dts, warnings } } export const getEntrypointPaths = (path: string) => { diff --git a/src/build.ts b/src/build.ts index 0974dd4d..2c8ad3b0 100644 --- a/src/build.ts +++ b/src/build.ts @@ -42,6 +42,7 @@ export async function build (rootDir: string, stub: boolean, inputConfig: BuildC peerDependencies: [], alias: {}, replace: {}, + failOnWarn: true, rollup: { emitCJS: false, cjsBridge: false, @@ -75,6 +76,7 @@ export async function build (rootDir: string, stub: boolean, inputConfig: BuildC // Build context const ctx: BuildContext = { options, + warnings: new Set(), pkg, buildEntries: [], usedImports: new Set(), @@ -182,10 +184,18 @@ export async function build (rootDir: string, stub: boolean, inputConfig: BuildC // Validate validateDependencies(ctx) - validatePackage(pkg, rootDir) + validatePackage(pkg, rootDir, ctx) // Call build:done await ctx.hooks.callHook('build:done', ctx) consola.log('') + + if (ctx.warnings.size) { + consola.warn('Build is done with some warnings:\n\n' + Array.from(ctx.warnings).map(msg => '- ' + msg).join('\n')) + if (ctx.options.failOnWarn) { + consola.error('Exiting with code (1). You can change this behavior by setting `failOnWarn: false` .') + process.exit(1) + } + } } diff --git a/src/builder/rollup.ts b/src/builder/rollup.ts index 7cb91d71..a0d2bb26 100644 --- a/src/builder/rollup.ts +++ b/src/builder/rollup.ts @@ -10,9 +10,8 @@ import _esbuild from 'rollup-plugin-esbuild' import dts from 'rollup-plugin-dts' import replace from '@rollup/plugin-replace' import { relative, resolve, dirname } from 'pathe' -import consola from 'consola' import { resolvePath } from 'mlly' -import { getpkg, tryResolve } from '../utils' +import { getpkg, tryResolve, warn } from '../utils' import type { BuildContext } from '../types' import { JSONPlugin } from './plugins/json' import { rawPlugin } from './plugins/raw' @@ -140,7 +139,7 @@ export function getRollupOptions (ctx: BuildContext): RollupOptions { return false } if (!isExplicitExternal) { - consola.warn(`Inlining implicit external ${id}`) + warn(ctx, `Inlined implicit external ${id}`) } return isExplicitExternal }, diff --git a/src/types.ts b/src/types.ts index fcb43e5d..b6a81216 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,6 +66,7 @@ export interface BuildOptions { devDependencies: string[] alias: { [find: string]: string }, replace: { [find: string]: string }, + failOnWarn?: boolean rollup: RollupBuildOptions } @@ -74,6 +75,7 @@ export interface BuildContext { pkg: PackageJson, buildEntries: { path: string, bytes?: number, exports?: string[], chunks?: string[] }[] usedImports: Set + warnings: Set hooks: Hookable } diff --git a/src/utils.ts b/src/utils.ts index 35e856e8..05d6c21d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,14 +5,21 @@ import { dirname, resolve } from 'pathe' import mkdirp from 'mkdirp' import _rimraf from 'rimraf' import jiti from 'jiti' +import consola from 'consola' import type { PackageJson } from 'pkg-types' import { autoPreset } from './auto' -import type { BuildPreset, BuildConfig } from './types' +import type { BuildPreset, BuildConfig, BuildContext } from './types' export async function ensuredir (path: string) { await mkdirp(dirname(path)) } +export function warn (ctx: BuildContext, message: string) { + if (ctx.warnings.has(message)) { return } + consola.debug('[unbuild] [warn]', message) + ctx.warnings.add(message) +} + export async function symlink (from: string, to: string, force: boolean = true) { await ensuredir(to) if (force) { diff --git a/src/validate.ts b/src/validate.ts index d64982db..3e564aa6 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -1,15 +1,14 @@ import { existsSync } from 'fs' import chalk from 'chalk' -import consola from 'consola' import { resolve } from 'pathe' import { PackageJson } from 'pkg-types' -import { extractExportFilenames, getpkg } from './utils' +import { extractExportFilenames, getpkg, warn } from './utils' import { BuildContext } from './types' export function validateDependencies (ctx: BuildContext) { const usedDependencies = new Set() const unusedDependencies = new Set(Object.keys(ctx.pkg.dependencies || {})) - const implicitDependnecies = new Set() + const implicitDependencies = new Set() for (const id of ctx.usedImports) { unusedDependencies.delete(id) usedDependencies.add(id) @@ -26,18 +25,18 @@ export function validateDependencies (ctx: BuildContext) { !ctx.options.dependencies.includes(getpkg(id)) && !ctx.options.peerDependencies.includes(getpkg(id)) ) { - implicitDependnecies.add(id) + implicitDependencies.add(id) } } if (unusedDependencies.size) { - consola.warn('Potential unused dependencies found:', Array.from(unusedDependencies).map(id => chalk.cyan(id)).join(', ')) + warn(ctx, 'Potential unused dependencies found: ' + Array.from(unusedDependencies).map(id => chalk.cyan(id)).join(', ')) } - if (implicitDependnecies.size && !ctx.options.rollup.inlineDependencies) { - consola.warn('Potential implicit dependencies found:', Array.from(implicitDependnecies).map(id => chalk.cyan(id)).join(', ')) + if (implicitDependencies.size && !ctx.options.rollup.inlineDependencies) { + warn(ctx, 'Potential implicit dependencies found: ' + Array.from(implicitDependencies).map(id => chalk.cyan(id)).join(', ')) } } -export function validatePackage (pkg: PackageJson, rootDir: string) { +export function validatePackage (pkg: PackageJson, rootDir: string, ctx: BuildContext) { if (!pkg) { return } const filenames = new Set([ @@ -57,6 +56,6 @@ export function validatePackage (pkg: PackageJson, rootDir: string) { } } if (missingOutputs.length) { - consola.warn(`Potential missing package.json files: ${missingOutputs.map(o => chalk.cyan(o)).join(', ')}`) + warn(ctx, `Potential missing package.json files: ${missingOutputs.map(o => chalk.cyan(o)).join(', ')}`) } } diff --git a/test/auto.test.ts b/test/auto.test.ts index c6aaad02..a0fd99dd 100644 --- a/test/auto.test.ts +++ b/test/auto.test.ts @@ -7,7 +7,8 @@ describe('inferEntries', () => { expect(result).to.deep.equal({ cjs: true, dts: false, - entries: [{ input: 'src/test' }] + entries: [{ input: 'src/test' }], + warnings: [] }) }) @@ -15,17 +16,20 @@ describe('inferEntries', () => { expect(inferEntries({ bin: 'dist/cli.cjs' }, ['src/', 'src/cli.ts'])).to.deep.equal({ cjs: true, dts: false, - entries: [{ input: 'src/cli' }] + entries: [{ input: 'src/cli' }], + warnings: [] }) expect(inferEntries({ bin: { nuxt: 'dist/cli.js' } }, ['src/', 'src/cli.ts'])).to.deep.equal({ cjs: true, dts: false, - entries: [{ input: 'src/cli' }] + entries: [{ input: 'src/cli' }], + warnings: [] }) expect(inferEntries({ bin: { nuxt: 'dist/cli.js' }, type: 'module' }, ['src/', 'src/cli.ts'])).to.deep.equal({ cjs: false, dts: false, - entries: [{ input: 'src/cli' }] + entries: [{ input: 'src/cli' }], + warnings: [] }) }) @@ -34,7 +38,8 @@ describe('inferEntries', () => { expect(result).to.deep.equal({ cjs: false, dts: false, - entries: [{ input: 'src/test' }] + entries: [{ input: 'src/test' }], + warnings: [] }) }) @@ -43,7 +48,8 @@ describe('inferEntries', () => { expect(result).to.deep.equal({ cjs: false, dts: false, - entries: [{ input: 'src/other/runtime/index' }] + entries: [{ input: 'src/other/runtime/index' }], + warnings: [] }) }) @@ -51,20 +57,22 @@ describe('inferEntries', () => { expect(inferEntries({ main: 'dist/test.cjs', types: 'custom/handwritten.d.ts' }, ['src/', 'src/test.ts'])).to.deep.equal({ cjs: true, dts: false, - entries: [{ input: 'src/test' } + entries: [{ input: 'src/test' }], + warnings: [ + 'Could not find entrypoint for custom/handwritten.d.ts' ] }) expect(inferEntries({ main: 'dist/test.cjs', module: 'dist/test.mjs', types: 'dist/test.d.ts' }, ['src/', 'src/test.ts'])).to.deep.equal({ cjs: true, dts: true, - entries: [{ input: 'src/test' } - ] + entries: [{ input: 'src/test' }], + warnings: [] }) expect(inferEntries({ main: 'dist/test.cjs', module: 'dist/test.mjs', typings: 'dist/test.d.ts' }, ['src/', 'src/test.ts'])).to.deep.equal({ cjs: true, dts: true, - entries: [{ input: 'src/test' } - ] + entries: [{ input: 'src/test' }], + warnings: [] }) }) @@ -73,7 +81,8 @@ describe('inferEntries', () => { expect(result).to.deep.equal({ cjs: true, dts: true, - entries: [{ input: 'src/test' }] + entries: [{ input: 'src/test' }], + warnings: [] }) }) @@ -81,7 +90,10 @@ describe('inferEntries', () => { expect(inferEntries({ exports: 'dist/test.js' }, ['src/', 'src/index.ts'])).to.deep.equal({ cjs: false, entries: [], - dts: false + dts: false, + warnings: [ + 'Could not find entrypoint for dist/test.js' + ] }) }) @@ -89,7 +101,8 @@ describe('inferEntries', () => { expect(inferEntries({ exports: { './*': './*' } }, ['src/', 'src/', 'src/index.ts'])).to.deep.equal({ cjs: false, entries: [], - dts: false + dts: false, + warnings: [] }) }) @@ -100,7 +113,8 @@ describe('inferEntries', () => { entries: [ { input: 'src/index' }, { input: 'src/test' } - ] + ], + warnings: [] }) }) @@ -108,17 +122,20 @@ describe('inferEntries', () => { expect(inferEntries({ exports: './dist/runtime/*' }, ['src/', 'src/runtime/', 'src/runtime/test.js'])).to.deep.equal({ cjs: false, dts: false, - entries: [{ format: 'esm', input: 'src/runtime/', outDir: './dist/runtime/' }] + entries: [{ format: 'esm', input: 'src/runtime/', outDir: './dist/runtime/' }], + warnings: [] }) expect(inferEntries({ exports: { './runtime/*': './dist/runtime/*.mjs,' } }, ['src/', 'src/runtime/'])).to.deep.equal({ cjs: false, dts: false, - entries: [{ format: 'esm', input: 'src/runtime/', outDir: './dist/runtime/' }] + entries: [{ format: 'esm', input: 'src/runtime/', outDir: './dist/runtime/' }], + warnings: [] }) expect(inferEntries({ exports: { './runtime/*': { require: './dist/runtime/*' } } }, ['src/', 'src/runtime/'])).to.deep.equal({ cjs: true, dts: false, - entries: [{ format: 'cjs', input: 'src/runtime/', outDir: './dist/runtime/' }] + entries: [{ format: 'cjs', input: 'src/runtime/', outDir: './dist/runtime/' }], + warnings: [] }) }) }) diff --git a/test/fixture/src/index.ts b/test/fixture/src/index.ts index 11c49278..78233def 100644 --- a/test/fixture/src/index.ts +++ b/test/fixture/src/index.ts @@ -13,3 +13,6 @@ import('os').then(os => console.log(os.arch())) import('./test.html').then(console.log) export const foo = 'bar' + +// Failing test +// export * from 'defu' diff --git a/test/validate.test.ts b/test/validate.test.ts index 7c48ebd9..c0ee7a9d 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -7,8 +7,9 @@ import { BuildEntry } from '../src/types' describe('validatePackage', () => { it('detects missing files', () => { - const logs: string[] = [] - consola.mock(type => type === 'warn' ? (str: string) => logs.push(str) : () => { }) + const buildContext = { + warnings: new Set() + } as any validatePackage({ main: './dist/test', @@ -20,23 +21,25 @@ describe('validatePackage', () => { './runtime/*': './runtime/*.mjs', '.': { node: './src/index.ts' } } - }, join(fileURLToPath(import.meta.url), '../fixture')) + }, join(fileURLToPath(import.meta.url), '../fixture'), buildContext) + + const warnings = Array.from(buildContext.warnings) - expect(logs[0]).to.include('Potential missing') - expect(logs[0]).not.to.include('src/index.ts') + expect(warnings[0]).to.include('Potential missing') + expect(warnings[0]).not.to.include('src/index.ts') for (const file of ['dist/test', 'dist/cli', 'dist/mod', 'runtime']) { - expect(logs[0]).to.include(file) + expect(warnings[0]).to.include(file) } }) }) describe('validateDependecies', () => { it('detects implicit deps', () => { - const logs: string[] = [] - consola.mock(type => type === 'warn' ? (str: string) => logs.push(str) : () => { }) + const warnings = new Set() validateDependencies({ + warnings, pkg: {}, buildEntries: [], hooks: [] as any, @@ -64,7 +67,7 @@ describe('validateDependecies', () => { } }) - expect(logs[0]).to.include('Potential implicit dependencies found:') + expect(Array.from(warnings)[0]).to.include('Potential implicit dependencies found:') }) it('does not print implicit deps warning for peerDependencies', () => {