Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/vscode-scaffold-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/b2c-tooling-sdk': minor
---

Add `detectSourceFromPath()` for context-aware scaffold parameter detection, `cartridgePathForDestination()` export, and `builtInScaffoldsDir` option on `createScaffoldRegistry()` for bundled consumers
4 changes: 4 additions & 0 deletions packages/b2c-tooling-sdk/src/scaffold/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,14 @@ export {
resolveRemoteSource,
isRemoteSource,
validateAgainstSource,
cartridgePathForDestination,
detectSourceFromPath,
} from './sources.js';
export type {SourceDetectionResult} from './sources.js';

// Registry
export {ScaffoldRegistry, createScaffoldRegistry} from './registry.js';
export type {ScaffoldRegistryOptions} from './registry.js';

// Engine
export {
Expand Down
25 changes: 7 additions & 18 deletions packages/b2c-tooling-sdk/src/scaffold/parameter-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/

import path from 'node:path';
import type {B2CInstance} from '../instance/index.js';
import type {Scaffold, ScaffoldParameter, ScaffoldChoice} from './types.js';
import {evaluateCondition} from './validators.js';
import {resolveLocalSource, resolveRemoteSource, isRemoteSource, validateAgainstSource} from './sources.js';
import {
resolveLocalSource,
resolveRemoteSource,
isRemoteSource,
validateAgainstSource,
cartridgePathForDestination,
} from './sources.js';

/**
* Options for resolving scaffold parameters.
Expand Down Expand Up @@ -64,22 +69,6 @@ export interface ResolvedParameterSchema {
warning?: string;
}

/**
* Path to use for scaffold destination so files are generated under outputDir (e.g. working directory).
* Returns a path relative to projectRoot when the cartridge is under projectRoot, so the executor
* joins with outputDir instead of ignoring it. Otherwise returns the absolute path.
*/
function cartridgePathForDestination(absolutePath: string, projectRoot: string): string {
const normalizedRoot = path.resolve(projectRoot);
const normalizedPath = path.resolve(absolutePath);
const relative = path.relative(normalizedRoot, normalizedPath);
// Use relative path only when cartridge is under projectRoot (no leading '..')
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
return relative;
}
return absolutePath;
}

/**
* Resolve scaffold parameters by:
* 1. Validating provided variables against sources
Expand Down
25 changes: 22 additions & 3 deletions packages/b2c-tooling-sdk/src/scaffold/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,30 @@ function filterScaffolds(scaffolds: Scaffold[], options: ScaffoldDiscoveryOption
return filtered;
}

/**
* Options for creating a scaffold registry
*/
export interface ScaffoldRegistryOptions {
/**
* Override the built-in scaffolds directory. Useful for bundled environments
* (e.g. VS Code extensions) where the SDK's data files are copied to a
* different location. Defaults to the SDK's own `data/scaffolds/` directory.
*/
builtInScaffoldsDir?: string;
}

