Skip to content

Commit

Permalink
feat: error on duplicated entries (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Dec 29, 2024
1 parent f0f7aa5 commit 265d082
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 10 deletions.
10 changes: 6 additions & 4 deletions src/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { glob } from 'glob'
import { getExportTypeFromFile, type ParsedExportsInfo } from './exports'
import { PackageMetadata, type Entries, ExportPaths } from './types'
import { logger } from './logger'
import { baseNameWithoutExtension, validateEntryFiles } from './util/file-path'
import {
baseNameWithoutExtension,
getSourcePathFromExportPath,
isBinExportPath,
isESModulePackage,
Expand Down Expand Up @@ -287,7 +287,7 @@ export async function collectSourceEntriesByExportPath(
const dirPath = path.join(sourceFolderPath, dirName)

// Match <name>{,/index}.{<ext>,<runtime>.<ext>}
const globalPatterns = [
const entryFilesPatterns = [
`${baseName}.{${[...availableExtensions].join(',')}}`,
`${baseName}.{${[...runtimeExportConventions].join(',')}}.{${[
...availableExtensions,
Expand All @@ -298,13 +298,15 @@ export async function collectSourceEntriesByExportPath(
].join(',')}}`,
]

const files = await glob(globalPatterns, {
const entryFiles = await glob(entryFilesPatterns, {
cwd: dirPath,
nodir: true,
ignore: PRIVATE_GLOB_PATTERN,
})

for (const file of files) {
validateEntryFiles(entryFiles)

for (const file of entryFiles) {
const ext = path.extname(file).slice(1)
if (!availableExtensions.has(ext) || isTestFile(file)) continue

Expand Down
2 changes: 1 addition & 1 deletion src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import type {
ParsedExportCondition,
} from './types'
import {
baseNameWithoutExtension,
getMainFieldExportType,
isESModulePackage,
joinRelativePath,
normalizePath,
} from './utils'
import { baseNameWithoutExtension } from './util/file-path'
import {
BINARY_TAG,
dtsExtensionsMap,
Expand Down
56 changes: 56 additions & 0 deletions src/util/file-path.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
getPathWithoutExtension,
baseNameWithoutExtension,
validateEntryFiles,
} from './file-path'

describe('getFirstBaseName', () => {
it('should return first part base name without extension of file path', () => {
expect(getPathWithoutExtension('index.js')).toBe('index')
expect(getPathWithoutExtension('index.d.ts')).toBe('index')
expect(getPathWithoutExtension('index')).toBe('index')
// give few segments nested file path
expect(getPathWithoutExtension('./foo/nested/index.js')).toBe(
'./foo/nested',
)
expect(getPathWithoutExtension('./foo/nested/index.d.ts')).toBe(
'./foo/nested',
)
expect(getPathWithoutExtension('./foo.jsx')).toBe('./foo')
})
})

describe('baseNameWithoutExtension', () => {
it('should return full base name without last extension of file path', () => {
// give few segments nested file path
expect(baseNameWithoutExtension('dist/foo/nested/index.js')).toBe('index')
expect(
baseNameWithoutExtension('dist/foo/nested/index.development.ts'),
).toBe('index.development')
expect(
baseNameWithoutExtension('dist/foo/nested/index.react-server.js'),
).toBe('index.react-server')
})
})

describe('validateEntryFiles', () => {
it('should throw error if there are multiple files with the same base name', () => {
expect(() =>
validateEntryFiles(['index.js', 'index/index.ts']),
).toThrowError('Conflicted entry files found for entries: .')
})
it.only('should throw error if the normalized base names are same', () => {
expect(() => validateEntryFiles(['foo/index.jsx', 'foo.ts'])).toThrowError(
'Conflicted entry files found for entries: ./foo',
)
})
it('should not throw error if there are no multiple files with the same base name', () => {
expect(() =>
validateEntryFiles([
'index.development.js',
'index.ts',
'index.react-server.mjs',
]),
).not.toThrow()
})
})
55 changes: 55 additions & 0 deletions src/util/file-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import path from 'path'

// Example: ./src/util/foo.ts -> ./src/util/foo
// Example: ./src/util/foo/index.ts -> ./src/util/foo
// Example: ./src/util/foo.d.ts -> ./src/util/foo
export const getPathWithoutExtension = (filePath: string): string => {
const pathWithoutExtension = filePath
// Remove the file extension first
.replace(/(\.\w+)+$/, '')
// Remove '/index' if it exists at the end of the path
.replace(/\/index$/, '')

return pathWithoutExtension
}

// Example: ./src/util/foo.development.ts -> foo.development
// Example: ./src/util/foo.react-server.ts -> foo.react-server
export const baseNameWithoutExtension = (filePath: string): string => {
return path.basename(filePath, path.extname(filePath))
}

export function validateEntryFiles(entryFiles: string[]) {
const fileBasePaths = new Set<string>()
const duplicatePaths = new Set<string>()

for (const filePath of entryFiles) {
// Check if there are multiple files with the same base name
const filePathWithoutExt = filePath
.slice(0, -path.extname(filePath).length)
.replace(/\\/g, '/')
const segments = filePathWithoutExt.split('/')
const lastSegment = segments.pop() || ''

if (lastSegment !== 'index' && lastSegment !== '') {
segments.push(lastSegment)
}
const fileBasePath = segments.join('/')

if (fileBasePaths.has(fileBasePath)) {
duplicatePaths.add(
// Add a dot if the base name is empty, 'foo' -> './foo', '' -> '.'
'./' + filePath.replace(/\\/g, '/'),
)
}
fileBasePaths.add(fileBasePath)
}

if (duplicatePaths.size > 0) {
throw new Error(
`Conflicted entry files found for entries: ${[...duplicatePaths].join(
', ',
)}`,
)
}
}
5 changes: 1 addition & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { logger } from './logger'
import { type OutputOptions } from 'rollup'
import { posixRelativify } from './lib/format'
import { baseNameWithoutExtension } from './util/file-path'

export function exit(err: string | Error) {
logger.error(err)
Expand Down Expand Up @@ -175,10 +176,6 @@ export const getMainFieldExportType = (pkg: PackageMetadata) => {
return mainExportType
}

// TODO: add unit test
export const baseNameWithoutExtension = (filename: string): string =>
path.basename(filename, path.extname(filename))

export const isTestFile = (filename: string): boolean =>
/\.(test|spec)$/.test(baseNameWithoutExtension(filename))

Expand Down
17 changes: 17 additions & 0 deletions test/integration/conflicted-entry/conflicted-entry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createIntegrationTest } from '../utils'

describe('integration - conflicted-entry', () => {
it('should error on conflicted entries', async () => {
await createIntegrationTest(
{
directory: __dirname,
},
async ({ code, stderr }) => {
expect(code).toBe(1)
expect(stderr).toContain(
'Conflicted entry files found for entries: ./foo',
)
},
)
})
})
6 changes: 6 additions & 0 deletions test/integration/conflicted-entry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "conflicted-entry",
"exports": {
"./foo": "./dist/foo.js"
}
}
1 change: 1 addition & 0 deletions test/integration/conflicted-entry/src/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Foo {}
1 change: 1 addition & 0 deletions test/integration/conflicted-entry/src/foo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Foo {}
2 changes: 1 addition & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"bunchee": ["./src/index.ts"]
}
},
"include": ["./**/*.test.ts"]
"include": ["./**/*.test.ts", "../src/**/*.test.ts"]
}

0 comments on commit 265d082

Please sign in to comment.