@@ -3,14 +3,71 @@ import yaml from "js-yaml"
33
44import type { GitLabCi } from "./"
55
6+ // Wrapper class to mark arrays that should be rendered in flow style
7+ class FlowArray < T = unknown > extends Array < T > {
8+ constructor ( ...items : T [ ] ) {
9+ super ( )
10+ this . push ( ...items )
11+ }
12+ }
13+
14+ // Custom YAML type for !reference tags
15+ const referenceTag = new yaml . Type ( "!reference" , {
16+ kind : "sequence" ,
17+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
18+ construct : ( data ) => data ,
19+ predicate : ( obj ) => {
20+ return obj instanceof FlowArray
21+ } ,
22+ represent : ( obj : unknown ) => {
23+ return obj
24+ } ,
25+ instanceOf : FlowArray ,
26+ } )
27+
28+ const CUSTOM_SCHEMA = yaml . DEFAULT_SCHEMA . extend ( { explicit : [ referenceTag ] } )
29+
30+ /**
31+ * Process a value to convert !reference strings to proper arrays
32+ */
33+ function processReferences ( value : unknown ) : unknown {
34+ if ( typeof value === "string" && value . startsWith ( "!reference [" ) ) {
35+ // Parse "!reference [.template, script]" into FlowArray format
36+ const match = / ^ ! r e f e r e n c e \s * \[ ( [ ^ \] ] + ) \] $ / . exec ( value )
37+ if ( match ?. [ 1 ] ) {
38+ const parts = match [ 1 ] . split ( "," ) . map ( ( s ) => s . trim ( ) )
39+ if ( parts . length === 2 ) {
40+ return new FlowArray ( ...parts )
41+ }
42+ }
43+ }
44+
45+ if ( Array . isArray ( value ) ) {
46+ return value . map ( processReferences )
47+ }
48+
49+ if ( value && typeof value === "object" ) {
50+ const result : Record < string , unknown > = { }
51+ for ( const [ key , val ] of Object . entries ( value ) ) {
52+ result [ key ] = processReferences ( val )
53+ }
54+ return result
55+ }
56+
57+ return value
58+ }
59+
660/**
761 * Convert a plain `GitLabCi` object to a YAML string.
862 *
963 * @param config - The YAML-serializable `GitLabCi` object produced by `getPlainObject()`.
1064 * @returns YAML string representation of the pipeline.
1165 */
1266export function toYaml ( config : GitLabCi ) {
13- const { jobs, ...rest } = config
67+ // Process references before serialization
68+ const processed = processReferences ( config ) as GitLabCi
69+
70+ const { jobs, ...rest } = processed
1471
1572 // Define preferred order for top-level keys
1673 const keyOrder = [
@@ -54,15 +111,64 @@ export function toYaml(config: GitLabCi) {
54111 }
55112 }
56113
57- const yamlString = yaml . dump ( ordered , { noRefs : true , sortKeys : false , lineWidth : - 1 } )
114+ const yamlString = yaml . dump ( ordered , {
115+ noRefs : true ,
116+ sortKeys : false ,
117+ lineWidth : - 1 ,
118+ schema : CUSTOM_SCHEMA ,
119+ } )
58120
59- // Add blank lines between top-level sections for better readability
121+ // Post-process to convert multiline !reference to inline format
60122 const lines = yamlString . split ( "\n" )
61123 const resultLines : string [ ] = [ ]
124+ let i = 0
125+
126+ while ( i < lines . length ) {
127+ const line = lines [ i ]
128+
129+ // Check if this line contains a multiline !reference tag
130+ if ( line && line . trim ( ) === "- !reference" ) {
131+ // Next two lines should contain the array elements
132+ const nextLine1 = lines [ i + 1 ]
133+ const nextLine2 = lines [ i + 2 ]
134+
135+ if (
136+ nextLine1 &&
137+ nextLine2 &&
138+ nextLine1 . trim ( ) . startsWith ( "- " ) &&
139+ nextLine2 . trim ( ) . startsWith ( "- " )
140+ ) {
141+ const elem1 = nextLine1 . trim ( ) . slice ( 2 )
142+ const elem2 = nextLine2 . trim ( ) . slice ( 2 )
143+
144+ // Get the indentation from the original "- !reference" line
145+ const match = / ^ ( \s * ) / . exec ( line )
146+ const indent = match ?. [ 1 ] ?? ""
147+
148+ // Create inline format
149+ resultLines . push ( `${ indent } - !reference [${ elem1 } , ${ elem2 } ]` )
150+
151+ // Skip the next two lines
152+ i += 3
153+ continue
154+ }
155+ }
156+
157+ if ( line !== undefined ) {
158+ resultLines . push ( line )
159+ }
160+ i ++
161+ }
162+
163+ const processedYaml = resultLines . join ( "\n" )
164+
165+ // Add blank lines between top-level sections for better readability
166+ const finalLines = processedYaml . split ( "\n" )
167+ const outputLines : string [ ] = [ ]
62168 let lastTopLevelKey : string | null = null
63169 let previousLineWasValue = false
64170
65- for ( const line of lines ) {
171+ for ( const line of finalLines ) {
66172 const trimmed = line . trim ( )
67173
68174 // Check if this is a top-level key (no indentation and ends with :)
@@ -83,10 +189,10 @@ export function toYaml(config: GitLabCi) {
83189 previousLineWasValue = true
84190 }
85191
86- resultLines . push ( line )
192+ outputLines . push ( line )
87193 }
88194
89- return resultLines . join ( "\n" )
195+ return outputLines . join ( "\n" )
90196}
91197
92198/**
0 commit comments