Skip to content

Commit

Permalink
feat: support tooling API for deployment of LWC components
Browse files Browse the repository at this point in the history
  • Loading branch information
Codeneos committed Sep 29, 2022
1 parent 42f703a commit 8c6a031
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 21 deletions.
13 changes: 11 additions & 2 deletions packages/vlocity-deploy/src/datapackDeployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,19 @@ export interface DatapackDeploymentOptions extends RecordBatchOptions {
*/
strictDependencies?: boolean;
/**
* When enabled LWC enabled OmniScripts will get compiled into native LWC components and be deployed to the target org during deployment.
* @default true;
* When enabled LWC enabled OmniScripts will not get compiled into native LWC components and be deployed to the target org during deployment.
*
* Use this if you want to manually compile OmniScripts into LWC or have a batch process ot activate OmniScript LWCs in bulk.
* @default false;
*/
skipLwcActivation?: boolean;
/**
* When true LWC components are deployed using the tooling API instead of the metadata API. The tooling API is usually faster and thus the proffered way to compiled deploy LWC components.
*
* Disable this if you need to use the metadata API to deploy LWC components.
* @default true;
*/
useToolingApi?: boolean;
}

@injectable.transient()
Expand Down
14 changes: 10 additions & 4 deletions packages/vlocity-deploy/src/datapackDeployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ const datapackDeploymentDefaultOptions = {
purgeMatchingDependencies: false,
purgeLookupOptimization: true,
bulkDependencyResolution: true,
deltaCheck: false
deltaCheck: false,
skipLwcActivation: false,
useToolingApi: true
};

