Skip to content

Commit 16dc4f8

Browse files
committed
converter fixes
1 parent 28170bd commit 16dc4f8

7 files changed

Lines changed: 320 additions & 68 deletions

File tree

packages/b2c-cli/src/commands/pipeline/convert.ts

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@ import {convertPipeline, type ConvertResult} from '@salesforce/b2c-tooling-sdk/o
1313
import {glob} from 'glob';
1414
import {t} from '../../i18n/index.js';
1515

16+
interface ConvertError {
17+
pipelineName: string;
18+
error: string;
19+
}
20+
1621
interface ConvertResponse {
1722
results: ConvertResult[];
23+
errors: ConvertError[];
1824
totalPipelines: number;
1925
successCount: number;
26+
failureCount: number;
2027
warningCount: number;
2128
}
2229

@@ -78,8 +85,10 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
7885

7986
const response: ConvertResponse = {
8087
results: conversionResults.results,
88+
errors: conversionResults.errors,
8189
totalPipelines: inputFiles.length,
8290
successCount: conversionResults.successCount,
91+
failureCount: conversionResults.failureCount,
8392
warningCount: conversionResults.warningCount,
8493
};
8594

@@ -89,12 +98,26 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
8998

9099
if (!dryRun) {
91100
this.log('');
92-
this.log(
93-
t('commands.pipeline.convert.summary', 'Converted {{count}} pipeline(s) with {{warnings}} warning(s)', {
94-
count: conversionResults.successCount,
95-
warnings: conversionResults.warningCount,
96-
}),
97-
);
101+
if (conversionResults.failureCount > 0) {
102+
this.log(
103+
t(
104+
'commands.pipeline.convert.summaryWithFailures',
105+
'Converted {{success}} pipeline(s), {{failures}} failed, {{warnings}} warning(s)',
106+
{
107+
success: conversionResults.successCount,
108+
failures: conversionResults.failureCount,
109+
warnings: conversionResults.warningCount,
110+
},
111+
),
112+
);
113+
} else {
114+
this.log(
115+
t('commands.pipeline.convert.summary', 'Converted {{count}} pipeline(s) with {{warnings}} warning(s)', {
116+
count: conversionResults.successCount,
117+
warnings: conversionResults.warningCount,
118+
}),
119+
);
120+
}
98121
}
99122

