@@ -90,30 +90,25 @@ const CODEGEN_TEMPLATE_ID_BY_LANGUAGE: Readonly<Record<string, string>> = {
9090// languageSettings entry for http must exist or the portal widget fails to render.
9191const HTTP_TEMPLATE_ID = "http_curl_v1" as const ;
9292
93- // Deep clone used for copy-on-write transforms. The config is plain JSON, so a JSON
94- // round-trip is an exact, dependency-free copy.
95- function clone < T > ( value : T ) : T {
96- return JSON . parse ( JSON . stringify ( value ) ) as T ;
97- }
98-
9993// Immutable per-language portal setting. Build new values via the static factory and
10094// the with* transforms; the wrapped data is never mutated after construction.
10195export class LanguageSetting {
10296 private constructor ( private readonly data : LanguageSettingData ) { }
10397
10498 public static from ( data : LanguageSettingData = { } ) : LanguageSetting {
105- return new LanguageSetting ( clone ( data ) ) ;
99+ return new LanguageSetting ( data ) ;
106100 }
107101
108102 /** Returns a copy with all AI editor integrations (Cursor/Claude Code/VS Code) enabled. */
109103 public withAiIntegrationsEnabled ( ) : LanguageSetting {
110- const data = clone ( this . data ) ;
111- data . aiIntegration = {
112- cursor : { isEnabled : true } ,
113- claudeCode : { isEnabled : true } ,
114- vscode : { isEnabled : true }
115- } ;
116- return new LanguageSetting ( data ) ;
104+ return new LanguageSetting ( {
105+ ...this . data ,
106+ aiIntegration : {
107+ cursor : { isEnabled : true } ,
108+ claudeCode : { isEnabled : true } ,
109+ vscode : { isEnabled : true }
110+ }
111+ } ) ;
117112 }
118113
119114 public toJSON ( ) : LanguageSettingData {
@@ -127,22 +122,19 @@ export class PortalSettings {
127122 private constructor ( private readonly data : PortalSettingsData ) { }
128123
129124 public static from ( data : PortalSettingsData = { } ) : PortalSettings {
130- return new PortalSettings ( clone ( data ) ) ;
125+ return new PortalSettings ( data ) ;
131126 }
132127
133128 /** Returns a copy with the API-call base URL set. */
134129 public withBaseUrl ( baseUrl : UrlPath ) : PortalSettings {
135- const data = clone ( this . data ) ;
136- data . baseUrl = baseUrl . toString ( ) ;
137- return new PortalSettings ( data ) ;
130+ return new PortalSettings ( { ...this . data , baseUrl : baseUrl . toString ( ) } ) ;
138131 }
139132
140133 // Returns a copy with AI editor integrations enabled for the given SDK languages
141134 // (friendly ids). Always adds the http entry the portal needs to render and opens the
142135 // portal on the first SDK language. Languages without a codegen template id are skipped.
143136 public withAiIntegrations ( languages : string [ ] ) : PortalSettings {
144- const data = clone ( this . data ) ;
145- const languageSettings : { [ language : string ] : LanguageSettingData } = { ...data . languageSettings } ;
137+ const languageSettings : { [ language : string ] : LanguageSettingData } = { ...this . data . languageSettings } ;
146138 languageSettings [ HTTP_TEMPLATE_ID ] = languageSettings [ HTTP_TEMPLATE_ID ] ?? { } ;
147139
148140 let firstSdkTemplateId : string | undefined ;
@@ -155,11 +147,11 @@ export class PortalSettings {
155147 languageSettings [ templateId ] = LanguageSetting . from ( languageSettings [ templateId ] ) . withAiIntegrationsEnabled ( ) . toJSON ( ) ;
156148 }
157149
158- data . languageSettings = languageSettings ;
159- if ( firstSdkTemplateId ) {
160- data . initialPlatform = firstSdkTemplateId ;
161- }
162- return new PortalSettings ( data ) ;
150+ return new PortalSettings ( {
151+ ... this . data ,
152+ languageSettings ,
153+ ... ( firstSdkTemplateId ? { initialPlatform : firstSdkTemplateId } : { } )
154+ } ) ;
163155 }
164156
165157 public toJSON ( ) : PortalSettingsData {
@@ -205,30 +197,34 @@ export class BuildConfig {
205197
206198 /** Returns a copy with the portal's languageConfig set from the selected friendly language ids. */
207199 public withPortalLanguages ( languages : string [ ] ) : BuildConfig {
208- const data = clone ( this . data ) ;
209- data . generatePortal ! . languageConfig = getLanguagesConfig ( languages ) ;
210- return new BuildConfig ( data ) ;
200+ const portal = this . data . generatePortal ! ;
201+ return new BuildConfig ( {
202+ ...this . data ,
203+ generatePortal : { ...portal , languageConfig : getLanguagesConfig ( languages ) }
204+ } ) ;
211205 }
212206
213207 /** Returns a copy with the API Copilot configuration set (or overwritten). */
214208 public withApiCopilotConfig ( config : CopilotConfig ) : BuildConfig {
215- const data = clone ( this . data ) ;
216- data . apiCopilotConfig = { ...config } ;
217- return new BuildConfig ( data ) ;
209+ return new BuildConfig ( { ...this . data , apiCopilotConfig : { ...config } } ) ;
218210 }
219211
220212 // Returns a copy with API Copilot enabled for a locally-served portal: stores the
221213 // Copilot config, points the portal base URL at the local serve URL, and turns on AI
222214 // editor integrations for the configured SDK languages.
223215 public withApiCopilotForPortal ( key : string , welcomeMessage : string , baseUrl : string ) : BuildConfig {
224- const data = clone ( this . data ) ;
225- const portal = data . generatePortal ! ;
226- data . apiCopilotConfig = { isEnabled : true , key, welcomeMessage } ;
227- portal . baseUrl = baseUrl ;
228- portal . portalSettings = PortalSettings . from ( portal . portalSettings )
229- . withAiIntegrations ( Object . keys ( portal . languageConfig ) )
230- . toJSON ( ) ;
231- return new BuildConfig ( data ) ;
216+ const portal = this . data . generatePortal ! ;
217+ return new BuildConfig ( {
218+ ...this . data ,
219+ apiCopilotConfig : { isEnabled : true , key, welcomeMessage } ,
220+ generatePortal : {
221+ ...portal ,
222+ baseUrl,
223+ portalSettings : PortalSettings . from ( portal . portalSettings )
224+ . withAiIntegrations ( Object . keys ( portal . languageConfig ) )
225+ . toJSON ( )
226+ }
227+ } ) ;
232228 }
233229
234230 public updateBuildConfigBaseUrl ( serveUrl : UrlPath ) : Result < BuildConfig , "unchanged" > {
@@ -245,29 +241,22 @@ export class BuildConfig {
245241 return err ( "unchanged" ) ;
246242 }
247243
248- const data = clone ( this . data ) ;
249- const portal = data . generatePortal ! ;
250- if ( portal . portalSettings ?. baseUrl ) {
251- portal . portalSettings = PortalSettings . from ( portal . portalSettings ) . withBaseUrl ( serveUrl ) . toJSON ( ) ;
252- } else {
253- portal . baseUrl = serveUrl . toString ( ) ;
254- }
255- return ok ( new BuildConfig ( data ) ) ;
244+ const portal = this . data . generatePortal ! ;
245+ const updatedPortal : PortalConfig = portal . portalSettings ?. baseUrl
246+ ? { ...portal , portalSettings : PortalSettings . from ( portal . portalSettings ) . withBaseUrl ( serveUrl ) . toJSON ( ) }
247+ : { ...portal , baseUrl : serveUrl . toString ( ) } ;
248+ return ok ( new BuildConfig ( { ...this . data , generatePortal : updatedPortal } ) ) ;
256249 }
257250
258251 // Returns a copy with a recipe workflow added (or replaced, matched by permalink).
259252 public withRecipeWorkflow ( name : string , functionName : string , scriptPath : string ) : BuildConfig {
260- const data = clone ( this . data ) ;
261- const recipes = ( data . recipes ??= { } ) ;
262- const workflows = ( recipes . workflows ??= [ ] ) ;
253+ const recipes = this . data . recipes ?? { } ;
254+ const workflows = recipes . workflows ?? [ ] ;
263255 const permalink = `page:recipes/${ functionName } ` ;
264256 const workflow : RecipeWorkflow = { name, permalink, functionName, scriptPath } ;
265257 const existingIndex = workflows . findIndex ( ( w ) => w . permalink === permalink ) ;
266- if ( existingIndex === - 1 ) {
267- workflows . push ( workflow ) ;
268- } else {
269- workflows [ existingIndex ] = workflow ;
270- }
271- return new BuildConfig ( data ) ;
258+ const updatedWorkflows =
259+ existingIndex === - 1 ? [ ...workflows , workflow ] : workflows . map ( ( w , index ) => ( index === existingIndex ? workflow : w ) ) ;
260+ return new BuildConfig ( { ...this . data , recipes : { ...recipes , workflows : updatedWorkflows } } ) ;
272261 }
273262}
0 commit comments