/**
Expand Down Expand Up @@ -140,7 +142,7 @@ export class DatapackDeployment extends AsyncEventEmitter<DatapackDeploymentEven
}

private writeDeploymentSummaryToLog(timer: Timer) {
// Generate a reasonable log message that summerizes the deployment
// Generate a reasonable log message that summarizes the deployment
const deployMessage = `Deployed ${this.deployedRecordCount} records${this.failedRecordCount ? `, failed ${this.failedRecordCount}` : ' without errors'}`;

if (this.options.deltaCheck) {
Expand Down Expand Up @@ -244,8 +246,12 @@ export class DatapackDeployment extends AsyncEventEmitter<DatapackDeploymentEven
}
}

if (records.size == 0 && Iterable.some(this.records, ([,record]) => record.isPending)) {
throw new Error('Unable to deploy records; circular dependency detected');
if (records.size == 0) {
for (const record of Iterable.filter(this.records.values(), record => record.isPending)) {
const unsatisfiedDependencies = record.getUnresolvedDependencies().map(d => d.dependency.VlocityLookupRecordSourceKey ?? d.dependency.VlocityMatchingRecordSourceKey).join(', ');
this.logger.error(`Unable to deploy ${record.sourceKey} due to unsatisfied dependencies: ${unsatisfiedDependencies}`);
record.updateStatus(DeploymentStatus.Failed, `Missing dependencies: ${unsatisfiedDependencies}`);
}
}

return records.size > 0 ? records : undefined;
Expand Down
42 changes: 32 additions & 10 deletions packages/vlocity-deploy/src/deploymentSpecs/omniScript.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectable , LifecyclePolicy , Logger } from '@vlocode/core';
import { SalesforcePackage, SalesforceService } from '@vlocode/salesforce';
import { forEachAsyncParallel, Timer } from '@vlocode/util';
import { SalesforceDeployService, SalesforcePackage, SalesforceService } from '@vlocode/salesforce';
import { forEachAsyncParallel, Iterable, Timer } from '@vlocode/util';
import { DatapackDeploymentRecord, DeploymentStatus } from '../datapackDeploymentRecord';
import { VlocityDatapack } from '../datapack';
import type { DatapackDeploymentEvent, DatapackDeploymentOptions } from '../datapackDeployer';
Expand All @@ -14,14 +14,15 @@ export class OmniScript implements DatapackDeploymentSpec {

public constructor(
private readonly activator: OmniScriptActivator,
private readonly salesforceService: SalesforceService,
@injectable.param('DatapackDeploymentOptions') private readonly options: DatapackDeploymentOptions,
private readonly logger: Logger) {
}

public async preprocess(datapack: VlocityDatapack) {
if (datapack.IsLwcEnabled__c && this.options.skipLwcActivation !== true) {
if (datapack.IsLwcEnabled__c && !this.options.skipLwcActivation) {
this.lwcEnabledDatapacks.add(datapack.key);
} else {
this.logger.verbose(`Skipping LWC component compilation for ${datapack.key} due to "skipLwcActivation" being set as "true"`);
}

// Insert as inactive and update later in the process these are activated
Expand All @@ -37,24 +38,45 @@ export class OmniScript implements DatapackDeploymentSpec {
}

public async afterDeploy(event: DatapackDeploymentEvent) {
const packages = new Array<SalesforcePackage>();
// First activate the OmniScripts which generates the OmniScriptDefinition__c records
await forEachAsyncParallel(event.getDeployedRecords('OmniScript__c'), async record => {
try {
const timer = new Timer();
await this.activator.activate(record.recordId, { skipLwcDeployment: true });
if (this.lwcEnabledDatapacks.has(record.datapackKey)) {
packages.push(await this.activator.getLwcComponentBundle(record.recordId));
}
this.logger.info(`Activated ${record.datapackKey} [${timer.stop()}]`);
} catch(err) {
this.logger.error(`Failed to activate ${record.datapackKey} -- ${err}`);
record.updateStatus(DeploymentStatus.Failed, err.message || err);
}
}, 4);

// Then compile the LWC components; this is not done in a parallel loop to avoid LOCK errors from the tooling API
if (!this.options.skipLwcActivation) {
await this.deployLwcComponents(event);
}
}

public async deployLwcComponents(event: DatapackDeploymentEvent) {
const packages = new Array<SalesforcePackage>();

for (const record of Iterable.filter(event.getDeployedRecords('OmniScript__c'), r => this.lwcEnabledDatapacks.has(r.datapackKey))) {
try {
if (this.options.useToolingApi) {
await this.activator.activateLwc(record.recordId, { toolingApi: true });
} else {
packages.push(await this.activator.getLwcComponentBundle(record.recordId));
}
} catch(err) {
this.logger.error(`Failed to deploy LWC component for ${record.datapackKey} -- ${err}`);
record.updateStatus(DeploymentStatus.Failed, err.message || err);
}
}

if (packages.length) {
this.logger.info(`Deploying ${packages.length} LWC components`);
await this.salesforceService.deploy.deployPackage(packages.reduce((p, c) => p.merge(c)));
const timer = new Timer();
this.logger.info(`Deploying ${packages.length} LWC component(s) using metadata api...`);
await new SalesforceDeployService(undefined, Logger.null).deployPackage(packages.reduce((p, c) => p.merge(c)));
this.logger.info(`Deployed ${packages.length} LWC components [${timer.stop()}]`);
}
}
}
47 changes: 42 additions & 5 deletions packages/vlocity-deploy/src/omniScript/omniScriptActivator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QueryService, SalesforceService, NamespaceService, QueryBuilder, SalesforceDeployService } from '@vlocode/salesforce';
import { injectable, Logger } from '@vlocode/core';
import { OmniScriptDefinition, OmniScriptDetail } from './omniScriptDefinition';
import { Iterable } from '@vlocode/util';
import { Iterable, Timer } from '@vlocode/util';
import { OmniScriptLwcCompiler } from './omniScriptLwcCompiler';
import { ScriptDefinitionProvider } from './scriptDefinitionProvider';

Expand Down Expand Up @@ -63,9 +63,9 @@ export class OmniScriptActivator {
* Activate the LWC component for the specified OmniScript regardless of the script is LWC enabled or not.
* @param id Id of the OmniScript for which to activate the LWC component
*/
public async activateLwc(id: string) {
public async activateLwc(id: string, options?: { toolingApi?: boolean }) {
const definition = await this.definitionProvider.getScriptDefinition(id);
await this.deployLwcComponent(definition);
await this.deployLwcComponent(definition, options);
}

/**
Expand All @@ -78,13 +78,50 @@ export class OmniScriptActivator {
return this.lwcCompiler.compileToPackage(definition);
}

private async deployLwcComponent(definition: OmniScriptDefinition) {
private async deployLwcComponent(definition: OmniScriptDefinition, options?: { toolingApi?: boolean }) {
const timer = new Timer();
const apiLabel = options?.toolingApi ? 'tooling' : 'metadata';
this.logger.info(`Deploying OmniScript ${definition.bpType}/${definition.bpSubType} LWC component with ${apiLabel} api...`);

if (options?.toolingApi) {
await this.deployLwcWithToolingApi(definition);
} else {
await this.deployLwcWithMetadataApi(definition);
}

this.logger.info(`Deployed OmniScript ${definition.bpType}/${definition.bpSubType} LWC component with ${apiLabel} api [${timer.stop()}]`);
}

private async deployLwcWithMetadataApi(definition: OmniScriptDefinition) {
const sfPackage = await this.lwcCompiler.compileToPackage(definition);
const deployService = new SalesforceDeployService(this.salesforceService, Logger.null);
const result = await deployService.deployPackage(sfPackage);
const result = await deployService.deployPackage(sfPackage);
if (!result.success) {
throw new Error(`OmniScript LWC Component deployment failed: ${result.details?.componentFailures.map(failure => failure.problem)}`);
}
}

private async deployLwcWithToolingApi(definition: OmniScriptDefinition) {
const tollingRecord = await this.lwcCompiler.compileToolingRecord(definition)
const result = await this.upsertToolingRecord(`LightningComponentBundle`, tollingRecord);
if (!result.success) {
throw new Error(`OmniScript LWC Component deployment failed: ${JSON.stringify(result.errors)}`);
}
}

private async upsertToolingRecord(type: string, toolingRecord: { Id?: string, FullName: string, Metadata: any }): Promise<{ success: boolean, errors: string[] }> {
const connection = await this.salesforceService.getJsForceConnection();
if (!toolingRecord.Id) {
const existingRecord = await connection.tooling.query<{ Id: string }>(`SELECT Id FROM ${type} WHERE DeveloperName = '${toolingRecord.FullName}'`);
if (existingRecord.totalSize > 0) {
toolingRecord.Id = existingRecord.records[0].Id;
}
}

if (toolingRecord.Id) {
return connection.tooling.update(type, toolingRecord) as any;
}
return connection.tooling.create(type, toolingRecord) as any;
}

private async getScript(input: OmniScriptDetail | string) {
Expand Down
41 changes: 41 additions & 0 deletions packages/vlocity-deploy/src/omniScript/omniScriptLwcCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { injectable } from '@vlocode/core';
import { OmniScriptDefinition, OmniScriptDetail } from './omniScriptDefinition';
import { VlocityNamespaceService } from '../vlocityNamespaceService';
import { ScriptDefinitionProvider } from './scriptDefinitionProvider';
import { Timer, XML } from '@vlocode/util';

export interface CompiledResource {
name: string,
Expand Down Expand Up @@ -52,6 +53,7 @@ export class OmniScriptLwcCompiler{
throw new Error('Unable to find OmniScript LWC compiler; is the Vlocity managed package installed?');
}


// create JSDOM to init compiler
const connection = await this.salesforceService.getJsForceConnection();
if (!this.compilerApiVersion) {
Expand Down Expand Up @@ -95,6 +97,45 @@ export class OmniScriptLwcCompiler{
return { name: componentName, resources }
}

public async compileToolingRecord(scriptDefinition: OmniScriptDefinition, options?: { lwcName?: string }) {
// Get the name of the component or generate it and compile the OS
const componentName = options?.lwcName ?? this.getLwcName({ type: scriptDefinition.bpType, subType: scriptDefinition.bpSubType, language: scriptDefinition.bpLang });
const compiler = await this.getCompiler();
const resources = await compiler.compileActivated(componentName, scriptDefinition, false, true, this.namespaceService.getNamespace());

const componentMetaDefinition = resources.find(r => r.name.endsWith('.js-meta.xml'));
if (!componentMetaDefinition) {
throw new Error(`LWC compiler did not generate a .js-meta.xml file for ${componentName}`);
}

const { LightningComponentBundle: componentDef } = XML.parse(componentMetaDefinition.source);
const targetConfigs = XML.stringify(componentDef.targetConfigs, undefined, { headless: true });

const toolingMetadata = {
apiVersion: componentDef.apiVersion,
lwcResources: {
lwcResource: resources.filter(f => !f.name.endsWith('.js-meta.xml')).map(res =>({
filePath: res.name,
source: Buffer.from(res.source, 'utf-8').toString('base64')
}))
},
capabilities: {
capability: []
},
isExposed: componentDef.isExposed,
description: componentDef.description,
masterLabel: componentDef.masterLabel,
runtimeNamespace: componentDef.runtimeNamespace,
targets: componentDef.targets,
targetConfigs: Buffer.from(targetConfigs, 'utf-8').toString('base64')
}

return {
FullName: componentName,
Metadata: toolingMetadata
};
}

/**
* Compile and package an OmniScript into a deployable Metadata package
* @param scriptDefinition Definition of the OmniScript to compile
Expand Down

0 comments on commit 8c6a031

Please sign in to comment.