eprec 0.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -29
  3. package/app/assets/styles.css +129 -0
  4. package/app/client/app.tsx +37 -0
  5. package/app/client/counter.tsx +22 -0
  6. package/app/client/entry.tsx +8 -0
  7. package/app/components/layout.tsx +37 -0
  8. package/app/config/env.ts +31 -0
  9. package/app/config/import-map.ts +9 -0
  10. package/app/config/init-env.ts +3 -0
  11. package/app/config/routes.ts +5 -0
  12. package/app/helpers/render.ts +6 -0
  13. package/app/router.tsx +102 -0
  14. package/app/routes/index.tsx +50 -0
  15. package/app-server.ts +60 -0
  16. package/cli.ts +173 -0
  17. package/package.json +46 -7
  18. package/process-course/chapter-processor.ts +1037 -0
  19. package/process-course/cli.ts +236 -0
  20. package/process-course/config.ts +50 -0
  21. package/process-course/edits/cli.ts +167 -0
  22. package/process-course/edits/combined-video-editor.ts +316 -0
  23. package/process-course/edits/edit-workspace.ts +90 -0
  24. package/process-course/edits/index.ts +20 -0
  25. package/process-course/edits/regenerate-transcript.ts +84 -0
  26. package/process-course/edits/remove-ranges.test.ts +36 -0
  27. package/process-course/edits/remove-ranges.ts +287 -0
  28. package/process-course/edits/timestamp-refinement.test.ts +25 -0
  29. package/process-course/edits/timestamp-refinement.ts +172 -0
  30. package/process-course/edits/transcript-diff.test.ts +105 -0
  31. package/process-course/edits/transcript-diff.ts +214 -0
  32. package/process-course/edits/transcript-output.test.ts +50 -0
  33. package/process-course/edits/transcript-output.ts +36 -0
  34. package/process-course/edits/types.ts +26 -0
  35. package/process-course/edits/video-editor.ts +246 -0
  36. package/process-course/errors.test.ts +63 -0
  37. package/process-course/errors.ts +82 -0
  38. package/process-course/ffmpeg.ts +449 -0
  39. package/process-course/jarvis-commands/handlers.ts +71 -0
  40. package/process-course/jarvis-commands/index.ts +14 -0
  41. package/process-course/jarvis-commands/parser.test.ts +348 -0
  42. package/process-course/jarvis-commands/parser.ts +257 -0
  43. package/process-course/jarvis-commands/types.ts +46 -0
  44. package/process-course/jarvis-commands/windows.ts +254 -0
  45. package/process-course/logging.ts +24 -0
  46. package/process-course/paths.test.ts +59 -0
  47. package/process-course/paths.ts +53 -0
  48. package/process-course/summary.test.ts +209 -0
  49. package/process-course/summary.ts +210 -0
  50. package/process-course/types.ts +85 -0
  51. package/process-course/utils/audio-analysis.test.ts +348 -0
  52. package/process-course/utils/audio-analysis.ts +463 -0
  53. package/process-course/utils/chapter-selection.test.ts +307 -0
  54. package/process-course/utils/chapter-selection.ts +136 -0
  55. package/process-course/utils/file-utils.test.ts +83 -0
  56. package/process-course/utils/file-utils.ts +57 -0
  57. package/process-course/utils/filename.test.ts +27 -0
  58. package/process-course/utils/filename.ts +12 -0
  59. package/process-course/utils/time-ranges.test.ts +221 -0
  60. package/process-course/utils/time-ranges.ts +86 -0
  61. package/process-course/utils/transcript.test.ts +257 -0
  62. package/process-course/utils/transcript.ts +86 -0
  63. package/process-course/utils/video-editing.ts +44 -0
  64. package/process-course-video.ts +389 -0
  65. package/public/robots.txt +2 -0
  66. package/server/bundling.ts +210 -0
  67. package/speech-detection.ts +355 -0
  68. package/utils.ts +138 -0
  69. package/whispercpp-transcribe.ts +343 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env bun
