1+ import type { Loader } from 'astro/loaders'
2+ import fs from 'node:fs'
3+ import path from 'node:path'
4+
5+ type ExampleEntry = {
6+ id : string
7+ title : string
8+ description : string
9+ prerequisites : string
10+ code : string
11+ category : string
12+ categoryLabel : string
13+ order : number
14+ filename : string
15+ runCommand : string
16+ }
17+
18+ interface CategoryMeta {
19+ label : string
20+ description : string
21+ slug : string
22+ }
23+
24+ const CATEGORIES : Record < string , CategoryMeta > = {
25+ abi : {
26+ label : 'ABI Encoding' ,
27+ description : 'ABI type parsing, encoding, and decoding following the ARC-4 specification.' ,
28+ slug : 'abi' ,
29+ } ,
30+ algo25 : {
31+ label : 'Mnemonic Utilities' ,
32+ description : 'Mnemonic and seed conversion utilities following the Algorand 25-word mnemonic standard.' ,
33+ slug : 'algo25' ,
34+ } ,
35+ algod_client : {
36+ label : 'Algod Client' ,
37+ description : 'Algorand node operations and queries using the AlgodClient.' ,
38+ slug : 'algod-client' ,
39+ } ,
40+ algorand_client : {
41+ label : 'Algorand Client' ,
42+ description : 'High-level AlgorandClient API for simplified blockchain interactions.' ,
43+ slug : 'algorand-client' ,
44+ } ,
45+ common : {
46+ label : 'Common Utilities' ,
47+ description : 'Utility functions and helpers.' ,
48+ slug : 'common' ,
49+ } ,
50+ indexer_client : {
51+ label : 'Indexer Client' ,
52+ description : 'Blockchain data queries using the IndexerClient.' ,
53+ slug : 'indexer-client' ,
54+ } ,
55+ kmd_client : {
56+ label : 'KMD Client' ,
57+ description : 'Key Management Daemon operations for wallet and key management.' ,
58+ slug : 'kmd-client' ,
59+ } ,
60+ testing : {
61+ label : 'Testing' ,
62+ description : 'Testing utilities for mock server setup and Vitest integration.' ,
63+ slug : 'testing' ,
64+ } ,
65+ transact : {
66+ label : 'Transactions' ,
67+ description : 'Low-level transaction construction and signing.' ,
68+ slug : 'transact' ,
69+ } ,
70+ }
71+
72+ function lineSeparator ( text : string , isBullet : boolean , lastWasBullet : boolean ) : string {
73+ if ( ! text ) return ''
74+ if ( isBullet || lastWasBullet ) return '\n'
75+ return ' '
76+ }
77+
78+ function parseJSDoc ( content : string ) : { title : string ; description : string ; prerequisites : string } {
79+ const jsdocMatch = content . match ( / \/ \* \* \n ( [ \s \S ] * ?) \* \/ / )
80+
81+ if ( ! jsdocMatch ) {
82+ return { title : 'Example' , description : '' , prerequisites : '' }
83+ }
84+
85+ const jsdocContent = jsdocMatch [ 1 ]
86+
87+ // Extract title from "Example: Title" line
88+ const titleMatch = jsdocContent . match ( / \* \s * E x a m p l e : \s * ( .+ ) / )
89+ const title = titleMatch ?. [ 1 ] ?. trim ( ) || 'Example'
90+
91+ // Extract description - lines after title until Prerequisites or end
92+ const lines = jsdocContent . split ( '\n' ) . map ( ( line ) => line . replace ( / ^ \s * \* \s ? / , '' ) . trim ( ) )
93+
94+ let description = ''
95+ let prerequisites = ''
96+ let inPrerequisites = false
97+ let lastLineWasBullet = false
98+
99+ for ( const line of lines ) {
100+ if ( line . startsWith ( 'Example:' ) ) continue
101+
102+ if ( line . toLowerCase ( ) . startsWith ( 'prerequisites:' ) || line . toLowerCase ( ) === 'prerequisites' ) {
103+ inPrerequisites = true
104+ lastLineWasBullet = false
105+ const prereqContent = line . replace ( / p r e r e q u i s i t e s : ? \s * / i, '' ) . trim ( )
106+ if ( prereqContent ) prerequisites = prereqContent
107+ continue
108+ }
109+
110+ if ( line . startsWith ( '@' ) ) continue
111+
112+ if ( ! line ) {
113+ lastLineWasBullet = false
114+ if ( inPrerequisites ) {
115+ if ( prerequisites ) prerequisites += '\n'
116+ } else if ( description ) {
117+ description += '\n'
118+ }
119+ continue
120+ }
121+
122+ const isBullet = line . startsWith ( '-' ) || line . startsWith ( '•' )
123+ if ( inPrerequisites ) {
124+ prerequisites += lineSeparator ( prerequisites , isBullet , lastLineWasBullet ) + line
125+ } else {
126+ description += lineSeparator ( description , isBullet , lastLineWasBullet ) + line
127+ }
128+ lastLineWasBullet = isBullet
129+ }
130+
131+ return {
132+ title,
133+ description : description . trim ( ) ,
134+ prerequisites : prerequisites . trim ( ) || 'LocalNet running (`algokit localnet start`)' ,
135+ }
136+ }
137+
138+ /**
139+ * Extract order number from filename (e.g., "01-example.ts" -> 1)
140+ */
141+ function extractOrder ( filename : string ) : number {
142+ const match = filename . match ( / ^ ( \d + ) - / )
143+ return match ? parseInt ( match [ 1 ] , 10 ) : 999
144+ }
145+
146+ function createSlug ( filename : string ) : string {
147+ return filename . replace ( / \. t s $ / , '' ) . replace ( / _ / g, '-' )
148+ }
149+
150+ export function examplesLoader ( ) : Loader {
151+ return {
152+ name : 'examples-loader' ,
153+ load : async ( { store, logger } ) => {
154+ const examplesDir = path . resolve ( process . cwd ( ) , '..' , 'examples' )
155+
156+ logger . info ( `Loading examples from ${ examplesDir } ` )
157+
158+ if ( ! fs . existsSync ( examplesDir ) ) {
159+ logger . error ( `Examples directory not found: ${ examplesDir } ` )
160+ return
161+ }
162+
163+ const entries : ExampleEntry [ ] = [ ]
164+
165+ for ( const [ categoryDir , meta ] of Object . entries ( CATEGORIES ) ) {
166+ const categoryPath = path . join ( examplesDir , categoryDir )
167+
168+ if ( ! fs . existsSync ( categoryPath ) ) {
169+ logger . warn ( `Category directory not found: ${ categoryPath } ` )
170+ continue
171+ }
172+
173+ const files = fs . readdirSync ( categoryPath ) . filter ( ( f ) => f . endsWith ( '.ts' ) && ! f . startsWith ( '_' ) )
174+
175+ for ( const filename of files ) {
176+ const filePath = path . join ( categoryPath , filename )
177+ const content = fs . readFileSync ( filePath , 'utf-8' )
178+ const { title, description, prerequisites } = parseJSDoc ( content )
179+ const order = extractOrder ( filename )
180+ const slug = createSlug ( filename )
181+
182+ const entry : ExampleEntry = {
183+ id : `${ meta . slug } /${ slug } ` ,
184+ title,
185+ description,
186+ prerequisites,
187+ code : content ,
188+ category : categoryDir ,
189+ categoryLabel : meta . label ,
190+ order,
191+ filename,
192+ runCommand : `npm run example ${ categoryDir } /${ filename } ` ,
193+ }
194+
195+ entries . push ( entry )
196+ }
197+ }
198+
199+ entries . sort ( ( a , b ) => {
200+ if ( a . category !== b . category ) {
201+ return a . category . localeCompare ( b . category )
202+ }
203+ return a . order - b . order
204+ } )
205+
206+ logger . info ( `Found ${ entries . length } examples across ${ Object . keys ( CATEGORIES ) . length } categories` )
207+
208+ for ( const entry of entries ) {
209+ store . set ( {
210+ id : entry . id ,
211+ data : entry ,
212+ } )
213+ }
214+ } ,
215+ }
216+ }
217+
218+ export { CATEGORIES }
219+ export type { ExampleEntry , CategoryMeta }
0 commit comments