Skip to content

Commit 1afdea2

Browse files
chore: wip
1 parent a0cccac commit 1afdea2

35 files changed

+361
-53
lines changed

packages/bun-plugin/src/index.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
1-
import type { DtsGenerationOption } from '@stacksjs/dtsx'
1+
import type { DtsGenerationOption, GenerationStats } from '@stacksjs/dtsx'
22
import type { BunPlugin } from 'bun'
33
import process from 'node:process'
44
import { generate } from '@stacksjs/dtsx'
55

66
/**
77
* Configuration interface extending DtsGenerationOption with build-specific properties
88
*/
9-
interface PluginConfig extends DtsGenerationOption {
9+
export interface PluginConfig extends DtsGenerationOption {
1010
build?: {
1111
config: {
1212
root?: string
1313
outdir?: string
1414
}
1515
}
16+
17+
/**
18+
* Callback after successful generation
19+
*/
20+
onSuccess?: (stats: GenerationStats) => void | Promise<void>
21+
22+
/**
23+
* Callback on generation error
24+
*/
25+
onError?: (error: Error) => void | Promise<void>
26+
27+
/**
28+
* Whether to fail the build on generation error
29+
* @default true
30+
*/
31+
failOnError?: boolean
1632
}
1733

1834
/**
@@ -21,12 +37,34 @@ interface PluginConfig extends DtsGenerationOption {
2137
* @returns BunPlugin instance
2238
*/
2339
export function dts(options: PluginConfig = {}): BunPlugin {
40+
const { onSuccess, onError, failOnError = true, ...dtsOptions } = options
41+
2442
return {
2543
name: 'bun-plugin-dtsx',
2644

2745
async setup(build) {
28-
const config = normalizeConfig(options, build)
29-
await generate(config)
46+
try {
47+
const config = normalizeConfig(dtsOptions, build)
48+
const stats = await generate(config)
49+
50+
if (onSuccess) {
51+
await onSuccess(stats)
52+
}
53+
}
54+
catch (error) {
55+
const err = error instanceof Error ? error : new Error(String(error))
56+
57+
if (onError) {
58+
await onError(err)
59+
}
60+
else {
61+
console.error('[bun-plugin-dtsx] Error generating declarations:', err.message)
62+
}
63+
64+
if (failOnError) {
65+
throw err
66+
}
67+
}
3068
},
3169
}
3270
}
@@ -56,6 +94,6 @@ function normalizeConfig(options: PluginConfig, build: PluginConfig['build']): D
5694
}
5795
}
5896

59-
export type { DtsGenerationOption }
97+
export type { DtsGenerationOption, GenerationStats }
6098

6199
export default dts

packages/dtsx/bin/cli.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const defaultOptions: DtsGenerationConfig = {
1717
tsconfigPath: 'tsconfig.json',
1818
verbose: false,
1919
importOrder: ['bun'],
20+
dryRun: false,
21+
stats: false,
22+
continueOnError: false,
2023
}
2124

