LWC Editor using Tooling API

Currently, Salesforce is not allowing the creation or editing of LWC files on the developer console. To make simple changes to LWC files within salesforce without using Visual Studio Code.

Below are the steps I followed to create the component:

  1. Created a connected app for using tooling API within the salesforce.
  2. Created Named credentials with details from step 1.
  3. Created Aura enabled methods in Apex class to get, create or update LWC components.
  4. Created Future methods to overcome timeout error while making a call to tooling API.
  5. Created LWC component to imperatively call apex methods from step 4 and edit or create the same source.
Below is the sample code and screenshots:


class LWCEditor
 
public class LWCEditor {
    @AuraEnabled
    public static String getLWCs() {
        String query='SELECT+Id,Description,DeveloperName,IsExposed,TargetConfigs+FROM+LightningComponentBundle';
        String s = 'services/data/v49.0/tooling/query/?q='+query;
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:LWCToolingAPI/'+s);
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        Http http = new Http();
        HTTPResponse res = http.send(req);
        while (res.getStatusCode() == 302) {
            req.setEndpoint(res.getHeader('Location'));
            res = new Http().send(req);
        }
        System.debug(res.getBody());
        return res.getBody();
    }
    
    @AuraEnabled
    public static String getLWCResource(String bundleId) {
        String query='SELECT+Id,LightningComponentBundleId,Format,FilePath,Source+FROM+LightningComponentResource+WHERE+LightningComponentBundleId=\'' + bundleId +  '\'';
        String s = 'services/data/v49.0/tooling/query/?q='+query;
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:LWCToolingAPI/'+s);
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        Http http = new Http();
        HTTPResponse res = http.send(req);
        while (res.getStatusCode() == 302) {
            req.setEndpoint(res.getHeader('Location'));
            res = new Http().send(req);
        }
        System.debug(res.getBody());
        return res.getBody();
    }
    @AuraEnabled
    public static String getLWCList() {
        //List lwcList = [SELECT Id, DeveloperName, CreatedBy.Name, CreatedDate, LastModifiedDate FROM LightningComponentBundle];
        //system.debug('lwcList : '+lwcList.size());
        return '';
    }
    
    @AuraEnabled
    public static String updateLWCResource(String id,String fpath,String bundleId,String format, String source) {
        //FutureToolClass.createResource(fpath, bundleId, format, source);
        FutureToolClass.updateResource(id,fpath, bundleId, format, source);
        
        return 'update call sent';
        //return '';
    }
    @AuraEnabled
    public static String createLWCResource(String description,String developerName,String isExposed,String includeCSS,List targets,String sourceHTML,String sourceJS,String sourceMETA) {
        //FutureToolClass.createResource(fpath, bundleId, format, source);
        //FutureToolClass.createResource(fpath, bundleId, format, source);
        FutureToolClass.createResource(description, developerName, isExposed, includeCSS, targets,sourceHTML,sourceJS,sourceMETA);
        return 'update call sent';
        //return '';
    }

}

