@@ -3,23 +3,38 @@ import type { ReactiveArgs } from '~/types'
33import { Buffer } from 'node:buffer'
44import { access , mkdir , readFile , writeFile } from 'node:fs/promises'
55import { defu } from 'defu'
6- import { dirname } from 'pathe'
6+ import { dirname , extname } from 'pathe'
7+ import { parse as yamlParse , stringify as yamlStringify } from 'yaml'
78import { logger } from './logger'
89
910export type FileOutputState = ReactiveArgs < {
1011 filePath : string
1112 data : string
12- mergeResult : undefined | string
13- mergeType : 'json' | 'concat'
13+ /**
14+ * This property is auto-populated for deep merge-able files (json, yaml) in hooks like `onFileOutputDeepMerge`, the user can also set it manually on prior hooks.
15+ */
16+ parsedData ?: Record < string , unknown > | undefined
17+ mergeResult ?: string | undefined
18+ mergeType : 'json' | 'yaml' | 'concat' | 'custom'
1419 isValidFileToMerge : boolean
20+ /**
21+ * A custom id for the file, used for advanced hook usecase like `custom` merge
22+ */
23+ fid ?: string
1524} >
1625
1726export interface FileOutputHooks extends Hooks {
1827 onFileOutput : ( state : FileOutputState ) => void | Promise < void >
1928
20- onFileOutputJsonMerge : ( state : FileOutputState ) => void | Promise < void >
29+ onFileOutputCustomMerge : ( state : FileOutputState ) => void | Promise < void >
2130
2231 onFileOutputConcatMerge : ( state : FileOutputState ) => void | Promise < void >
32+
33+ onFileOutputDeepMerge : ( state : FileOutputState ) => void | Promise < void >
34+
35+ onFileOutputJsonMerge : ( state : FileOutputState ) => void | Promise < void >
36+
37+ onFileOutputYamlMerge : ( state : FileOutputState ) => void | Promise < void >
2338}
2439
2540export interface FileOutputOptions {
@@ -32,11 +47,11 @@ export interface FileOutputOptions {
3247 * Control existing file content merging policy.
3348 *
3449 * Options:
35- * + `'json '`: perform deep merge for json files only.
50+ * + `'deep '`: perform deep merge supported files only (json, yaml) .
3651 * + `true`: concatenate data of all files (json are still deep-merged).
3752 *
3853 */
39- mergeContent ?: boolean | 'json '
54+ mergeContent ?: boolean | 'deep '
4055}
4156
4257/**
@@ -52,12 +67,12 @@ export async function fileOutput(filePath: string, data: string, options?: FileO
5267 mergeContent,
5368 } = options ?? { }
5469
70+ const mergeType = _fileMergeType ( filePath )
5571 const state : FileOutputState = {
5672 filePath,
5773 data,
58- mergeResult : undefined as undefined | string ,
59- mergeType : filePath . endsWith ( '.json' ) ? 'json' : 'concat' ,
60- isValidFileToMerge : mergeContent === true || ( mergeContent === 'json' && filePath . endsWith ( '.json' ) ) ,
74+ mergeType,
75+ isValidFileToMerge : mergeContent === true || ( mergeContent === 'deep' && mergeType !== 'concat' ) ,
6176 }
6277
6378 if ( hookable )
@@ -71,25 +86,59 @@ export async function fileOutput(filePath: string, data: string, options?: FileO
7186 logger . info ( `Merging file "${ state . filePath } "...` )
7287
7388 switch ( state . mergeType ) {
89+ case 'custom' : {
90+ if ( ! hookable )
91+ throw new Error ( 'Expect `hookable` to be provided when mergeType is "custom"' )
92+
93+ await hookable . callHook ( 'onFileOutputCustomMerge' , state )
94+
95+ if ( ! state . mergeResult )
96+ throw new Error ( 'Expect `mergeResult` to be provided when mergeType is "custom"' )
97+
98+ state . data = state . mergeResult
99+ break
100+ }
101+ case 'concat' : {
102+ if ( hookable )
103+ await hookable . callHook ( 'onFileOutputConcatMerge' , state )
104+
105+ state . data = state . mergeResult ?? Buffer . concat ( [ await readFile ( state . filePath ) , Buffer . from ( state . data ) ] ) . toString ( )
106+ break
107+ }
74108 case 'json' : {
75109 if ( typeof state . data !== 'string' )
76110 throw new Error ( 'Please provide `data` as a JSON stringified object' )
77111
78- if ( hookable )
112+ state . parsedData = JSON . parse ( state . data )
113+
114+ if ( hookable ) {
115+ await hookable . callHook ( 'onFileOutputDeepMerge' , state )
79116 await hookable . callHook ( 'onFileOutputJsonMerge' , state )
117+ }
80118
81119 state . data = state . mergeResult ?? JSON . stringify (
82- defu ( JSON . parse ( state . data ) , JSON . parse ( await readFile ( state . filePath , 'utf-8' ) ) ) ,
120+ defu ( state . parsedData , JSON . parse ( await readFile ( state . filePath , 'utf-8' ) ) ) ,
83121 undefined ,
84122 2 ,
85123 )
86124 break
87125 }
88- case 'concat ' : {
89- if ( hookable )
90- await hookable . callHook ( 'onFileOutputConcatMerge' , state )
126+ case 'yaml ' : {
127+ if ( typeof state . data !== 'string' )
128+ throw new Error ( 'Please provide `data` as a YAML stringified object' )
91129
92- state . data = state . mergeResult ?? Buffer . concat ( [ await readFile ( state . filePath ) , Buffer . from ( state . data ) ] ) . toString ( )
130+ state . parsedData = yamlParse ( state . data )
131+
132+ if ( hookable ) {
133+ await hookable . callHook ( 'onFileOutputDeepMerge' , state )
134+ await hookable . callHook ( 'onFileOutputYamlMerge' , state )
135+ }
136+
137+ state . data = state . mergeResult ?? yamlStringify (
138+ defu ( state . parsedData , yamlParse ( await readFile ( state . filePath , 'utf-8' ) ) ) ,
139+ undefined ,
140+ 2 ,
141+ )
93142 break
94143 }
95144 default :
@@ -100,3 +149,17 @@ export async function fileOutput(filePath: string, data: string, options?: FileO
100149 // Write the file
101150 return await writeFile ( state . filePath , state . data )
102151}
152+
153+ function _fileMergeType ( filePath : string ) {
154+ const extension = extname ( filePath )
155+
156+ switch ( extension ) {
157+ case '.json' :
158+ return 'json'
159+ case '.yaml' :
160+ case '.yml' :
161+ return 'yaml'
162+ default :
163+ return 'concat'
164+ }
165+ }
0 commit comments