Skip to content

Commit

Permalink
feat: validate build outputs against package.json (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe authored Dec 14, 2021
1 parent ba82fcb commit c9ce0b0
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 122 deletions.
61 changes: 2 additions & 59 deletions src/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { normalize, join } from 'pathe'
import consola from 'consola'
import chalk from 'chalk'
import type { PackageJson } from 'pkg-types'
import { listRecursively } from './utils'
import { extractExportFilenames, listRecursively } from './utils'
import { BuildEntry, definePreset, MkdistBuildEntry } from './types'

type OutputDescriptor = { file: string, type?: 'esm' | 'cjs' }
type InferEntriesResult = { entries: BuildEntry[], cjs?: boolean, dts?: boolean }

export const autoPreset = definePreset(() => {
Expand Down Expand Up @@ -43,7 +42,7 @@ export const autoPreset = definePreset(() => {
*/
export function inferEntries (pkg: PackageJson, sourceFiles: string[]): InferEntriesResult {
// Come up with a list of all output files & their formats
const outputs: OutputDescriptor[] = extractExportFilenames(pkg.exports)
const outputs = extractExportFilenames(pkg.exports)

if (pkg.bin) {
const binaries = typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin)
Expand Down Expand Up @@ -117,63 +116,7 @@ export function inferEntries (pkg: PackageJson, sourceFiles: string[]): InferEnt
return { entries, cjs, dts }
}

export function inferExportType (condition: string, previousConditions: string[] = [], filename = ''): 'esm' | 'cjs' {
if (filename) {
if (filename.endsWith('.d.ts')) {
return 'esm'
}
if (filename.endsWith('.mjs')) {
return 'esm'
}
if (filename.endsWith('.cjs')) {
return 'cjs'
}
}
switch (condition) {
case 'import':
return 'esm'
case 'require':
return 'cjs'
default: {
if (!previousConditions.length) {
// TODO: Check against type:module for default
return 'esm'
}
const [newCondition, ...rest] = previousConditions
return inferExportType(newCondition, rest, filename)
}
}
}

export function extractExportFilenames (exports: PackageJson['exports'], conditions: string[] = []): OutputDescriptor[] {
if (!exports) { return [] }
if (typeof exports === 'string') {
return [{ file: exports, type: 'esm' }]
}
return Object.entries(exports).flatMap(
([condition, exports]) => typeof exports === 'string'
? { file: exports, type: inferExportType(condition, conditions, exports) }
: extractExportFilenames(exports, [...conditions, condition])
)
}

export const getEntrypointPaths = (path: string) => {
const segments = normalize(path).split('/')
return segments.map((_, index) => segments.slice(index).join('/')).filter(Boolean)
}

export const getEntrypointFilenames = (path: string, supportedExtensions = ['.ts', '.mjs', '.cjs', '.js', '.json']) => {
if (path.startsWith('./')) { path = path.slice(2) }

const filenames = getEntrypointPaths(path).flatMap((path) => {
const basefile = path.replace(/\.\w+$/, '')
return [
basefile,
`${basefile}/index`
]
})

filenames.push('index')

return filenames.flatMap(name => supportedExtensions.map(ext => `${name}${ext}`))
}
3 changes: 2 additions & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import prettyBytes from 'pretty-bytes'
import mkdirp from 'mkdirp'
import { dumpObject, rmdir, tryRequire, resolvePreset } from './utils'
import type { BuildContext, BuildConfig, BuildOptions } from './types'
import { validateDependencies } from './validate'
import { validatePackage, validateDependencies } from './validate'
import { rollupBuild } from './builder/rollup'
import { typesBuild } from './builder/untyped'
import { mkdistBuild } from './builder/mkdist'
Expand Down Expand Up @@ -161,6 +161,7 @@ export async function build (rootDir: string, stub: boolean, inputConfig: BuildC

// Validate
validateDependencies(ctx)
validatePackage(pkg, rootDir)

// Call build:done
await ctx.hooks.callHook('build:done', ctx)
Expand Down
43 changes: 43 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { dirname, resolve } from 'pathe'
import mkdirp from 'mkdirp'
import _rimraf from 'rimraf'
import jiti from 'jiti'
import type { PackageJson } from 'pkg-types'
import { autoPreset } from './auto'
import type { BuildPreset, BuildConfig } from './types'

Expand Down Expand Up @@ -89,3 +90,45 @@ export function resolvePreset (preset: string | BuildPreset, rootDir: string): B
}
return preset as BuildConfig
}

export function inferExportType (condition: string, previousConditions: string[] = [], filename = ''): 'esm' | 'cjs' {
if (filename) {
if (filename.endsWith('.d.ts')) {
return 'esm'
}
if (filename.endsWith('.mjs')) {
return 'esm'
}
if (filename.endsWith('.cjs')) {
return 'cjs'
}
}
switch (condition) {
case 'import':
return 'esm'
case 'require':
return 'cjs'
default: {
if (!previousConditions.length) {
// TODO: Check against type:module for default
return 'esm'
}
const [newCondition, ...rest] = previousConditions
return inferExportType(newCondition, rest, filename)
}
}
}

export type OutputDescriptor = { file: string, type?: 'esm' | 'cjs' }

export function extractExportFilenames (exports: PackageJson['exports'], conditions: string[] = []): OutputDescriptor[] {
if (!exports) { return [] }
if (typeof exports === 'string') {
return [{ file: exports, type: 'esm' }]
}
return Object.entries(exports).flatMap(
([condition, exports]) => typeof exports === 'string'
? { file: exports, type: inferExportType(condition, conditions, exports) }
: extractExportFilenames(exports, [...conditions, condition])
)
}
29 changes: 28 additions & 1 deletion src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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 { BuildContext } from './types'
import { getpkg } from './utils'

export function validateDependencies (ctx: BuildContext) {
const usedDependencies = new Set<string>()
Expand Down Expand Up @@ -32,3 +35,27 @@ export function validateDependencies (ctx: BuildContext) {
consola.warn('Potential implicit dependencies found:', Array.from(implicitDependnecies).map(id => chalk.cyan(id)).join(', '))
}
}

export function validatePackage (pkg: PackageJson, rootDir: string) {
if (!pkg) { return }

const filenames = new Set([
...typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin || {}),
pkg.main,
pkg.module,
pkg.types,
pkg.typings,
...extractExportFilenames(pkg.exports).map(i => i.file)
].map(i => i && resolve(rootDir, i.replace(/\/[^/]*\*.*$/, ''))))

const missingOutputs = []

for (const filename of filenames) {
if (filename && !filename.includes('*') && !existsSync(filename)) {
missingOutputs.push(filename.replace(rootDir + '/', ''))
}
}
if (missingOutputs.length) {
consola.warn(`Potential missing package.json files: ${missingOutputs.map(o => chalk.cyan(o)).join(', ')}`)
}
}
62 changes: 1 addition & 61 deletions test/auto.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { expect } from 'chai'
import jiti from 'jiti'

const { inferEntries, inferExportType, extractExportFilenames, getEntrypointPaths, getEntrypointFilenames } =
jiti(import.meta.url)('../src/auto')
const { inferEntries, getEntrypointPaths } = jiti(import.meta.url)('../src/auto') as typeof import('../src/auto')

describe('inferEntries', () => {
it('recognises main and module outputs', () => {
Expand Down Expand Up @@ -126,32 +125,6 @@ describe('inferEntries', () => {
})
})

describe('inferExportType', () => {
it('infers export type by condition', () => {
expect(inferExportType('import')).to.equal('esm')
expect(inferExportType('require')).to.equal('cjs')
expect(inferExportType('node')).to.equal('esm')
expect(inferExportType('some_unknown_condition')).to.equal('esm')
})
it('infers export type based on previous conditions', () => {
expect(inferExportType('import', ['require'])).to.equal('esm')
expect(inferExportType('node', ['require'])).to.equal('cjs')
expect(inferExportType('node', ['import'])).to.equal('esm')
expect(inferExportType('node', ['unknown', 'require'])).to.equal('cjs')
})
})

describe('extractExportFilenames', () => {
it('handles strings', () => {
expect(extractExportFilenames('test')).to.deep.equal([{ file: 'test', type: 'esm' }])
})
it('handles nested objects', () => {
expect(extractExportFilenames({ require: 'test' })).to.deep.equal([{ file: 'test', type: 'cjs' }])
// @ts-ignore TODO: fix pkg-types
expect(extractExportFilenames({ require: { node: 'test', other: { import: 'this', require: 'that' } } })).to.deep.equal([{ file: 'test', type: 'cjs' }, { file: 'this', type: 'esm' }, { file: 'that', type: 'cjs' }])
})
})

describe('getEntrypointPaths', () => {
it('produces a list of possible paths', () => {
expect(getEntrypointPaths('./dist/foo/bar.js')).to.deep.equal([
Expand All @@ -165,36 +138,3 @@ describe('getEntrypointPaths', () => {
])
})
})

describe('getEntrypointFilenames', () => {
it('produces a list of possible source files', () => {
expect(getEntrypointFilenames('./dist/foo/bar.js', ['.ts'])).to.deep.equal([
'dist/foo/bar.ts',
'dist/foo/bar/index.ts',
'foo/bar.ts',
'foo/bar/index.ts',
'bar.ts',
'bar/index.ts',
'index.ts'
])
})
it('uses default filenames', () => {
expect(getEntrypointFilenames('bar.js')).to.deep.equal([
'bar.ts',
'bar.mjs',
'bar.cjs',
'bar.js',
'bar.json',
'bar/index.ts',
'bar/index.mjs',
'bar/index.cjs',
'bar/index.js',
'bar/index.json',
'index.ts',
'index.mjs',
'index.cjs',
'index.js',
'index.json'
])
})
})
30 changes: 30 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect } from 'chai'
import jiti from 'jiti'

const { extractExportFilenames, inferExportType } = jiti(import.meta.url)('../src/utils') as typeof import('../src/utils')

describe('inferExportType', () => {
it('infers export type by condition', () => {
expect(inferExportType('import')).to.equal('esm')
expect(inferExportType('require')).to.equal('cjs')
expect(inferExportType('node')).to.equal('esm')
expect(inferExportType('some_unknown_condition')).to.equal('esm')
})
it('infers export type based on previous conditions', () => {
expect(inferExportType('import', ['require'])).to.equal('esm')
expect(inferExportType('node', ['require'])).to.equal('cjs')
expect(inferExportType('node', ['import'])).to.equal('esm')
expect(inferExportType('node', ['unknown', 'require'])).to.equal('cjs')
})
})

describe('extractExportFilenames', () => {
it('handles strings', () => {
expect(extractExportFilenames('test')).to.deep.equal([{ file: 'test', type: 'esm' }])
})
it('handles nested objects', () => {
expect(extractExportFilenames({ require: 'test' })).to.deep.equal([{ file: 'test', type: 'cjs' }])
// @ts-ignore TODO: fix pkg-types
expect(extractExportFilenames({ require: { node: 'test', other: { import: 'this', require: 'that' } } })).to.deep.equal([{ file: 'test', type: 'cjs' }, { file: 'this', type: 'esm' }, { file: 'that', type: 'cjs' }])
})
})
33 changes: 33 additions & 0 deletions test/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { fileURLToPath } from 'url'
import jiti from 'jiti'
import { expect } from 'chai'
import consola from 'consola'
import { join } from 'pathe'

const { validatePackage } = jiti(import.meta.url)('../src/validate') as typeof import('../src/validate')

describe('validatePackage', () => {
it('detects missing files', () => {
const logs: string[] = []
consola.mock(type => type === 'warn' ? (str: string) => logs.push(str) : () => {})

validatePackage({
main: './dist/test',
bin: {
'./cli': './dist/cli'
},
module: 'dist/mod',
exports: {
'./runtime/*': './runtime/*.mjs',
'.': { node: './src/index.ts' }
}
}, join(fileURLToPath(import.meta.url), '../fixture'))

expect(logs[0]).to.include('Potential missing')
expect(logs[0]).not.to.include('src/index.ts')

for (const file of ['dist/test', 'dist/cli', 'dist/mod', 'runtime']) {
expect(logs[0]).to.include(file)
}
})
})

0 comments on commit c9ce0b0

Please sign in to comment.