class FutureToolClass
global class FutureToolClass {
    @future(callout=true)
    public static void createResource(String description,String developerName,String isExposed,String includeCSS,List targets,String sourceHTML,String sourceJS,String sourceMETA)
    {
        system.debug(targets);
        String sb = 'services/data/v49.0/tooling/sobjects/LightningComponentBundle';
        HttpRequest reqSB = new HttpRequest(); 
        String trag = '';
        String targForMetafile='';
        for(integer i=0;i'+targets.get(i)+'';
        }
        String targ = '"targets": {"target": ['+trag+']}';
        //String targ = '"targets": {"target": ["lightning__RecordPage","lightning__AppPage","lightning__HomePage"]}';
        
        String bodySB = '{"FullName":"'+developerName+'","Metadata":{"apiVersion":48,"description": null,"isExplicitImport": false,"isExposed": '+isExposed+',"masterLabel": "'+developerName+'",'+targ+'}}';
        system.debug('body : '+bodySB);    
        reqSB.setEndpoint('callout:LWCToolingAPI/'+sb);
        reqSB.setMethod('POST');
        reqSB.setBody(bodySB);
        reqSB.setHeader('Content-Type', 'application/json');
        Http http = new Http();
        HTTPResponse res = http.send(reqSB);
        system.debug(res.getBody());
        //{"id":"0Rb7F000000XzcqSAC","success":true,"errors":[],"warnings":[],"infos":[]}
        JSONParser parser = JSON.createParser(res.getBody());
        System.JSONToken jt;
        String bundleId;
        //system.debug(resMc.getBody());
        while (parser.nextToken() != null) {   
            if(parser.getCurrentToken()==JSONToken.FIELD_NAME && parser.getText() == 'id'){
                jt = parser.nextToken();
            }   
            if(parser.getCurrentToken()==JSONToken.VALUE_STRING && jt== parser.getCurrentToken()){
                bundleId = parser.getText();
            }
        }
        String source;
        String fpath;
        String format;
        
        system.debug('source');
        system.debug(source);
        system.debug('bundleId');
        system.debug(bundleId);
        
        
        //create js file
        //se format
        format = 'js';
        //se format
        //create FilePath
        fpath = 'lwc/'+developerName+'/'+developerName+'.'+format;
        //create FilePath
        //set Dummy Source
        source = sourceJS;
        //source = '"import { LightningElement } from \'lwc\'; \n export default class Empty extends LightningElement { \n }"';
        //set Dummy Source
        String s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource';
        HttpRequest reqJS = new HttpRequest();
        
        String body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}';
        system.debug('body : '+body);    
        reqJS.setEndpoint('callout:LWCToolingAPI/'+s);
        reqJS.setMethod('POST');
        reqJS.setBody(body);
        reqJS.setHeader('Content-Type', 'application/json');
        http = new Http();
        res = http.send(reqJS);
        System.debug(res.getBody());
        //create js file
        //
        
        //create html file
        //create FilePath
        fpath = 'lwc/'+developerName+'/'+developerName+'.html';
        //create FilePath
        //se format
        format = 'html';
        //se format
        //set Dummy Source
        source = sourceHTML;
        //source = '"\n"';
        //set Dummy Source
        s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource';
        HttpRequest reqHtml = new HttpRequest();
        
        body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}';
        system.debug('body : '+body);    
        reqHtml.setEndpoint('callout:LWCToolingAPI/'+s);
        reqHtml.setMethod('POST');
        reqHtml.setBody(body);
        reqHtml.setHeader('Content-Type', 'application/json');
        Http httpH = new Http();
        res = httpH.send(reqHtml);
        System.debug(res.getBody());
        //create html file
        
        //create meta file
        //se format
        format = 'js-meta.xml';
        //se format
        //create FilePath
        fpath = 'lwc/'+developerName+'/'+developerName+'.'+format;
        //create FilePath
        //set Dummy Source
        source = sourceMETA;
        //source = '"\n  \n 50 \n '+isExposed+' \n 	 '+targForMetafile+'\n  \n "';
        //set Dummy Source
        s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource';
        HttpRequest reqMeta = new HttpRequest();
        
        body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}';
        system.debug('body : '+body);    
        reqMeta.setEndpoint('callout:LWCToolingAPI/'+s);
        reqMeta.setMethod('POST');
        reqMeta.setBody(body);
        reqMeta.setHeader('Content-Type', 'application/json');
        http = new Http();
        res = http.send(reqMeta);
        System.debug(res.getBody());
        //create meta file
        if(includeCSS == 'true' || includeCSS == 'TRUE' || includeCSS == 'True'){
            //create css file if needed
        //se format
        format = 'css';
        //se format
        //create FilePath
        fpath = 'lwc/'+developerName+'/'+developerName+'.'+format;
        //create FilePath
        //set Dummy Source
        source = '"#dummy css"';
        //set Dummy Source
        s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource';
        HttpRequest reqCSS = new HttpRequest();
        
        body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}';
        system.debug('body : '+body);    
        reqCSS.setEndpoint('callout:LWCToolingAPI/'+s);
        reqCSS.setMethod('POST');
        reqCSS.setBody(body);
        reqCSS.setHeader('Content-Type', 'application/json');
        http = new Http();
        res = http.send(reqCSS);
        System.debug(res.getBody());
        //create css file if needed
        }
        //return res.getBody();
    }
    @future(callout=true)
    public static void updateResource(String id,String fpath,String bundleId,String format, String source)
    {   
        system.debug('source');
        system.debug(source);
        system.debug('bundleId');
        system.debug(bundleId);
       
        String s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource/'+id;
        HttpRequest req = new HttpRequest();
        
        String body = '{"Source" : ' + source + '}';
        system.debug('body : '+body);    
        req.setEndpoint('callout:LWCToolingAPI/'+s);
        req.setMethod('PATCH');
        req.setBody(body);
        req.setHeader('Content-Type', 'application/json');
        Http http = new Http();
        HTTPResponse res = http.send(req);
        while (res.getStatusCode() == 302) {
            req.setEndpoint(res.getHeader('Location'));
            res = new Http().send(req);
        }
        System.debug(res.getBody());
        //return res.getBody();
    }
}
LWC component LWC Editor html:
<template>
		<lightning-card  title="LWC Editor">
				<lightning-button label="New" slot="actions" variant="brand" onclick={handleNewLWC}></lightning-button>
				<lightning-button label="Save" slot="actions" variant="success"  onclick={handleSaveLWC}></lightning-button>
				
				<div class="c-container">
            <lightning-layout multiple-rows="true">
                <lightning-layout-item padding="around-small" size="12">
                    <lightning-layout>
                        <lightning-layout-item padding="around-small" size="3">
                            <div class="page-section page-right">
                                <h2>Existing Components</h2>
                                <template if:true={listLWCs}>
																		<lightning-accordion class="example-accordion">
																				<template for:each={listLWCs} for:item="bear" >
																						<lightning-accordion-section name={bear.Id} label={bear.DeveloperName} key={bear.Id} onclick={loadResources}>
																								<template if:true={listResources}>
																										<template for:each={listResources} for:item="lr" >
																												<span key={lr.Id}>
																												<p id={lr.Id} onclick={loadSource}>{lr.FilePath}</p><br/>
																												</span>
																										</template>
																								</template>
																						</lightning-accordion-section>																						
																				</template>
																		</lightning-accordion>
																		
																</template>
                            </div>
                        </lightning-layout-item>
                        <lightning-layout-item padding="around-small" size="9">
                            <div class="page-section page-main">
                                <h2>Editor</h2><!--
																<lightning-formatted-rich-text value={source}>
																</lightning-formatted-rich-text>
																123-->
																<textarea id="codeContainer" name="codeContainer" rows="15" cols="100" onblur={updateSource}>
																		{source}
																</textarea><!--
                                <lightning-input-rich-text value={source} formats={formats}>
																		 <lightning-rich-text-toolbar-button-group slot="toolbar" aria-label="Save Button Group">
																				<lightning-rich-text-toolbar-button
																						icon-name="utility:save"
																						icon-alternative-text="Save"
																						onclick={handleSave}>
																				</lightning-rich-text-toolbar-button>
																		</lightning-rich-text-toolbar-button-group>
																</lightning-input-rich-text>-->
                            </div>
                        </lightning-layout-item>
                    </lightning-layout>
                </lightning-layout-item>
								
            </lightning-layout>
        </div>
		
				<p slot="footer">Card Footer</p>
		</lightning-card>
		<template if:true={isModalOpen}>
				<section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open">
            <div class="slds-modal__container">
                <!-- Modal/Popup Box LWC header here -->
                <header class="slds-modal__header">
                    <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeModal}>
                        <lightning-icon icon-name="utility:close"
                            alternative-text="close"
                            variant="inverse"
                            size="small" ></lightning-icon>
                        <span class="slds-assistive-text">Close</span>
                    </button>
                    <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Modal/PopUp Box header LWC</h2>
                </header>
                <!-- Modal/Popup Box LWC body starts here -->
                <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
										<lightning-input type="text" label="Component name" onblur={sCompName}></lightning-input><br/>
										<lightning-input type="checkbox" label="Include CSS file" name="cssOption" onchange={sCheckboxValue}></lightning-input><br/>
										<lightning-input type="checkbox" label="Is Exposed?" name="exposedOption" onchange={sCheckboxValue}></lightning-input><br/>
										<h2 class="header">Target</h2><br/>
										<lightning-input type="checkbox" label="lightning__HomePage" name="lightning__HomePage" onchange={sCheckboxValue}></lightning-input><br/>
										<lightning-input type="checkbox" label="lightning__RecordPage" name="lightning__RecordPage" onchange={sCheckboxValue}></lightning-input><br/>
										<lightning-input type="checkbox" label="lightning__AppPage" name="lightning__AppPage" onchange={sCheckboxValue}></lightning-input><br/>
										<lightning-input type="checkbox" label="lightning__Tab" name="lightning__Tab" onchange={sCheckboxValue}></lightning-input><br/>
										
								</div>
								<footer class="slds-modal__footer">
                    <button class="slds-button slds-button_neutral" onclick={closeModal} title="Cancel">Cancel</button>
                    <button class="slds-button slds-button_brand" onclick={submitDetails} title="OK">OK</button>
                </footer>
						</div>
				</section>
				<div class="slds-backdrop slds-backdrop_open"></div>
		</template>
