ReviewOS

also looking at this

stacks/dtsx

fix(generator): skip glob and relative entrypoints outside the root

#3126
Open mkantautas wants to merge fix/glob-entrypoint-outside-root into main
3 files +74 -2

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/config.mdmodified+2-0
Changes to docs/config.md
@@ -210,6 +210,8 @@ entrypoints: [
210210]
211211```
212212
213A file an entry point reaches outside `root` (an absolute path, or a pattern such as `../*.ts`) is skipped with a warning, since its declaration would be written outside `outdir`. With `outputStructure: 'flat'` it is written into `outdir` under its file name.
214
213215### Output Structure
214216
215217Choose between mirroring source structure or flat output:
packages/dtsx/src/generator.tsmodified+8-2
Changes to packages/dtsx/src/generator.ts
@@ -801,9 +801,15 @@ async function findFiles(config: DtsGenerationConfig): Promise<string[]> {
801801 absolute: true,
802802 onlyFiles: true,
803803 })) {
804 // Skip .d.ts files, node_modules, and excluded patterns
804 // Skip .d.ts files, node_modules, and excluded patterns. A pattern
805 // reaching above the root (`../*.ts`, `../index.ts`) matches files
806 // whose declarations would be written outside outdir; they are
807 // skipped as an absolute path outside the root is.
805808 if (isProcessableSourceFile(file) && !file.includes('node_modules')) {
806 if (!isExcluded(file, excludePatterns, rootPath)) {
809 if (!isWithinRoot(file, rootPath) && config.outputStructure !== 'flat') {
810 logger.warn(`Skipping entrypoint outside root: ${relative(config.cwd, file)}`)
811 }
812 else if (!isExcluded(file, excludePatterns, rootPath)) {
807813 files.push(file)
808814 }
809815 }
packages/dtsx/test/entrypoint-outside-root.test.tsadded+64-0
Changes to packages/dtsx/test/entrypoint-outside-root.test.ts
@@ -0,0 +1,64 @@
1/**
2 * An entrypoint pattern reaching above the root wrote its declarations outside
3 * outdir:
4 *
5 * generate({ root: 'src', outdir: 'dist', entrypoints: ['../*.ts'] })
6 *
7 * wrote `outside.d.ts` next to `outside.ts`, and `../outside.ts` did the same.
8 * An absolute path outside the root was already skipped with a warning; a
9 * glob or relative pattern now is too.
10 */
11import { afterEach, describe, expect, it } from 'bun:test'
12import { existsSync } from 'node:fs'
13import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
14import { tmpdir } from 'node:os'
15import { join } from 'node:path'
16import { generate } from '../src/generator'
17
18const tempDirectories: string[] = []
19
20afterEach(async () => {
21 await Promise.all(tempDirectories.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
22})
23
24/** A project with `src/inside.ts` and `outside.ts` next to `src`. */
25async function project(): Promise<string> {
26 const cwd = await mkdtemp(join(tmpdir(), 'dtsx-outside-root-'))
27 tempDirectories.push(cwd)
28 await mkdir(join(cwd, 'src'))
29 await writeFile(join(cwd, 'src', 'inside.ts'), 'export const inside = 1\n')
30 await writeFile(join(cwd, 'outside.ts'), 'export const outside = 2\n')
31 return cwd
32}
33
34describe('entrypoints outside the root', () => {
35 it.each([
36 ['glob', ['../*.ts']],
37 ['relative path', ['../outside.ts']],
38 ])('are not written outside outdir: %s', async (_label, entrypoints) => {
39 const cwd = await project()
40 await expect(generate({ cwd, root: 'src', outdir: 'dist', entrypoints, clean: false })).rejects.toThrow('No source files matched')
41 expect(existsSync(join(cwd, 'outside.d.ts'))).toBe(false)
42 expect(existsSync(join(cwd, 'dist', 'outside.d.ts'))).toBe(false)
43 })
44
45 it('are skipped beside an entrypoint inside the root', async () => {
46 const cwd = await project()
47 await generate({ cwd, root: 'src', outdir: 'dist', entrypoints: ['*.ts', '../*.ts'], clean: false })
48 expect(existsSync(join(cwd, 'outside.d.ts'))).toBe(false)
49 expect(existsSync(join(cwd, 'dist', 'outside.d.ts'))).toBe(false)
50 expect(existsSync(join(cwd, 'dist', 'inside.d.ts'))).toBe(true)
51 })
52
53 it('are written into outdir with a flat output structure', async () => {
54 const cwd = await project()
55 await generate({ cwd, root: 'src', outdir: 'dist', entrypoints: ['../*.ts'], outputStructure: 'flat', clean: false })
56 expect(existsSync(join(cwd, 'outside.d.ts'))).toBe(false)
57 expect(existsSync(join(cwd, 'dist', 'outside.d.ts'))).toBe(true)
58 })
59
60 it('still fail when an exclude leaves nothing', async () => {
61 const cwd = await project()
62 await expect(generate({ cwd, root: 'src', outdir: 'dist', entrypoints: ['../*.ts'], exclude: ['../*.ts'], outputStructure: 'flat', clean: false })).rejects.toThrow('No source files matched')
63 })
64})