2225
cli
@@ -36,9 +39,13 @@ cli
3639
default: defaultOptions.importOrder?.join(','),
3740
type: [String],
3841
})
42+
.option('--dry-run', 'Show what would be generated without writing files', { default: defaultOptions.dryRun })
43+
.option('--stats', 'Show statistics after generation', { default: defaultOptions.stats })
44+
.option('--continue-on-error', 'Continue processing other files if one fails', { default: defaultOptions.continueOnError })
3945
.example('dtsx generate')
4046
.example('dtsx generate --entrypoints src/index.ts,src/utils.ts --outdir dist/types')
4147
.example('dtsx generate --import-order "node:,bun,@myorg/"')
48+
.example('dtsx generate --dry-run --stats')
4249
.action(async (options: DtsGenerationOption) => {
4350
try {
4451
const config: DtsGenerationConfig = {
@@ -47,10 +54,13 @@ cli
4754
root: resolve(options.root || defaultOptions.root),
4855
outdir: resolve(options.outdir || defaultOptions.outdir),
4956
tsconfigPath: resolve(options.tsconfigPath || defaultOptions.tsconfigPath),
50-
keepComments: options.keepComments || defaultOptions.keepComments,
51-
clean: options.clean || defaultOptions.clean,
52-
verbose: options.verbose || defaultOptions.verbose,
57+
keepComments: options.keepComments ?? defaultOptions.keepComments,
58+
clean: options.clean ?? defaultOptions.clean,
59+
verbose: options.verbose ?? defaultOptions.verbose,
5360
importOrder: options.importOrder || defaultOptions.importOrder,
61+
dryRun: options.dryRun ?? defaultOptions.dryRun,
62+
stats: options.stats ?? defaultOptions.stats,
63+
continueOnError: options.continueOnError ?? defaultOptions.continueOnError,
5464
}
5565

5666
await generate(config)

packages/dtsx/src/generator.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-console */
22

3-
import type { DtsGenerationConfig, ProcessingContext } from './types'
3+
import type { DtsGenerationConfig, GenerationStats, ProcessingContext } from './types'
44
import { Glob } from 'bun'
55
import { mkdir, readFile } from 'node:fs/promises'
66
import { dirname, relative, resolve } from 'node:path'
@@ -12,9 +12,22 @@ import { writeToFile } from './utils'
1212
/**
1313
* Generate DTS files from TypeScript source files
1414
*/
15-
export async function generate(options?: Partial<DtsGenerationConfig>): Promise<void> {
15+
export async function generate(options?: Partial<DtsGenerationConfig>): Promise<GenerationStats> {
16+
const startTime = Date.now()
1617
const config = { ...defaultConfig, ...options }
1718

19+
// Statistics tracking
20+
const stats: GenerationStats = {
21+
filesProcessed: 0,
22+
filesGenerated: 0,
23+
filesFailed: 0,
24+
declarationsFound: 0,
25+
importsProcessed: 0,
26+
exportsProcessed: 0,
27+
durationMs: 0,
28+
errors: [],
29+
}
30+
1831
// Log start if verbose
1932
if (config.verbose) {
2033
console.log('Starting DTS generation...')
@@ -32,27 +45,78 @@ export async function generate(options?: Partial<DtsGenerationConfig>): Promise<
3245
for (const file of files) {
3346
try {
3447
const outputPath = getOutputPath(file, config)
35-
const dtsContent = await processFile(file, config)
36-
37-
// Ensure output directory exists
38-
await mkdir(dirname(outputPath), { recursive: true })
48+
const { content: dtsContent, declarationCount, importCount, exportCount } = await processFileWithStats(file, config)
49+
50+
stats.filesProcessed++
51+
stats.declarationsFound += declarationCount
52+
stats.importsProcessed += importCount
53+
stats.exportsProcessed += exportCount
54+
55+
if (config.dryRun) {
56+
// Dry run - just show what would be generated
57+
console.log(`[dry-run] Would generate: ${outputPath}`)
58+
if (config.verbose) {
59+
console.log('--- Content preview ---')
60+
console.log(dtsContent.slice(0, 500) + (dtsContent.length > 500 ? '\n...' : ''))
61+
console.log('--- End preview ---')
62+
}
63+
}
64+
else {
65+
// Ensure output directory exists
66+
await mkdir(dirname(outputPath), { recursive: true })
3967

40-
// Write the DTS file
41-
await writeToFile(outputPath, dtsContent)
68+
// Write the DTS file
69+
await writeToFile(outputPath, dtsContent)
70+
stats.filesGenerated++
4271

43-
if (config.verbose) {
44-
console.log(`Generated: ${outputPath}`)
72+
if (config.verbose) {
73+
console.log(`Generated: ${outputPath}`)
74+
}
4575
}
4676
}
4777
catch (error) {
48-
console.error(`Error processing ${file}:`, error)
49-
throw error
78+
const errorMessage = error instanceof Error ? error.message : String(error)
79+
stats.filesFailed++
80+
stats.errors.push({ file, error: errorMessage })
81+
82+
if (config.continueOnError) {
83+
console.error(`[warning] Error processing ${file}: ${errorMessage}`)
84+
}
85+
else {
86+
console.error(`Error processing ${file}:`, error)
87+
throw error
88+
}
89+
}
90+
}
91+
92+
stats.durationMs = Date.now() - startTime
93+
94+
// Show stats if enabled
95+
if (config.stats) {
96+
console.log('\n--- Generation Statistics ---')
97+
console.log(`Files processed: ${stats.filesProcessed}`)
98+
console.log(`Files generated: ${stats.filesGenerated}`)
99+
if (stats.filesFailed > 0) {
100+
console.log(`Files failed: ${stats.filesFailed}`)
101+
}
102+
console.log(`Declarations found: ${stats.declarationsFound}`)
103+
console.log(`Imports processed: ${stats.importsProcessed}`)
104+
console.log(`Exports processed: ${stats.exportsProcessed}`)
105+
console.log(`Duration: ${stats.durationMs}ms`)
106+
if (stats.errors.length > 0) {
107+
console.log('\nErrors:')
108+
for (const { file, error } of stats.errors) {
109+
console.log(` - ${file}: ${error}`)
110+
}
50111
}
112+
console.log('-----------------------------\n')
51113
}
52114

53115
if (config.verbose) {
54116
console.log('DTS generation complete!')
55117
}
118+
119+
return stats
56120
}
57121

58122
/**
@@ -118,12 +182,27 @@ export async function processFile(
118182
filePath: string,
119183
config: DtsGenerationConfig,
120184
): Promise<string> {
185+
const result = await processFileWithStats(filePath, config)
186+
return result.content
187+
}
188+
189+
/**
190+
* Process a single TypeScript file and return DTS with statistics
191+
*/
192+
async function processFileWithStats(
193+
filePath: string,
194+
config: DtsGenerationConfig,
195+
): Promise<{ content: string, declarationCount: number, importCount: number, exportCount: number }> {
121196
// Read the source file
122197
const sourceCode = await readFile(filePath, 'utf-8')
123198

124199
// Extract declarations
125200
const declarations = extractDeclarations(sourceCode, filePath, config.keepComments)
126201

202+
// Count imports and exports
203+
const importCount = declarations.filter(d => d.kind === 'import').length
204+
const exportCount = declarations.filter(d => d.kind === 'export' || d.isExported).length
205+
127206
// Create processing context
128207
const context: ProcessingContext = {
129208
filePath,
@@ -137,5 +216,10 @@ export async function processFile(
137216
// Process declarations to generate DTS
138217
const dtsContent = processDeclarations(declarations, context, config.keepComments, config.importOrder)
139218

140-
return dtsContent
219+
return {
220+
content: dtsContent,
221+
declarationCount: declarations.length,
222+
importCount,
223+
exportCount,
224+
}
141225
}

packages/dtsx/src/types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ export interface DtsGenerationConfig {
2323
* @example ['node:', 'bun', '@myorg/']
2424
*/
2525
importOrder?: string[]
26+
/**
27+
* Dry run mode - show what would be generated without writing files
28+
*/
29+
dryRun?: boolean
30+
/**
31+
* Show statistics after generation (files processed, declarations found, etc.)
32+
*/
33+
stats?: boolean
34+
/**
35+
* Continue processing other files if one file fails
36+
* @default false
37+
*/
38+
continueOnError?: boolean
39+
}
40+
41+
/**
42+
* Generation statistics
43+
*/
44+
export interface GenerationStats {
45+
filesProcessed: number
46+
filesGenerated: number
47+
filesFailed: number
48+
declarationsFound: number
49+
importsProcessed: number
50+
exportsProcessed: number
51+
durationMs: number
52+
errors: Array<{ file: string, error: string }>
2653
}
2754

2855
/**

packages/dtsx/src/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { pathToFileURL } from 'node:url'
66
import { config } from './config'
77

88
export async function writeToFile(filePath: string, content: string): Promise<void> {
9-
await Bun.write(filePath, content)
9+
// Normalize line endings to LF and ensure trailing newline
10+
let normalized = content.replace(/\r\n/g, '\n')
11+
if (!normalized.endsWith('\n')) {
12+
normalized += '\n'
13+
}
14+
await Bun.write(filePath, normalized)
1015
}
1116

1217
export async function getAllTypeScriptFiles(directory?: string): Promise<string[]> {

packages/dtsx/test/dts.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('dts-generation', () => {
2626
'0012',
2727
]
2828

29-
// List of all fixture files to test (excluding checker.ts which is too large)
29+
// List of all fixture files to test
3030
const fixtures = [
3131
'abseil.io',
3232
'class',
@@ -44,6 +44,11 @@ describe('dts-generation', () => {
4444
'variable',
4545
]
4646

47+
// Large fixture files (slower tests)
48+
const largeFixtures = [
49+
'checker',
50+
]
51+
4752
// Generate a test for each example file
4853
examples.forEach((example) => {
4954
it(`should properly generate types for example ${example}`, async () => {
@@ -90,6 +95,29 @@ describe('dts-generation', () => {
9095
})
9196
})
9297

98+
// Generate a test for each large fixture file (slower tests)
99+
largeFixtures.forEach((fixture) => {
100+
it(`should properly generate types for large fixture ${fixture}`, async () => {
101+
const config: DtsGenerationOption = {
102+
entrypoints: [join(inputDir, `${fixture}.ts`)],
103+
outdir: generatedDir,
104+
clean: false,
105+
tsconfigPath: join(__dirname, '..', 'tsconfig.json'),
106+
outputStructure: 'flat',
107+
}
108+
109+
await generate(config)
110+
111+
const outputPath = join(outputDir, `${fixture}.d.ts`)
112+
const generatedPath = join(generatedDir, `${fixture}.d.ts`)
113+
114+
const expectedContent = await Bun.file(outputPath).text()
115+
const generatedContent = await Bun.file(generatedPath).text()
116+
117+
expect(generatedContent).toBe(expectedContent)
118+
}, 30000) // 30 second timeout for large files
119+
})
120+
93121
afterEach(async () => {
94122
// Clean up generated files
95123
try {

packages/dtsx/test/fixtures/output/abseil.io.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ export declare const abseilioPackage: {
5252
aliases: readonly [];
5353
fullPath: 'abseil.io'
5454
};
55-
export type AbseilioPackage = typeof abseilioPackage
55+
export type AbseilioPackage = typeof abseilioPackage

0 commit comments

Comments
 (0)