</template>

LWC component LWC Editor JS
1
import { LightningElement } from 'lwc';
import getLWCs from '@salesforce/apex/LWCEditor.getLWCs';
import getResources from '@salesforce/apex/LWCEditor.getLWCResource';
import updateResources from '@salesforce/apex/LWCEditor.updateLWCResource';
import createLWCResource from '@salesforce/apex/LWCEditor.createLWCResource';

export default class LWCEditor extends LightningElement {
		
    listLWCs;
		listResources;
		error;
		successMessage;
		bundleId = '';
		resourceId = '';
		filePath='';
		source = '';
		format = '';
		isModalOpen = false;
		targetsList = '';
		
		//new LWC component variables
		developerName;
		includeCSS=false;
		isExposed=false;
		targets = [];
		//new LWC component variables
		formats = ['font', 'size', 'bold', 'italic', 'underline', 'strike', 'list', 'indent', 'align', 'link', 'clean', 'table', 'header', 'color','code'];
		
		connectedCallback() {
				this.loadLWCs();
		}
	loadLWCs() {
		getLWCs()
			.then(result => {
				console.log(result);
				let parsedValue = JSON.parse(result)
				this.listLWCs = parsedValue.records;
			})
			.catch(error => {
				this.error = error;
			});
	}
		
	loadResources(event) {
			if(event.target.name!==undefined){
					this.bundleId = event.target.name;
					getResources({bundleId:event.target.name})
							.then(result => {
							console.log(result);
							let parsedValue1 = JSON.parse(result)
							this.listResources = parsedValue1.records;
					})
							.catch(error => {
							console.log(error);
							this.error = error;
					});
			}		
	}
		loadSource(event) {
				for(let acc of this.listResources){
						if(acc.Id ===  event.target.id.substring(0,18)){
								console.log('334455' );
								console.log(acc.FilePath );
								console.log(acc.Source );
								this.resourceId = acc.Id;
								this.filePath = acc.FilePath;
								this.source = acc.Source;
								this.format = acc.Format;
						}else{
								console.log('error');
								console.log('else');
						}
				}
		}
		
