1- import { Effect } from 'effect'
1+ import { Effect , Stream } from 'effect'
22import {
33 chunk as chunkInternal ,
44 streamChunks as streamChunksInternal ,
@@ -7,7 +7,13 @@ import { extractEntities } from './extract'
77import { parseCode } from './parser'
88import { detectLanguage } from './parser/languages'
99import { buildScopeTree } from './scope'
10- import type { Chunk , ChunkOptions , Language } from './types'
10+ import type {
11+ Chunk ,
12+ ChunkOptions ,
13+ Language ,
14+ ParseResult ,
15+ ScopeTree ,
16+ } from './types'
1117
1218/**
1319 * Error thrown when chunking fails
@@ -138,6 +144,114 @@ export async function chunk(
138144 return Effect . runPromise ( chunkEffect ( filepath , code , options ) )
139145}
140146
147+ /**
148+ * Prepare the chunking pipeline (parse, extract, build scope tree)
149+ * Returns the parsed result and scope tree needed for chunking
150+ */
151+ const prepareChunking = (
152+ filepath : string ,
153+ code : string ,
154+ options ?: ChunkOptions ,
155+ ) : Effect . Effect <
156+ { parseResult : ParseResult ; scopeTree : ScopeTree ; language : Language } ,
157+ ChunkingError | UnsupportedLanguageError
158+ > => {
159+ return Effect . gen ( function * ( ) {
160+ // Step 1: Detect language (or use override)
161+ const language : Language | null =
162+ options ?. language ?? detectLanguage ( filepath )
163+
164+ if ( ! language ) {
165+ return yield * Effect . fail ( new UnsupportedLanguageError ( filepath ) )
166+ }
167+
168+ // Step 2: Parse the code
169+ const parseResult = yield * Effect . tryPromise ( {
170+ try : ( ) => parseCode ( code , language ) ,
171+ catch : ( error : unknown ) =>
172+ new ChunkingError ( 'Failed to parse code' , error ) ,
173+ } )
174+
175+ // Step 3: Extract entities from AST
176+ const entities = yield * Effect . mapError (
177+ extractEntities ( parseResult . tree . rootNode , language , code ) ,
178+ ( error : unknown ) =>
179+ new ChunkingError ( 'Failed to extract entities' , error ) ,
180+ )
181+
182+ // Step 4: Build scope tree
183+ const scopeTree = yield * Effect . mapError (
184+ buildScopeTree ( entities ) ,
185+ ( error : unknown ) =>
186+ new ChunkingError ( 'Failed to build scope tree' , error ) ,
187+ )
188+
189+ return { parseResult, scopeTree, language }
190+ } )
191+ }
192+
193+ /**
194+ * Create an Effect Stream that yields chunks
195+ *
196+ * This is the Effect-native streaming API. Use this if you're working
197+ * within the Effect ecosystem and want full composability.
198+ *
199+ * @param filepath - The file path (used for language detection)
200+ * @param code - The source code to chunk
201+ * @param options - Optional chunking configuration
202+ * @returns Effect Stream of chunks with context
203+ *
204+ * @example
205+ * ```ts
206+ * import { chunkStreamEffect } from 'astchunk'
207+ * import { Effect, Stream } from 'effect'
208+ *
209+ * const program = Stream.runForEach(
210+ * chunkStreamEffect('src/utils.ts', sourceCode),
211+ * (chunk) => Effect.log(chunk.text)
212+ * )
213+ *
214+ * Effect.runPromise(program)
215+ * ```
216+ */
217+ export const chunkStreamEffect = (
218+ filepath : string ,
219+ code : string ,
220+ options ?: ChunkOptions ,
221+ ) : Stream . Stream < Chunk , ChunkingError | UnsupportedLanguageError > => {
222+ return Stream . unwrap (
223+ Effect . map ( prepareChunking ( filepath , code , options ) , ( prepared ) => {
224+ const { parseResult, scopeTree, language } = prepared
225+
226+ // Create stream from the internal generator
227+ return Stream . fromAsyncIterable (
228+ streamChunksInternal (
229+ parseResult . tree . rootNode ,
230+ code ,
231+ scopeTree ,
232+ language ,
233+ options ,
234+ filepath ,
235+ ) ,
236+ ( error ) => new ChunkingError ( 'Stream iteration failed' , error ) ,
237+ ) . pipe (
238+ // Attach parse error to chunks if present
239+ Stream . map ( ( chunk ) =>
240+ parseResult . error
241+ ? {
242+ ...chunk ,
243+ context : {
244+ ...chunk . context ,
245+ parseError : parseResult . error ,
246+ } ,
247+ }
248+ : chunk ,
249+ ) ,
250+ )
251+ } ) ,
252+ )
253+ }
254+
141255/**
142256 * Stream source code chunks as they are generated
143257 *
@@ -154,9 +268,9 @@ export async function chunk(
154268 *
155269 * @example
156270 * ```ts
157- * import { stream } from 'astchunk'
271+ * import { chunkStream } from 'astchunk'
158272 *
159- * for await (const chunk of stream ('src/utils.ts', sourceCode)) {
273+ * for await (const chunk of chunkStream ('src/utils.ts', sourceCode)) {
160274 * console.log(chunk.text, chunk.context)
161275 * }
162276 * ```
@@ -166,49 +280,14 @@ export async function* chunkStream(
166280 code : string ,
167281 options ?: ChunkOptions ,
168282) : AsyncGenerator < Chunk > {
169- // Detect language (or use override)
170- const language : Language | null =
171- options ?. language ?? detectLanguage ( filepath )
172-
173- if ( ! language ) {
174- throw new UnsupportedLanguageError ( filepath )
175- }
176-
177- // Parse the code
178- let parseResult : Awaited < ReturnType < typeof parseCode > >
179- try {
180- parseResult = await parseCode ( code , language )
181- } catch ( error ) {
182- throw new ChunkingError ( 'Failed to parse code' , error )
183- }
184-
185- // Extract entities from AST
186- let entities : Awaited <
187- ReturnType < typeof extractEntities > extends Effect . Effect < infer A , unknown >
188- ? A
189- : never
190- >
191- try {
192- entities = await Effect . runPromise (
193- extractEntities ( parseResult . tree . rootNode , language , code ) ,
194- )
195- } catch ( error ) {
196- throw new ChunkingError ( 'Failed to extract entities' , error )
197- }
283+ // Prepare the chunking pipeline
284+ const prepared = await Effect . runPromise (
285+ prepareChunking ( filepath , code , options ) ,
286+ )
198287
199- // Build scope tree
200- let scopeTree : Awaited <
201- ReturnType < typeof buildScopeTree > extends Effect . Effect < infer A , unknown >
202- ? A
203- : never
204- >
205- try {
206- scopeTree = await Effect . runPromise ( buildScopeTree ( entities ) )
207- } catch ( error ) {
208- throw new ChunkingError ( 'Failed to build scope tree' , error )
209- }
288+ const { parseResult, scopeTree, language } = prepared
210289
211- // Stream chunks from the internal generator, passing filepath for context
290+ // Stream chunks from the internal generator
212291 const chunkGenerator = streamChunksInternal (
213292 parseResult . tree . rootNode ,
214293 code ,
0 commit comments