Skip to content

Commit 14e2c37

Browse files
committed
add --allow-unsupported flag for pipeline conversion
Allow pipeline conversion to continue when encountering unsupported pipelets by generating TODO comments instead of failing. This enables partial conversion of pipelines that contain pipelets without script API equivalents (e.g., GetLastVisitedProducts, ImportInventoryLists). - Add --allow-unsupported CLI flag - Modify validatePipeline() to return warnings instead of throwing - Generate UNSUPPORTED comments with reason and key bindings - Add tests for the new functionality
1 parent b1c140e commit 14e2c37

File tree

7 files changed

+124
-17
lines changed

7 files changed

+124
-17
lines changed

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
5959
description: 'Preview generated code without writing files',
6060
default: false,
6161
}),
62+
'allow-unsupported': Flags.boolean({
63+
description: 'Continue conversion with unsupported pipelets (generates TODO comments instead of failing)',
64+
default: false,
65+
}),
6266
};
6367

6468
protected operations = {
@@ -67,7 +71,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
6771

6872
async run(): Promise<ConvertResponse> {
6973
const {input} = this.args;
70-
const {output, 'dry-run': dryRun} = this.flags;
74+
const {output, 'dry-run': dryRun, 'allow-unsupported': allowUnsupported} = this.flags;
7175

7276
// Resolve input files
7377
const inputFiles = await this.resolveInputFiles(input);
@@ -81,7 +85,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
8185
}
8286

8387
// Process all files and collect results
84-
const conversionResults = await this.processFiles(inputFiles, output, dryRun);
88+
const conversionResults = await this.processFiles(inputFiles, output, dryRun, allowUnsupported);
8589

8690
const response: ConvertResponse = {
8791
results: conversionResults.results,
@@ -130,6 +134,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
130134
inputFile: string,
131135
output: string | undefined,
132136
dryRun: boolean,
137+
allowUnsupported: boolean,
133138
): Promise<{result?: ConvertResult; success: boolean; error?: ConvertError}> {
134139
const pipelineName = basename(inputFile, '.xml');
135140

@@ -152,6 +157,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
152157
const result = await this.operations.convertPipeline(inputFile, {
153158
outputPath,
154159
dryRun,
160+
allowUnsupported,
155161
});
156162

157163
if (result.warnings.length > 0) {
@@ -181,8 +187,8 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
181187
}
182188

183189
return {result, success};
184-
} catch (err) {
185-
const errorMessage = err instanceof Error ? err.message : String(err);
190+
} catch (error) {
191+
const errorMessage = error instanceof Error ? error.message : String(error);
186192
this.logToStderr(
187193
t('commands.pipeline.convert.failed', 'Failed to convert {{name}}: {{error}}', {
188194
name: pipelineName,
@@ -203,6 +209,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
203209
inputFiles: string[],
204210
output: string | undefined,
205211
dryRun: boolean,
212+
allowUnsupported: boolean,
206213
): Promise<{
207214
results: ConvertResult[];
208215
errors: ConvertError[];
@@ -219,7 +226,7 @@ export default class PipelineConvert extends BaseCommand<typeof PipelineConvert>
219226
// Process files sequentially to maintain order and avoid concurrent file operations
220227
for (const inputFile of inputFiles) {
221228
// eslint-disable-next-line no-await-in-loop
222-
const fileResult = await this.processFile(inputFile, output, dryRun);
229+
const fileResult = await this.processFile(inputFile, output, dryRun, allowUnsupported);
223230
if (fileResult.result) {
224231
results.push(fileResult.result);
225232
warningCount += fileResult.result.warnings.length;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export const VARIABLE_MAPPINGS: Record<string, string> = {
2323
CurrentHttpParameterMap: 'request.httpParameterMap',
2424
};
2525

26+
/**
27+
* Options for code generation.
28+
*/
29+
export interface GeneratorOptions {
30+
/** If true, allow unsupported pipelets to be converted with UNSUPPORTED comments instead of failing. */
31+
allowUnsupported?: boolean;
32+
}
33+
2634
/**
2735
* Context for code generation.
2836
*/
@@ -37,6 +45,8 @@ export interface GeneratorContext {
3745
requires: Map<string, string>;
3846
/** The current pipeline name (for call node resolution). */
3947
pipelineName: string;
48+
/** Generator options. */
49+
options?: GeneratorOptions;
4050
}
4151

4252
/**

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414

1515
import type {AnalyzedFunction, AnalysisResult, PipelineIR} from '../types.js';
1616
import {generateBlock} from './blocks.js';
17-
import {type GeneratorContext, getRequireVarName, indent} from './helpers.js';
17+
import {type GeneratorContext, type GeneratorOptions, getRequireVarName, indent} from './helpers.js';
1818

1919
/**
2020
* Generates JavaScript controller code from the analysis result.
2121
*
2222
* @param pipeline - The parsed pipeline IR
2323
* @param analysis - The control flow analysis result
24+
* @param options - Generator options
2425
* @returns Generated JavaScript code
2526
*/
26-
export function generateController(pipeline: PipelineIR, analysis: AnalysisResult): string {
27+
export function generateController(pipeline: PipelineIR, analysis: AnalysisResult, options?: GeneratorOptions): string {
2728
// Collect all requires from all functions
2829
// Only include dw/* modules and relative controller paths at the top
2930
// Script pipelets use inline require() calls
@@ -54,6 +55,7 @@ export function generateController(pipeline: PipelineIR, analysis: AnalysisResul
5455
declaredVars: new Set(),
5556
requires: allRequires,
5657
pipelineName: pipeline.name,
58+
options,
5759
};
5860

5961
const funcCode = generateFunction(func, context);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* @module operations/pipeline/generator/nodes
1111
*/
1212

13+
import {getUnconvertablePipelet} from '../pipelets/index.js';
1314
import type {
1415
CallNodeIR,
1516
EndNodeIR,
@@ -250,6 +251,20 @@ function generatePipeletNode(node: PipeletNodeIR, context: GeneratorContext): st
250251
const ind = indent(context.indent);
251252
const lines: string[] = [];
252253

254+
// Check if pipelet is unconvertable (only when allowUnsupported is true)
255+
if (context.options?.allowUnsupported) {
256+
const unconvertableInfo = getUnconvertablePipelet(node.pipeletName);
257+
if (unconvertableInfo) {
258+
lines.push(`${ind}// UNSUPPORTED: ${node.pipeletName}`);
259+
lines.push(`${ind}// Reason: ${unconvertableInfo.unconvertableReason ?? 'No script API equivalent'}`);
260+
lines.push(`${ind}// TODO: Manual conversion required`);
261+
for (const kb of node.keyBindings) {
262+
lines.push(`${ind}// ${kb.key} = ${kb.value}`);
263+
}
264+
return lines.join('\n');
265+
}
266+
}
267+
253268
switch (node.pipeletName) {
254269
// Common pipelets
255270
case 'Assign':

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,12 @@ export class UnconvertablePipelineError extends Error {
7373

7474
/**
7575
* Validates a pipeline for unconvertable pipelets.
76-
* @throws UnconvertablePipelineError if any unconvertable pipelets are found
76+
* @param pipeline - The pipeline IR to validate
77+
* @param allowUnsupported - If true, return warnings instead of throwing
78+
* @returns Array of warning messages when allowUnsupported is true
79+
* @throws UnconvertablePipelineError if unconvertable pipelets are found and allowUnsupported is false
7780
*/
78-
function validatePipeline(pipeline: PipelineIR): void {
81+
function validatePipeline(pipeline: PipelineIR, allowUnsupported: boolean = false): string[] {
7982
const unconvertable: Array<{name: string; reason: string}> = [];
8083

8184
for (const node of pipeline.nodes.values()) {
@@ -95,8 +98,14 @@ function validatePipeline(pipeline: PipelineIR): void {
9598
}
9699

97100
if (unconvertable.length > 0) {
101+
if (allowUnsupported) {
102+
// Return warnings instead of throwing
103+
return unconvertable.map((p) => `Unsupported pipelet "${p.name}": ${p.reason}`);
104+
}
98105
throw new UnconvertablePipelineError(pipeline.name, unconvertable);
99106
}
107+
108+
return [];
100109
}
101110

102111
// Re-export all types
@@ -173,13 +182,13 @@ export async function convertPipeline(inputPath: string, options: ConvertOptions
173182
const pipeline = await parsePipeline(xml, pipelineName);
174183

175184
// Validate for unconvertable pipelets
176-
validatePipeline(pipeline);
185+
const validationWarnings = validatePipeline(pipeline, options.allowUnsupported);
177186

178187
// Analyze
179188
const analysis = analyzePipeline(pipeline);
180189

181-
// Generate
182-
const code = generateController(pipeline, analysis);
190+
// Generate (pass allowUnsupported to enable UNSUPPORTED comment generation)
191+
const code = generateController(pipeline, analysis, {allowUnsupported: options.allowUnsupported});
183192

184193
// Write output if not dry-run
185194
let outputPath: string | undefined;
@@ -192,7 +201,7 @@ export async function convertPipeline(inputPath: string, options: ConvertOptions
192201
pipelineName,
193202
code,
194203
outputPath,
195-
warnings: analysis.warnings,
204+
warnings: [...validationWarnings, ...analysis.warnings],
196205
};
197206
}
198207

@@ -212,15 +221,19 @@ export async function convertPipeline(inputPath: string, options: ConvertOptions
212221
* console.log(result.code);
213222
* ```
214223
*/
215-
export async function convertPipelineContent(xml: string, pipelineName: string): Promise<ConvertResult> {
224+
export async function convertPipelineContent(
225+
xml: string,
226+
pipelineName: string,
227+
options: ConvertOptions = {},
228+
): Promise<ConvertResult> {
216229
const pipeline = await parsePipeline(xml, pipelineName);
217-
validatePipeline(pipeline);
230+
const validationWarnings = validatePipeline(pipeline, options.allowUnsupported);
218231
const analysis = analyzePipeline(pipeline);
219-
const code = generateController(pipeline, analysis);
232+
const code = generateController(pipeline, analysis, {allowUnsupported: options.allowUnsupported});
220233

221234
return {
222235
pipelineName,
223236
code,
224-
warnings: analysis.warnings,
237+
warnings: [...validationWarnings, ...analysis.warnings],
225238
};
226239
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface ConvertOptions {
2727
outputPath?: string;
2828
/** If true, don't write the file, just return the generated code. */
2929
dryRun?: boolean;
30+
/** If true, allow unsupported pipelets to be converted with TODO comments instead of failing. */
31+
allowUnsupported?: boolean;
3032
}
3133

3234
/**

packages/b2c-tooling-sdk/test/operations/pipeline/generator.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
analyzePipeline,
1111
generateController,
1212
convertPipelineContent,
13+
UnconvertablePipelineError,
1314
} from '@salesforce/b2c-tooling-sdk/operations/pipeline';
1415

1516
describe('operations/pipeline/generator', () => {
@@ -780,4 +781,61 @@ describe('operations/pipeline/generator', () => {
780781
expect(result.code).to.include('exports.Edit.public = true;');
781782
});
782783
});
784+
785+
describe('allowUnsupported option', () => {
786+
const xmlWithUnconvertable = `<?xml version="1.0" encoding="UTF-8"?>
787+
<pipeline>
788+
<branch basename="Test">
789+
<segment>
790+
<node><start-node name="Test"/></node>
791+
<simple-transition/>
792+
<node>
793+
<pipelet-node pipelet-name="GetLastVisitedProducts" pipelet-set-identifier="bc_api">
794+
<key-binding alias="Products" key="Products"/>
795+
</pipelet-node>
796+
</node>
797+
<simple-transition/>
798+
<node><end-node/></node>
799+
</segment>
800+
</branch>
801+
</pipeline>`;
802+
803+
it('throws UnconvertablePipelineError by default for unsupported pipelets', async () => {
804+
try {
805+
await convertPipelineContent(xmlWithUnconvertable, 'Test');
806+
expect.fail('Should have thrown UnconvertablePipelineError');
807+
} catch (error) {
808+
expect(error).to.be.instanceOf(UnconvertablePipelineError);
809+
expect((error as UnconvertablePipelineError).pipelineName).to.equal('Test');
810+
expect((error as UnconvertablePipelineError).unconvertablePipelets).to.have.length(1);
811+
expect((error as UnconvertablePipelineError).unconvertablePipelets[0].name).to.equal('GetLastVisitedProducts');
812+
}
813+
});
814+
815+
it('generates UNSUPPORTED comments when allowUnsupported is true', async () => {
816+
const result = await convertPipelineContent(xmlWithUnconvertable, 'Test', {allowUnsupported: true});
817+
818+
expect(result.code).to.include('// UNSUPPORTED: GetLastVisitedProducts');
819+
expect(result.code).to.include('// Reason: Session-based pipelet');
820+
expect(result.code).to.include('// TODO: Manual conversion required');
821+
expect(result.code).to.include('// Products = Products');
822+
});
823+
824+
it('returns warnings for unsupported pipelets when allowUnsupported is true', async () => {
825+
const result = await convertPipelineContent(xmlWithUnconvertable, 'Test', {allowUnsupported: true});
826+
827+
expect(result.warnings).to.include(
828+
'Unsupported pipelet "GetLastVisitedProducts": Session-based pipelet. Use session.custom or clickstream API instead.',
829+
);
830+
});
831+
832+
it('works with generateController directly when allowUnsupported is true', async () => {
833+
const pipeline = await parsePipeline(xmlWithUnconvertable, 'Test');
834+
const analysis = analyzePipeline(pipeline);
835+
const code = generateController(pipeline, analysis, {allowUnsupported: true});
836+
837+
expect(code).to.include('// UNSUPPORTED: GetLastVisitedProducts');
838+
expect(code).to.include('// TODO: Manual conversion required');
839+
});
840+
});
783841
});

0 commit comments

Comments
 (0)