100123
return response;
@@ -107,7 +130,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
107130
inputFile: string,
108131
output: string | undefined,
109132
dryRun: boolean,
110-
): Promise<{result: ConvertResult; success: boolean}> {
133+
): Promise<{result?: ConvertResult; success: boolean; error?: ConvertError}> {
111134
const pipelineName = basename(inputFile, '.xml');
112135

113136
if (!dryRun) {
@@ -125,37 +148,52 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
125148
? join(output, `${pipelineName}.js`)
126149
: join(dirname(inputFile), `${pipelineName}.js`);
127150

128-
const result = await this.operations.convertPipeline(inputFile, {
129-
outputPath,
130-
dryRun,
131-
});
151+
try {
152+
const result = await this.operations.convertPipeline(inputFile, {
153+
outputPath,
154+
dryRun,
155+
});
132156

133-
if (result.warnings.length > 0) {
134-
for (const warning of result.warnings) {
135-
this.warn(warning);
157+
if (result.warnings.length > 0) {
158+
for (const warning of result.warnings) {
159+
this.warn(warning);
160+
}
136161
}
137-
}
138162

139-
let success = false;
140-
if (dryRun) {
141-
// In dry-run mode, output the generated code
142-
if (!this.jsonEnabled()) {
143-
ux.stdout(`\n// === ${pipelineName}.js ===\n`);
144-
ux.stdout(result.code);
145-
ux.stdout('\n');
163+
let success = false;
164+
if (dryRun) {
165+
// In dry-run mode, output the generated code
166+
if (!this.jsonEnabled()) {
167+
ux.stdout(`\n// === ${pipelineName}.js ===\n`);
168+
ux.stdout(result.code);
169+
ux.stdout('\n');
170+
}
171+
success = true;
172+
} else if (outputPath) {
173+
// Write the file
174+
await writeFile(outputPath, result.code, 'utf8');
175+
this.log(
176+
t('commands.pipeline.convert.generated', 'Generated: {{path}}', {
177+
path: outputPath,
178+
}),
179+
);
180+
success = true;
146181
}
147-
} else if (outputPath) {
148-
// Write the file
149-
await writeFile(outputPath, result.code, 'utf8');
150-
this.log(
151-
t('commands.pipeline.convert.generated', 'Generated: {{path}}', {
152-
path: outputPath,
182+
183+
return {result, success};
184+
} catch (err) {
185+
const errorMessage = err instanceof Error ? err.message : String(err);
186+
this.logToStderr(
187+
t('commands.pipeline.convert.failed', 'Failed to convert {{name}}: {{error}}', {
188+
name: pipelineName,
189+
error: errorMessage,
153190
}),
154191
);
155-
success = true;
192+
return {
193+
success: false,
194+
error: {pipelineName, error: errorMessage},
195+
};
156196
}
157-
158-
return {result, success};
159197
}
160198

161199
/**
@@ -165,21 +203,37 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
165203
inputFiles: string[],
166204
output: string | undefined,
167205
dryRun: boolean,
168-
): Promise<{results: ConvertResult[]; successCount: number; warningCount: number}> {
206+
): Promise<{
207+
results: ConvertResult[];
208+
errors: ConvertError[];
209+
successCount: number;
210+
failureCount: number;
211+
warningCount: number;
212+
}> {
169213
const results: ConvertResult[] = [];
214+
const errors: ConvertError[] = [];
170215
let successCount = 0;
216+
let failureCount = 0;
171217
let warningCount = 0;
172218

173219
// Process files sequentially to maintain order and avoid concurrent file operations
174220
for (const inputFile of inputFiles) {
175221
// eslint-disable-next-line no-await-in-loop
176222
const fileResult = await this.processFile(inputFile, output, dryRun);
177-
results.push(fileResult.result);
178-
successCount += fileResult.success ? 1 : 0;
179-
warningCount += fileResult.result.warnings.length;
223+
if (fileResult.result) {
224+
results.push(fileResult.result);
225+
warningCount += fileResult.result.warnings.length;
226+
}
227+
if (fileResult.error) {
228+
errors.push(fileResult.error);
229+
failureCount++;
230+
}
231+
if (fileResult.success) {
232+
successCount++;
233+
}
180234
}
181235

182-
return {results, successCount, warningCount};
236+
return {results, errors, successCount, failureCount, warningCount};
183237
}
184238

185239
/**

packages/b2c-tooling-sdk/src/operations/pipeline/analyzer.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface AnalysisContext {
4343
requiredImports: Set<string>;
4444
/** Warnings accumulated during analysis. */
4545
warnings: string[];
46+
/** Stop node ID - analysis stops when reaching this node (for branch convergence). */
47+
stopAtNodeId?: string;
4648
}
4749

4850
/**
@@ -119,6 +121,11 @@ function analyzeFromNode(nodeId: string, context: AnalysisContext): ControlFlowB
119121
let currentId: string | undefined = nodeId;
120122

121123
while (currentId) {
124+
// Stop if we've reached the convergence point
125+
if (context.stopAtNodeId && currentId === context.stopAtNodeId) {
126+
break;
127+
}
128+
122129
// Cycle detection
123130
if (context.visited.has(currentId)) {
124131
context.warnings.push(`Cycle detected at node ${currentId}`);
@@ -208,18 +215,21 @@ function analyzeDecisionNode(node: DecisionNodeIR, context: AnalysisContext): If
208215
const yesTransition = node.transitions.find((t) => t.connector === 'yes');
209216
const noTransition = node.transitions.find((t) => t.connector === 'no');
210217

218+
// Find convergence point to limit branch analysis
219+
const convergencePoint = findConvergencePoint(node, context);
220+
211221
let thenBlock: ControlFlowBlock = {type: 'sequence', blocks: []};
212222
let elseBlock: ControlFlowBlock | undefined;
213223

214224
if (yesTransition) {
215-
const branchContext = createBranchContext(context);
225+
const branchContext = createBranchContext(context, convergencePoint);
216226
thenBlock = analyzeFromNode(yesTransition.targetId, branchContext);
217227
context.warnings.push(...branchContext.warnings);
218228
mergeImports(context, branchContext);
219229
}
220230

221231
if (noTransition) {
222-
const branchContext = createBranchContext(context);
232+
const branchContext = createBranchContext(context, convergencePoint);
223233
elseBlock = analyzeFromNode(noTransition.targetId, branchContext);
224234
context.warnings.push(...branchContext.warnings);
225235
mergeImports(context, branchContext);
@@ -296,20 +306,24 @@ function analyzePipeletWithError(node: PipeletNodeIR, context: AnalysisContext):
296306
const errorTransition = node.transitions.find((t) => t.connector === 'error');
297307
const successTransition = node.transitions.find((t) => !t.connector || t.connector === 'next');
298308

299-
// Try block: the pipelet + success path
309+
// Find convergence point to limit branch analysis
310+
const convergencePoint = findConvergencePoint(node, context);
311+
312+
// Try block: the pipelet only (success path analyzed separately after try-catch)
300313
const tryBlocks: ControlFlowBlock[] = [createStatement(node.id)];
301-
if (successTransition) {
302-
const successContext = createBranchContext(context);
314+
if (successTransition && convergencePoint) {
315+
// Only analyze the success path up to convergence point
316+
const successContext = createBranchContext(context, convergencePoint);
303317
const successBlock = analyzeFromNode(successTransition.targetId, successContext);
304318
tryBlocks.push(successBlock);
305319
context.warnings.push(...successContext.warnings);
306320
mergeImports(context, successContext);
307321
}
308322

309-
// Catch block: error path
323+
// Catch block: error path up to convergence point
310324
let catchBlock: ControlFlowBlock = {type: 'sequence', blocks: []};
311325
if (errorTransition) {
312-
const errorContext = createBranchContext(context);
326+
const errorContext = createBranchContext(context, convergencePoint);
313327
catchBlock = analyzeFromNode(errorTransition.targetId, errorContext);
314328
context.warnings.push(...errorContext.warnings);
315329
mergeImports(context, errorContext);
@@ -356,12 +370,13 @@ function buildInteractionContinueHandler(node: InteractionContinueNodeIR, contex
356370
/**
357371
* Creates a branch context for analyzing nested control flow.
358372
*/
359-
function createBranchContext(parent: AnalysisContext): AnalysisContext {
373+
function createBranchContext(parent: AnalysisContext, stopAtNodeId?: string): AnalysisContext {
360374
return {
361375
pipeline: parent.pipeline,
362376
visited: new Set(parent.visited),
363377
requiredImports: new Set(),
364378
warnings: [],
379+
stopAtNodeId,
365380
};
366381
}
367382

packages/b2c-tooling-sdk/src/operations/pipeline/generator/nodes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ function generateEndNode(node: EndNodeIR, context: GeneratorContext): string {
179179
function generateCallNode(node: CallNodeIR, context: GeneratorContext): string {
180180
const ind = indent(context.indent);
181181

182+
// Dynamic dispatch - target determined at runtime from site preference
183+
if (node.isDynamic && node.dynamicKey) {
184+
return `${ind}// TODO: Dynamic pipeline call - target resolved from key: ${node.dynamicKey}\n${ind}// Original: call-node start-name-key="${node.dynamicKey}"`;
185+
}
186+
182187
if (node.pipelineName === context.pipelineName) {
183188
// Same pipeline - direct function call
184189
return `${ind}${node.startName}();`;
@@ -194,6 +199,11 @@ function generateCallNode(node: CallNodeIR, context: GeneratorContext): string {
194199
function generateJumpNode(node: JumpNodeIR, context: GeneratorContext): string {
195200
const ind = indent(context.indent);
196201

202+
// Dynamic dispatch - target determined at runtime from site preference
203+
if (node.isDynamic && node.dynamicKey) {
204+
return `${ind}// TODO: Dynamic pipeline jump - target resolved from key: ${node.dynamicKey}\n${ind}// Original: jump-node start-name-key="${node.dynamicKey}"`;
205+
}
206+
197207
// Convert Pipeline-Start to Controller-Action URL
198208
return `${ind}response.redirect(URLUtils.url('${node.pipelineName}-${node.startName}'));`;
199209
}

packages/b2c-tooling-sdk/src/operations/pipeline/index.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,50 @@ import {basename} from 'path';
5454
import {analyzePipeline} from './analyzer.js';
5555
import {generateController} from './generator/index.js';
5656
import {parsePipeline} from './parser.js';
57-
import type {ConvertOptions, ConvertResult} from './types.js';
57+
import {getUnconvertablePipelet} from './pipelets/index.js';
58+
import type {ConvertOptions, ConvertResult, PipelineIR, PipeletNodeIR} from './types.js';
59+
60+
/**
61+
* Error thrown when a pipeline contains unconvertable pipelets.
62+
*/
63+
export class UnconvertablePipelineError extends Error {
64+
constructor(
65+
public readonly pipelineName: string,
66+
public readonly unconvertablePipelets: Array<{name: string; reason: string}>,
67+
) {
68+
const pipeletList = unconvertablePipelets.map((p) => ` - ${p.name}: ${p.reason}`).join('\n');
69+
super(`Pipeline "${pipelineName}" contains unconvertable pipelets:\n${pipeletList}`);
70+
this.name = 'UnconvertablePipelineError';
71+
}
72+
}
73+
74+
/**
75+
* Validates a pipeline for unconvertable pipelets.
76+
* @throws UnconvertablePipelineError if any unconvertable pipelets are found
77+
*/
78+
function validatePipeline(pipeline: PipelineIR): void {
79+
const unconvertable: Array<{name: string; reason: string}> = [];
80+
81+
for (const node of pipeline.nodes.values()) {
82+
if (node.type === 'pipelet') {
83+
const pipeletNode = node as PipeletNodeIR;
84+
const info = getUnconvertablePipelet(pipeletNode.pipeletName);
85+
if (info) {
86+
// Only add if not already in the list
87+
if (!unconvertable.some((p) => p.name === pipeletNode.pipeletName)) {
88+
unconvertable.push({
89+
name: pipeletNode.pipeletName,
90+
reason: info.unconvertableReason ?? 'No script API equivalent',
91+
});
92+
}
93+
}
94+
}
95+
}
96+
97+
if (unconvertable.length > 0) {
98+
throw new UnconvertablePipelineError(pipeline.name, unconvertable);
99+
}
100+
}
58101

59102
// Re-export all types
60103
export type {
@@ -91,7 +134,14 @@ export {analyzePipeline} from './analyzer.js';
91134
export {generateController} from './generator/index.js';
92135

93136
// Re-export pipelet utilities
94-
export {getPipeletMapping, isPipeletMapped, PIPELET_MAPPINGS} from './pipelets/index.js';
137+
export {
138+
getPipeletMapping,
139+
getUnconvertablePipelet,
140+
isPipeletMapped,
141+
isUnconvertablePipelet,
142+
PIPELET_MAPPINGS,
143+
UNCONVERTABLE_PIPELETS,
144+
} from './pipelets/index.js';
95145
export type {PipeletMapping} from './pipelets/index.js';
96146

97147
/**
@@ -122,6 +172,9 @@ export async function convertPipeline(inputPath: string, options: ConvertOptions
122172
// Parse
123173
const pipeline = await parsePipeline(xml, pipelineName);
124174

175+
// Validate for unconvertable pipelets
176+
validatePipeline(pipeline);
177+
125178
// Analyze
126179
const analysis = analyzePipeline(pipeline);
127180

@@ -161,6 +214,7 @@ export async function convertPipeline(inputPath: string, options: ConvertOptions
161214
*/
162215
export async function convertPipelineContent(xml: string, pipelineName: string): Promise<ConvertResult> {
163216
const pipeline = await parsePipeline(xml, pipelineName);
217+
validatePipeline(pipeline);
164218
const analysis = analyzePipeline(pipeline);
165219
const code = generateController(pipeline, analysis);
166220

0 commit comments

Comments
 (0)