/**
* Scaffold registry for discovering and managing scaffolds
*/
export class ScaffoldRegistry {
private providers: ScaffoldProvider[] = [];
private transformers: ScaffoldTransformer[] = [];
private scaffoldCache: Map<string, Scaffold[]> = new Map();
private readonly builtInScaffoldsDir: string;

constructor(options?: ScaffoldRegistryOptions) {
this.builtInScaffoldsDir = options?.builtInScaffoldsDir ?? SCAFFOLDS_DATA_DIR;
}

/**
* Add scaffold providers
Expand Down Expand Up @@ -179,7 +196,7 @@ export class ScaffoldRegistry {
}

// 2. Built-in scaffolds (lowest priority for built-ins)
const builtInScaffolds = await discoverScaffoldsFromDir(SCAFFOLDS_DATA_DIR, 'built-in');
const builtInScaffolds = await discoverScaffoldsFromDir(this.builtInScaffoldsDir, 'built-in');
allScaffolds.push(...builtInScaffolds);

// 3. User scaffolds (~/.b2c/scaffolds/)
Expand Down Expand Up @@ -258,7 +275,9 @@ export class ScaffoldRegistry {

/**
* Create a new scaffold registry instance
*
* @param options - Registry options (e.g. override built-in scaffolds directory)
*/
export function createScaffoldRegistry(): ScaffoldRegistry {
return new ScaffoldRegistry();
export function createScaffoldRegistry(options?: ScaffoldRegistryOptions): ScaffoldRegistry {
return new ScaffoldRegistry(options);
}
74 changes: 73 additions & 1 deletion packages/b2c-tooling-sdk/src/scaffold/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/

import fs from 'node:fs';
import path from 'node:path';
import {findCartridges} from '../operations/code/cartridges.js';
import type {B2CInstance} from '../instance/index.js';
import type {OcapiComponents} from '../clients/index.js';
import type {ScaffoldChoice, DynamicParameterSource, SourceResult} from './types.js';
import type {ScaffoldChoice, ScaffoldParameter, DynamicParameterSource, SourceResult} from './types.js';

/**
* Common B2C Commerce hook extension points.
Expand Down Expand Up @@ -129,3 +131,73 @@ export function validateAgainstSource(
// For hook-points and other sources, no validation (allow any value)
return {valid: true};
}

/**
* Path to use for scaffold destination so files are generated under outputDir (e.g. working directory).
* Returns a path relative to projectRoot when the cartridge is under projectRoot, so the executor
* joins with outputDir instead of ignoring it. Otherwise returns the absolute path.
*/
export function cartridgePathForDestination(absolutePath: string, projectRoot: string): string {
const normalizedRoot = path.resolve(projectRoot);
const normalizedPath = path.resolve(absolutePath);
const relative = path.relative(normalizedRoot, normalizedPath);
// Use relative path only when cartridge is under projectRoot (no leading '..')
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
return relative;
}
return absolutePath;
}

/**
* Result of detecting a source parameter value from a filesystem path.
*/
export interface SourceDetectionResult {
/** The resolved parameter value (e.g., cartridge name) */
value: string;
/** Companion variables to set (e.g., { cartridgeNamePath: "cartridges/app_custom" }) */
companionVariables: Record<string, string>;
}

/**
* Detect a parameter's source value from a filesystem context path.
*
* For `cartridges` source: walks up from `contextPath` looking for a `.project` file
* (cartridge marker), stopping at projectRoot. On match returns the cartridge name and
* companion path variable.
*
* @param param - The scaffold parameter with a `source` field
* @param contextPath - Filesystem path providing context (e.g., right-clicked folder)
* @param projectRoot - Project root directory
* @returns Detection result, or undefined if the source could not be detected
*/
export function detectSourceFromPath(
param: ScaffoldParameter,
contextPath: string,
projectRoot: string,
): SourceDetectionResult | undefined {
if (param.source !== 'cartridges') {
return undefined;
}

const normalizedRoot = path.resolve(projectRoot);
let current = path.resolve(contextPath);

// Walk up from contextPath, checking for .project at each level
while (current.length >= normalizedRoot.length) {
const projectFile = path.join(current, '.project');
if (fs.existsSync(projectFile)) {
const cartridgeName = path.basename(current);
const destPath = cartridgePathForDestination(current, projectRoot);
return {
value: cartridgeName,
companionVariables: {[`${param.name}Path`]: destPath},
};
}

const parent = path.dirname(current);
if (parent === current) break; // filesystem root
current = parent;
}

return undefined;
}
29 changes: 27 additions & 2 deletions packages/b2c-vs-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"onCommand:b2c-dx.promptAgent",
"onCommand:b2c-dx.listWebDav",
"onCommand:b2c-dx.scapiExplorer",
"onView:b2cSandboxExplorer"
"onView:b2cSandboxExplorer",
"onCommand:b2c-dx.scaffold.generate"
],
"main": "./dist/extension.js",
"contributes": {
Expand All @@ -53,6 +54,11 @@
"default": true,
"description": "Enable log tailing commands."
},
"b2c-dx.features.scaffold": {
"type": "boolean",
"default": true,
"description": "Enable scaffold generation commands."
},
"b2c-dx.logLevel": {
"type": "string",
"default": "info",
Expand Down Expand Up @@ -329,6 +335,12 @@
"title": "Import Site Archive",
"icon": "$(cloud-upload)",
"category": "B2C DX"
},
{
"command": "b2c-dx.scaffold.generate",
"title": "New from Scaffold...",
"icon": "$(file-code)",
"category": "B2C DX"
}
],
"menus": {
Expand Down Expand Up @@ -466,6 +478,13 @@
"group": "3_destructive@1"
}
],
"file/newFile": [
{
"command": "b2c-dx.scaffold.generate",
"group": "navigation",
"when": "workspaceFolderCount > 0"
}
],
"explorer/context": [
{
"command": "b2c-dx.webdav.download",
Expand All @@ -479,9 +498,15 @@
}
],
"b2c-dx.submenu": [
{
"command": "b2c-dx.scaffold.generate",
"when": "explorerResourceIsFolder",
"group": "1_scaffold"
},
{
"command": "b2c-dx.content.import",
"when": "explorerResourceIsFolder"
"when": "explorerResourceIsFolder",
"group": "2_import"
}
],
"commandPalette": [
Expand Down
13 changes: 13 additions & 0 deletions packages/b2c-vs-extension/scripts/esbuild-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ const REQUIRE_RESOLVE_PACKAGE_JSON_RE =
/require\d*\.resolve\s*\(\s*["']@salesforce\/b2c-tooling-sdk\/package\.json["']\s*\)/g;
const REQUIRE_RESOLVE_REPLACEMENT = "require('path').join(__dirname, 'package.json')";

// Copy SDK scaffold templates into dist/ so the extension can find them at runtime.
// The extension passes this path explicitly via createScaffoldRegistry({ builtInScaffoldsDir }).
const sdkRoot = path.join(pkgRoot, '..', 'b2c-tooling-sdk');

function copySdkScaffolds() {
const src = path.join(sdkRoot, 'data', 'scaffolds');
const dest = path.join(pkgRoot, 'dist', 'data', 'scaffolds');
if (!fs.existsSync(src)) return;
fs.cpSync(src, dest, {recursive: true});
}

function inlineSdkPackageJson() {
const outPath = path.join(pkgRoot, 'dist', 'extension.js');
let str = fs.readFileSync(outPath, 'utf8');
Expand Down Expand Up @@ -83,13 +94,15 @@ const buildOptions = {
};

if (watchMode) {
copySdkScaffolds();
const ctx = await esbuild.context(buildOptions);
await ctx.watch();
console.log('[esbuild] watching for changes...');
} else {
const result = await esbuild.build(buildOptions);

inlineSdkPackageJson();
copySdkScaffolds();

if (result.metafile && process.env.ANALYZE_BUNDLE) {
const metaPath = path.join(pkgRoot, 'dist', 'meta.json');
Expand Down
4 changes: 4 additions & 0 deletions packages/b2c-vs-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {registerContentTree} from './content-tree/index.js';
import {registerLogs} from './logs/index.js';
import {initializePlugins} from './plugins.js';
import {registerSandboxTree} from './sandbox-tree/index.js';
import {registerScaffold} from './scaffold/index.js';
import {registerWebDavTree} from './webdav-tree/index.js';

function getWebviewContent(context: vscode.ExtensionContext): string {
Expand Down Expand Up @@ -910,6 +911,9 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu
if (settings.get<boolean>('features.logTailing', true)) {
registerLogs(context, configProvider);
}
if (settings.get<boolean>('features.scaffold', true)) {
registerScaffold(context, configProvider, log);
}

// React to configuration changes
const configChangeListener = vscode.workspace.onDidChangeConfiguration((e) => {
Expand Down
21 changes: 21 additions & 0 deletions packages/b2c-vs-extension/src/scaffold/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* SPDX-License-Identifier: Apache-2
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/

import path from 'node:path';
import * as vscode from 'vscode';
import type {B2CExtensionConfig} from '../config-provider.js';
import {registerScaffoldCommands} from './scaffold-commands.js';

export function registerScaffold(
context: vscode.ExtensionContext,
configProvider: B2CExtensionConfig,
log: vscode.OutputChannel,
): void {
const builtInScaffoldsDir = path.join(context.extensionPath, 'dist', 'data', 'scaffolds');
log.appendLine(`[Scaffold] Built-in scaffolds dir: ${builtInScaffoldsDir}`);
const disposables = registerScaffoldCommands(configProvider, log, builtInScaffoldsDir);
context.subscriptions.push(...disposables);
}
Loading
Loading