		handleSaveLWC() {
				console.log('111222' );
				console.log(this.bundleId );
				console.log(this.filePath );	
				console.log('111222' );		
				updateResources({id:this.resourceId ,fpath:this.filePath,bundleId:this.bundleId,format:this.format, source:JSON.stringify(this.source)})
							.then(result => {
							console.log(result);
							//let parsedValue1 = JSON.parse(result)
							//this.listResources = parsedValue1.records;
					})
							.catch(error => {
							console.log(error);
							this.error = error;
					});
		}
		
		updateSource(event) {
				console.log('update' );
				console.log(event.target.value );
				this.source = event.target.value;
		}
		sCheckboxValue(event) {
				console.log('setCheckboxValue' );
				console.log(event.target.name );
				
				if(event.target.name === 'cssOption'){
						this.includeCSS = true;
				}
				if(event.target.name === 'exposedOption'){
						this.isExposed = true;
				}
				
				if(event.target.name === 'lightning__HomePage' || event.target.name === 'lightning__RecordPage' || event.target.name === 'lightning__AppPage' || event.target.name === 'lightning__Tab'){
						this.targets.push(event.target.name);
						this.targetsList =  this.targetsList + '\n <target>'+event.target.name+'</target>';
				}
		}
		sCompName(event) {
				console.log('sCompName' );
				this.developerName = event.target.value;
		}
		
		handleNewLWC() {
				this.isModalOpen = true;
		}
		closeModal() {
				this.isModalOpen = false;
		}
		submitDetails() {
				const d = new Date();
				let sourceHTML = '<!-- @description: \n @author: ChangeMeIn@UserSettingsUnder.SFDoc \n @last modified on: '+d.toDateString()+' \n @last modified by: ChangeMeIn@UserSettingsUnder.SFDoc-->\n<template>\n</template>';
				let sourceJS = 'import { LightningElement } from \'lwc\'; \n export default class Empty extends LightningElement { \n }';
				let sourceMETA = '<?xml version="1.0"?>\n <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> \n <apiVersion>50</apiVersion> \n <isExposed>'+this.isExposed+'</isExposed> \n 	<targets> '+this.targetsList+'\n </targets> \n </LightningComponentBundle>';
				createLWCResource({description:'' ,developerName:this.developerName,isExposed:this.isExposed,includeCSS:this.includeCSS,targets:this.targets,sourceHTML:JSON.stringify(sourceHTML),sourceJS:JSON.stringify(sourceJS),sourceMETA:JSON.stringify(sourceMETA) })
							.then(result => {
							console.log(result);
						this.successMessage = result;
								setTimeout(function () {
										window.location.reload();
								}, 5000);
							
							//let parsedValue1 = JSON.parse(result)
							//this.listResources = parsedValue1.records;
					})
							.catch(error => {
							console.log(error);
							this.error = error;
					});
				//createLWCResource(String description,String developerName,String isExposed,List<String> targets
				this.isModalOpen = false;
		}
}
2

Git repo: https://github.com/risha9177719594/LWCEditor

No comments:

Powered by Blogger.