2
+ import path from 'node:path'
3
+ import { mkdir } from 'node:fs/promises'
4
+ import { ensureFfmpegAvailable, getChapters } from './process-course/ffmpeg'
5
+ import { logInfo } from './process-course/logging'
6
+ import { parseCliArgs, type CliArgs } from './process-course/cli'
7
+ import { resolveChapterSelection } from './process-course/utils/chapter-selection'
8
+ import { removeDirIfEmpty } from './process-course/utils/file-utils'
9
+ import { writeJarvisLogs, writeSummaryLogs } from './process-course/summary'
10
+ import {
11
+ processChapter,
12
+ type ChapterProcessingOptions,
13
+ } from './process-course/chapter-processor'
14
+ import type {
15
+ JarvisEdit,
16
+ JarvisNote,
17
+ JarvisWarning,
18
+ ProcessedChapterInfo,
19
+ EditWorkspaceInfo,
20
+ } from './process-course/types'
21
+ import { formatSeconds } from './utils'
22
+ import { checkSegmentHasSpeech } from './speech-detection'
23
+
24
+ interface ProcessingSummary {
25
+ totalSelected: number
26
+ processed: number
27
+ skippedShortInitial: number
28
+ skippedShortTrimmed: number
29
+ skippedTranscription: number
30
+ fallbackNotes: number
31
+ logsWritten: number
32
+ jarvisWarnings: number
33
+ editsPending: number
34
+ }
35
+
36
+ export type ProcessCourseOptions = Omit<CliArgs, 'shouldExit'>
37
+
38
+ export async function runProcessCourse(options: ProcessCourseOptions) {
39
+ const {
40
+ inputPaths,
41
+ outputDir,
42
+ minChapterDurationSeconds,
43
+ dryRun,
44
+ keepIntermediates,
45
+ writeLogs,
46
+ chapterSelection,
47
+ enableTranscription,
48
+ whisperModelPath,
49
+ whisperLanguage,
50
+ whisperBinaryPath,
51
+ } = options
52
+
53
+ await ensureFfmpegAvailable()
54
+
55
+ // Process each input file in turn
56
+ for (const inputPath of inputPaths) {
57
+ // Determine output directory for this file
58
+ let fileOutputDir: string
59
+ if (outputDir) {
60
+ // If only one input file, use output directory as-is
61
+ // Otherwise, create a subdirectory for each file
62
+ fileOutputDir =
63
+ inputPaths.length === 1
64
+ ? outputDir
65
+ : path.join(outputDir, path.parse(inputPath).name)
66
+ } else {
67
+ // Default: create directory next to input file
68
+ fileOutputDir = path.join(
69
+ path.dirname(inputPath),
70
+ path.parse(inputPath).name,
71
+ )
72
+ }
73
+
74
+ await processInputFile({
75
+ inputPath,
76
+ outputDir: fileOutputDir,
77
+ minChapterDurationSeconds,
78
+ dryRun,
79
+ keepIntermediates,
80
+ writeLogs,
81
+ chapterSelection,
82
+ enableTranscription,
83
+ whisperModelPath,
84
+ whisperLanguage,
85
+ whisperBinaryPath,
86
+ })
87
+ }
88
+ }
89
+
90
+ export async function runProcessCourseCli(rawArgs?: string[]) {
91
+ const parsedArgs = parseCliArgs(rawArgs)
92
+ if (parsedArgs.shouldExit) {
93
+ return
94
+ }
95
+
96
+ await runProcessCourse(parsedArgs)
97
+ }
98
+
99
+ async function processInputFile(options: {
100
+ inputPath: string
101
+ outputDir: string
102
+ minChapterDurationSeconds: number
103
+ dryRun: boolean
104
+ keepIntermediates: boolean
105
+ writeLogs: boolean
106
+ chapterSelection: import('./process-course/types').ChapterSelection | null
107
+ enableTranscription: boolean
108
+ whisperModelPath: string
109
+ whisperLanguage: string
110
+ whisperBinaryPath: string | undefined
111
+ }) {
112
+ const {
113
+ inputPath,
114
+ outputDir,
115
+ minChapterDurationSeconds,
116
+ dryRun,
117
+ keepIntermediates,
118
+ writeLogs,
119
+ chapterSelection,
120
+ enableTranscription,
121
+ whisperModelPath,
122
+ whisperLanguage,
123
+ whisperBinaryPath,
124
+ } = options
125
+
126
+ const tmpDir = path.join(outputDir, '.tmp')
127
+
128
+ const inputFile = Bun.file(inputPath)
129
+ if (!(await inputFile.exists())) {
130
+ throw new Error(`Input file not found: ${inputPath}`)
131
+ }
132
+
133
+ if (!dryRun) {
134
+ await mkdir(outputDir, { recursive: true })
135
+ await mkdir(tmpDir, { recursive: true })
136
+ }
137
+
138
+ const chapters = await getChapters(inputPath)
139
+ if (chapters.length === 0) {
140
+ throw new Error('No chapters found. The input must contain chapters.')
141
+ }
142
+
143
+ const chapterIndexes = chapterSelection
144
+ ? resolveChapterSelection(chapterSelection, chapters.length)
145
+ : null
146
+
147
+ logStartupInfo({
148
+ inputPath,
149
+ chaptersCount: chapters.length,
150
+ chapterIndexes,
151
+ minChapterDurationSeconds,
152
+ dryRun,
153
+ keepIntermediates,
154
+ writeLogs,
155
+ enableTranscription,
156
+ whisperModelPath,
157
+ whisperLanguage,
158
+ whisperBinaryPath,
159
+ })
160
+
161
+ chapters.forEach((chapter) => {
162
+ logInfo(
163
+ `- [${chapter.index + 1}] ${chapter.title} (${formatSeconds(chapter.start)} -> ${formatSeconds(chapter.end)})`,
164
+ )
165
+ })
166
+
167
+ const selectedChapters = chapterIndexes
168
+ ? chapters.filter((chapter) => chapterIndexes.includes(chapter.index))
169
+ : chapters
170
+
171
+ const summary: ProcessingSummary = {
172
+ totalSelected: selectedChapters.length,
173
+ processed: 0,
174
+ skippedShortInitial: 0,
175
+ skippedShortTrimmed: 0,
176
+ skippedTranscription: 0,
177
+ fallbackNotes: 0,
178
+ logsWritten: 0,
179
+ jarvisWarnings: 0,
180
+ editsPending: 0,
181
+ }
182
+ const summaryDetails: string[] = []
183
+ const jarvisWarnings: JarvisWarning[] = []
184
+ const jarvisEdits: JarvisEdit[] = []
185
+ const jarvisNotes: JarvisNote[] = []
186
+ const editWorkspaces: EditWorkspaceInfo[] = []
187
+
188
+ const processingOptions: ChapterProcessingOptions = {
189
+ inputPath,
190
+ outputDir,
191
+ tmpDir,
192
+ minChapterDurationSeconds,
193
+ enableTranscription,
194
+ whisperModelPath,
195
+ whisperLanguage,
196
+ whisperBinaryPath,
197
+ keepIntermediates,
198
+ writeLogs,
199
+ dryRun,
200
+ }
201
+
202
+ // Track processed chapters that have speech (for combine logic)
203
+ const processedChaptersWithSpeech: ProcessedChapterInfo[] = []
204
+ let previousProcessedChapter: ProcessedChapterInfo | null = null
205
+
206
+ for (const chapter of selectedChapters) {
207
+ // Determine which chapter to combine with
208
+ // Always use the most recent processed chapter with speech (if any)
209
+ const chapterToCombineWith: ProcessedChapterInfo | null =
210
+ processedChaptersWithSpeech.at(-1) ?? null
211
+ // If previousProcessedChapter exists but is different, log that we're skipping it
212
+ if (
213
+ chapterToCombineWith !== null &&
214
+ previousProcessedChapter !== null &&
215
+ previousProcessedChapter !== chapterToCombineWith
216
+ ) {
217
+ logInfo(
218
+ `Previous chapter ${previousProcessedChapter.chapter.index + 1} has no speech. Using chapter ${chapterToCombineWith.chapter.index + 1} for combine instead.`,
219
+ )
220
+ }
221
+
222
+ const result = await processChapter(chapter, {
223
+ ...processingOptions,
224
+ previousProcessedChapter: chapterToCombineWith,
225
+ })
226
+
227
+ // Update summary based on result
228
+ if (result.status === 'processed') {
229
+ summary.processed += 1
230
+ } else {
231
+ switch (result.skipReason) {
232
+ case 'short-initial':
233
+ summary.skippedShortInitial += 1
234
+ summaryDetails.push(
235
+ `Skipped chapter ${chapter.index + 1} (${formatSeconds(chapter.end - chapter.start)} < ${formatSeconds(minChapterDurationSeconds)}).`,
236
+ )
237
+ break
238
+ case 'short-trimmed':
239
+ summary.skippedShortTrimmed += 1
240
+ summaryDetails.push(
241
+ `Skipped chapter ${chapter.index + 1} (trimmed duration too short).`,
242
+ )
243
+ break
244
+ case 'transcript':
245
+ case 'bad-take':
246
+ summary.skippedTranscription += 1
247
+ summaryDetails.push(
248
+ `Skipped chapter ${chapter.index + 1} (${result.skipReason}).`,
249
+ )
250
+ break
251
+ case 'dry-run':
252
+ // Dry run counts as processed
253
+ break
254
+ }
255
+ }
256
+
257
+ if (result.logWritten) {
258
+ summary.logsWritten += 1
259
+ }
260
+
261
+ if (result.fallbackNote) {
262
+ summary.fallbackNotes += 1
263
+ summaryDetails.push(
264
+ `Fallback for chapter ${chapter.index + 1}: ${result.fallbackNote}`,
265
+ )
266
+ }
267
+
268
+ if (result.jarvisWarning) {
269
+ jarvisWarnings.push(result.jarvisWarning)
270
+ summary.jarvisWarnings += 1
271
+ }
272
+
273
+ if (result.jarvisEdit) {
274
+ jarvisEdits.push(result.jarvisEdit)
275
+ }
276
+
277
+ if (result.jarvisNotes) {
278
+ jarvisNotes.push(...result.jarvisNotes)
279
+ }
280
+
281
+ if (result.editWorkspace) {
282
+ editWorkspaces.push(result.editWorkspace)
283
+ summary.editsPending += 1
284
+ }
285
+
286
+ // Update previous processed chapter for combine logic
287
+ if (result.status === 'processed' && result.processedInfo) {
288
+ previousProcessedChapter = result.processedInfo
289
+
290
+ // If we combined with a chapter, the combined output replaces that chapter in the list
291
+ if (chapterToCombineWith) {
292
+ const combineIndex = processedChaptersWithSpeech.findIndex(
293
+ (ch) => ch === chapterToCombineWith,
294
+ )
295
+ if (combineIndex >= 0) {
296
+ // Replace the combined chapter with the new combined output
297
+ processedChaptersWithSpeech[combineIndex] = result.processedInfo
298
+ } else {
299
+ // Shouldn't happen, but if it does, add the combined result
300
+ processedChaptersWithSpeech.push(result.processedInfo)
301
+ }
302
+ } else {
303
+ // Not a combine - check if this chapter has speech and add to list if it does
304
+ const hasSpeech = await checkSegmentHasSpeech(
305
+ result.processedInfo.outputPath,
306
+ result.processedInfo.processedDuration,
307
+ )
308
+ if (hasSpeech) {
309
+ processedChaptersWithSpeech.push(result.processedInfo)
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ // Always write jarvis logs (summary information)
316
+ await writeJarvisLogs({
317
+ outputDir,
318
+ inputPath,
319
+ jarvisWarnings,
320
+ jarvisEdits,
321
+ jarvisNotes,
322
+ editWorkspaces,
323
+ dryRun,
324
+ })
325
+
326
+ // Only write detailed summary log when writeLogs is enabled
327
+ if (writeLogs) {
328
+ await writeSummaryLogs({
329
+ tmpDir,
330
+ outputDir,
331
+ inputPath,
332
+ summary,
333
+ summaryDetails,
334
+ jarvisWarnings,
335
+ jarvisEdits,
336
+ editWorkspaces,
337
+ dryRun,
338
+ })
339
+ }
340
+
341
+ if (!dryRun) {
342
+ await removeDirIfEmpty(tmpDir)
343
+ }
344
+ }
345
+
346
+ function logStartupInfo(options: {
347
+ inputPath: string
348
+ chaptersCount: number
349
+ chapterIndexes: number[] | null
350
+ minChapterDurationSeconds: number
351
+ dryRun: boolean
352
+ keepIntermediates: boolean
353
+ writeLogs: boolean
354
+ enableTranscription: boolean
355
+ whisperModelPath: string
356
+ whisperLanguage: string
357
+ whisperBinaryPath: string | undefined
358
+ }) {
359
+ logInfo(`Processing: ${options.inputPath}`)
360
+ logInfo(`Chapters found: ${options.chaptersCount}`)
361
+ if (options.chapterIndexes) {
362
+ logInfo(
363
+ `Filtering to chapters: ${options.chapterIndexes.map((index) => index + 1).join(', ')}`,
364
+ )
365
+ }
366
+ logInfo(
367
+ `Skipping chapters shorter than ${formatSeconds(options.minChapterDurationSeconds)}.`,
368
+ )
369
+ if (options.dryRun) {
370
+ logInfo('Dry run enabled; no files will be written.')
371
+ } else if (options.keepIntermediates) {
372
+ logInfo('Keeping intermediate files for debugging.')
373
+ }
374
+ if (options.writeLogs) {
375
+ logInfo('Writing log files for skipped/fallback cases.')
376
+ }
377
+ if (options.enableTranscription) {
378
+ logInfo(
379
+ `Whisper transcription enabled (model: ${options.whisperModelPath}, language: ${options.whisperLanguage}, binary: ${options.whisperBinaryPath}).`,
380
+ )
381
+ }
382
+ }
383
+
384
+ if (import.meta.main) {
385
+ runProcessCourseCli().catch((error) => {
386
+ console.error(`[error] ${error instanceof Error ? error.message : error}`)
387
+ process.exit(1)
388
+ })
389
+ }
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Disallow:
@@ -0,0 +1,210 @@
1
+ import path from 'node:path'
2
+
3
+ type PackageJson = {
4
+ exports?: Record<string, { default?: string; types?: string } | string>
5
+ module?: string
6
+ main?: string
7
+ }
8
+
9
+ async function resolvePackageExport(
10
+ specifier: string,
11
+ rootDir: string,
12
+ ): Promise<string | null> {
13
+ const parts = specifier.split('/')
14
+ let packageName: string
15
+ let subpathParts: string[]
16
+
17
+ if (specifier.startsWith('@')) {
18
+ if (parts.length < 2) return null
19
+ packageName = `${parts[0]}/${parts[1]}`
20
+ subpathParts = parts.slice(2)
21
+ } else {
22
+ if (parts.length === 0 || !parts[0]) return null
23
+ packageName = parts[0]
24
+ subpathParts = parts.slice(1)
25
+ }
26
+
27
+ const subpath = subpathParts.length > 0 ? `./${subpathParts.join('/')}` : '.'
28
+ const packageDir = path.join(rootDir, 'node_modules', packageName)
29
+ const packageJsonPath = path.join(packageDir, 'package.json')
30
+ const packageJsonFile = Bun.file(packageJsonPath)
31
+
32
+ if (!(await packageJsonFile.exists())) return null
33
+
34
+ const packageJson = JSON.parse(
35
+ await packageJsonFile.text(),
36
+ ) as PackageJson
37
+
38
+ if (!packageJson.exports) {
39
+ const entryFile = packageJson.module || packageJson.main
40
+ if (entryFile) {
41
+ const entryPath = path.join(packageDir, entryFile)
42
+ if (await Bun.file(entryPath).exists()) return entryPath
43
+ }
44
+ const indexPath = path.join(packageDir, 'index.js')
45
+ return (await Bun.file(indexPath).exists()) ? indexPath : null
46
+ }
47
+
48
+ const exportEntry = packageJson.exports[subpath]
49
+ if (!exportEntry) return null
50
+
51
+ const exportPath =
52
+ typeof exportEntry === 'string'
53
+ ? exportEntry
54
+ : exportEntry.default
55
+
56
+ if (!exportPath) return null
57
+
58
+ const resolvedPath = path.join(packageDir, exportPath)
59
+ return (await Bun.file(resolvedPath).exists()) ? resolvedPath : null
60
+ }
61
+
62
+ const BUNDLING_CORS_HEADERS = {
63
+ 'Access-Control-Allow-Origin': '*',
64
+ 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
65
+ 'Access-Control-Allow-Headers': 'Accept, Content-Type',
66
+ } as const
67
+
68
+ export function createBundlingRoutes(rootDir: string) {
69
+ const clientDir = path.resolve(rootDir, 'app', 'client')
70
+
71
+ return {
72
+ '/app/client/*': async (request: Request) => {
73
+ if (request.method === 'OPTIONS') {
74
+ return new Response(null, {
75
+ status: 204,
76
+ headers: {
77
+ ...BUNDLING_CORS_HEADERS,
78
+ 'Access-Control-Max-Age': '86400',
79
+ },
80
+ })
81
+ }
82
+
83
+ const url = new URL(request.url)
84
+ const reqPath = path.posix.normalize(url.pathname.replace(/^\/+/, ''))
85
+ const resolved = path.resolve(rootDir, reqPath)
86
+
87
+ if (!resolved.startsWith(clientDir + path.sep)) {
88
+ return new Response('Forbidden', {
89
+ status: 403,
90
+ headers: BUNDLING_CORS_HEADERS,
91
+ })
92
+ }
93
+
94
+ if (!resolved.endsWith('.ts') && !resolved.endsWith('.tsx')) {
95
+ return new Response('Not Found', {
96
+ status: 404,
97
+ headers: BUNDLING_CORS_HEADERS,
98
+ })
99
+ }
100
+
101
+ const entryFile = Bun.file(resolved)
102
+ if (!(await entryFile.exists())) {
103
+ return new Response('Not Found', {
104
+ status: 404,
105
+ headers: BUNDLING_CORS_HEADERS,
106
+ })
107
+ }
108
+
109
+ const buildResult = await Bun.build({
110
+ entrypoints: [resolved],
111
+ target: 'browser',
112
+ minify: Bun.env.NODE_ENV === 'production',
113
+ splitting: false,
114
+ format: 'esm',
115
+ sourcemap: Bun.env.NODE_ENV === 'production' ? 'none' : 'inline',
116
+ jsx: { importSource: 'remix/component' },
117
+ })
118
+
119
+ if (!buildResult.success) {
120
+ const errorMessage = buildResult.logs
121
+ .map((log) => log.message)
122
+ .join('\n')
123
+ return new Response(errorMessage || 'Build failed', {
124
+ status: 500,
125
+ headers: {
126
+ 'Content-Type': 'text/plain',
127
+ ...BUNDLING_CORS_HEADERS,
128
+ },
129
+ })
130
+ }
131
+
132
+ const output = buildResult.outputs[0]
133
+ return new Response(output, {
134
+ headers: {
135
+ 'Content-Type': 'application/javascript',
136
+ 'Cache-Control':
137
+ Bun.env.NODE_ENV === 'production'
138
+ ? 'public, max-age=31536000, immutable'
139
+ : 'no-cache',
140
+ ...BUNDLING_CORS_HEADERS,
141
+ },
142
+ })
143
+ },
144
+
145
+ '/node_modules/*': async (request: Request) => {
146
+ if (request.method === 'OPTIONS') {
147
+ return new Response(null, {
148
+ status: 204,
149
+ headers: {
150
+ ...BUNDLING_CORS_HEADERS,
151
+ 'Access-Control-Max-Age': '86400',
152
+ },
153
+ })
154
+ }
155
+
156
+ const url = new URL(request.url)
157
+ const specifier = url.pathname.replace('/node_modules/', '')
158
+ const filepath = await resolvePackageExport(specifier, rootDir)
159
+
160
+ if (!filepath) {
161
+ return new Response('Package not found', {
162
+ status: 404,
163
+ headers: BUNDLING_CORS_HEADERS,
164
+ })
165
+ }
166
+
167
+ const nodeModulesDir = path.resolve(rootDir, 'node_modules')
168
+ if (!filepath.startsWith(nodeModulesDir + path.sep)) {
169
+ return new Response('Forbidden', {
170
+ status: 403,
171
+ headers: BUNDLING_CORS_HEADERS,
172
+ })
173
+ }
174
+
175
+ const buildResult = await Bun.build({
176
+ entrypoints: [filepath],
177
+ target: 'browser',
178
+ minify: Bun.env.NODE_ENV === 'production',
179
+ splitting: false,
180
+ format: 'esm',
181
+ sourcemap: Bun.env.NODE_ENV === 'production' ? 'none' : 'inline',
182
+ })
183
+
184
+ if (!buildResult.success) {
185
+ const errorMessage = buildResult.logs
186
+ .map((log) => log.message)
187
+ .join('\n')
188
+ return new Response(errorMessage || 'Build failed', {
189
+ status: 500,
190
+ headers: {
191
+ 'Content-Type': 'text/plain',
192
+ ...BUNDLING_CORS_HEADERS,
193
+ },
194
+ })
195
+ }
196
+
197
+ const output = buildResult.outputs[0]
198
+ return new Response(output, {
199
+ headers: {
200
+ 'Content-Type': 'application/javascript',
201
+ 'Cache-Control':
202
+ Bun.env.NODE_ENV === 'production'
203
+ ? 'public, max-age=31536000, immutable'
204
+ : 'no-cache',
205
+ ...BUNDLING_CORS_HEADERS,
206
+ },
207
+ })
208
+ },
209
+ }
210
+ }