@salesforce/b2c-tooling-sdk 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/xsd/abtest.xsd +14 -1
- package/data/xsd/bmext.xsd +18 -0
- package/data/xsd/commercefeaturestate.xsd +101 -0
- package/data/xsd/index.json +10 -2
- package/data/xsd/library.xsd +33 -0
- package/data/xsd/order.xsd +2 -0
- package/data/xsd/pagemetatag.xsd +1 -0
- package/data/xsd/search2.xsd +39 -0
- package/data/xsd/sort.xsd +31 -1
- package/data/xsd/storefronts.xsd +104 -0
- package/dist/cjs/cli/lifecycle.d.ts +1 -1
- package/dist/cjs/cli/lifecycle.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/operations/cap/index.d.ts +56 -0
- package/dist/cjs/operations/cap/index.js +57 -0
- package/dist/cjs/operations/cap/index.js.map +1 -0
- package/dist/cjs/operations/cap/install.d.ts +53 -0
- package/dist/cjs/operations/cap/install.js +181 -0
- package/dist/cjs/operations/cap/install.js.map +1 -0
- package/dist/cjs/operations/cap/list.d.ts +92 -0
- package/dist/cjs/operations/cap/list.js +230 -0
- package/dist/cjs/operations/cap/list.js.map +1 -0
- package/dist/cjs/operations/cap/package.d.ts +39 -0
- package/dist/cjs/operations/cap/package.js +77 -0
- package/dist/cjs/operations/cap/package.js.map +1 -0
- package/dist/cjs/operations/cap/pull.d.ts +21 -0
- package/dist/cjs/operations/cap/pull.js +77 -0
- package/dist/cjs/operations/cap/pull.js.map +1 -0
- package/dist/cjs/operations/cap/uninstall.d.ts +46 -0
- package/dist/cjs/operations/cap/uninstall.js +87 -0
- package/dist/cjs/operations/cap/uninstall.js.map +1 -0
- package/dist/cjs/operations/cap/validate.d.ts +47 -0
- package/dist/cjs/operations/cap/validate.js +235 -0
- package/dist/cjs/operations/cap/validate.js.map +1 -0
- package/dist/cjs/operations/code/deploy.d.ts +17 -1
- package/dist/cjs/operations/code/deploy.js +21 -2
- package/dist/cjs/operations/code/deploy.js.map +1 -1
- package/dist/cjs/operations/code/download.d.ts +66 -0
- package/dist/cjs/operations/code/download.js +180 -0
- package/dist/cjs/operations/code/download.js.map +1 -0
- package/dist/cjs/operations/code/index.d.ts +7 -1
- package/dist/cjs/operations/code/index.js +6 -0
- package/dist/cjs/operations/code/index.js.map +1 -1
- package/dist/cjs/operations/jobs/site-archive.d.ts +1 -0
- package/dist/cjs/operations/jobs/site-archive.js +30 -39
- package/dist/cjs/operations/jobs/site-archive.js.map +1 -1
- package/dist/cjs/operations/util/zip.d.ts +5 -0
- package/dist/cjs/operations/util/zip.js +25 -0
- package/dist/cjs/operations/util/zip.js.map +1 -0
- package/dist/cjs/plugins/loader.js +6 -1
- package/dist/cjs/plugins/loader.js.map +1 -1
- package/dist/cjs/skills/github.js +143 -49
- package/dist/cjs/skills/github.js.map +1 -1
- package/dist/cjs/skills/installer.d.ts +1 -0
- package/dist/cjs/skills/installer.js.map +1 -1
- package/dist/cjs/telemetry/index.d.ts +8 -0
- package/dist/cjs/telemetry/index.js.map +1 -1
- package/dist/esm/cli/lifecycle.d.ts +1 -1
- package/dist/esm/cli/lifecycle.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operations/cap/index.d.ts +56 -0
- package/dist/esm/operations/cap/index.js +57 -0
- package/dist/esm/operations/cap/index.js.map +1 -0
- package/dist/esm/operations/cap/install.d.ts +53 -0
- package/dist/esm/operations/cap/install.js +183 -0
- package/dist/esm/operations/cap/install.js.map +1 -0
- package/dist/esm/operations/cap/list.d.ts +92 -0
- package/dist/esm/operations/cap/list.js +231 -0
- package/dist/esm/operations/cap/list.js.map +1 -0
- package/dist/esm/operations/cap/package.d.ts +39 -0
- package/dist/esm/operations/cap/package.js +78 -0
- package/dist/esm/operations/cap/package.js.map +1 -0
- package/dist/esm/operations/cap/pull.d.ts +21 -0
- package/dist/esm/operations/cap/pull.js +78 -0
- package/dist/esm/operations/cap/pull.js.map +1 -0
- package/dist/esm/operations/cap/uninstall.d.ts +46 -0
- package/dist/esm/operations/cap/uninstall.js +98 -0
- package/dist/esm/operations/cap/uninstall.js.map +1 -0
- package/dist/esm/operations/cap/validate.d.ts +47 -0
- package/dist/esm/operations/cap/validate.js +235 -0
- package/dist/esm/operations/cap/validate.js.map +1 -0
- package/dist/esm/operations/code/deploy.d.ts +17 -1
- package/dist/esm/operations/code/deploy.js +21 -2
- package/dist/esm/operations/code/deploy.js.map +1 -1
- package/dist/esm/operations/code/download.d.ts +66 -0
- package/dist/esm/operations/code/download.js +180 -0
- package/dist/esm/operations/code/download.js.map +1 -0
- package/dist/esm/operations/code/index.d.ts +7 -1
- package/dist/esm/operations/code/index.js +6 -0
- package/dist/esm/operations/code/index.js.map +1 -1
- package/dist/esm/operations/jobs/site-archive.d.ts +1 -0
- package/dist/esm/operations/jobs/site-archive.js +30 -39
- package/dist/esm/operations/jobs/site-archive.js.map +1 -1
- package/dist/esm/operations/util/zip.d.ts +5 -0
- package/dist/esm/operations/util/zip.js +26 -0
- package/dist/esm/operations/util/zip.js.map +1 -0
- package/dist/esm/plugins/loader.js +6 -1
- package/dist/esm/plugins/loader.js.map +1 -1
- package/dist/esm/skills/github.js +143 -49
- package/dist/esm/skills/github.js.map +1 -1
- package/dist/esm/skills/installer.d.ts +1 -0
- package/dist/esm/skills/installer.js.map +1 -1
- package/dist/esm/telemetry/index.d.ts +8 -0
- package/dist/esm/telemetry/index.js.map +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CommerceAppManifest } from './validate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for CAP packaging.
|
|
4
|
+
*/
|
|
5
|
+
export interface CommerceAppPackageOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Output path for the zip file.
|
|
8
|
+
* - If a directory: zip is written to `{outputPath}/{id}-v{version}.zip`
|
|
9
|
+
* - If a .zip path: written to that exact location
|
|
10
|
+
* - Default: current working directory
|
|
11
|
+
*/
|
|
12
|
+
outputPath?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Result of CAP packaging.
|
|
16
|
+
*/
|
|
17
|
+
export interface CommerceAppPackageResult {
|
|
18
|
+
/** Absolute path to the produced zip file. */
|
|
19
|
+
outputPath: string;
|
|
20
|
+
/** Parsed manifest. */
|
|
21
|
+
manifest: CommerceAppManifest;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Packages a CAP directory into a distributable .zip file.
|
|
25
|
+
*
|
|
26
|
+
* The zip root directory is named `{id}-v{version}/` as required by the CAP spec.
|
|
27
|
+
* Reads commerce-app.json to determine the app name and version.
|
|
28
|
+
*
|
|
29
|
+
* @param sourceDir - Path to the CAP directory
|
|
30
|
+
* @param options - Packaging options
|
|
31
|
+
* @returns Result with the output zip path and manifest
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const result = await commerceAppPackage('./commerce-avalara-tax-app-v0.2.5');
|
|
36
|
+
* console.log(`Packaged to: ${result.outputPath}`);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function commerceAppPackage(sourceDir: string, options?: CommerceAppPackageOptions): Promise<CommerceAppPackageResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Commerce App Package (CAP) packaging.
|
|
8
|
+
*
|
|
9
|
+
* Zips a CAP directory into a distributable .zip file with the correct
|
|
10
|
+
* root directory naming convention ({id}-v{version}/).
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import JSZip from 'jszip';
|
|
15
|
+
import { getLogger } from '../../logging/logger.js';
|
|
16
|
+
import { addDirectoryToZip } from '../util/zip.js';
|
|
17
|
+
import { readManifest } from './install.js';
|
|
18
|
+
/**
|
|
19
|
+
* Packages a CAP directory into a distributable .zip file.
|
|
20
|
+
*
|
|
21
|
+
* The zip root directory is named `{id}-v{version}/` as required by the CAP spec.
|
|
22
|
+
* Reads commerce-app.json to determine the app name and version.
|
|
23
|
+
*
|
|
24
|
+
* @param sourceDir - Path to the CAP directory
|
|
25
|
+
* @param options - Packaging options
|
|
26
|
+
* @returns Result with the output zip path and manifest
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const result = await commerceAppPackage('./commerce-avalara-tax-app-v0.2.5');
|
|
31
|
+
* console.log(`Packaged to: ${result.outputPath}`);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export async function commerceAppPackage(sourceDir, options = {}) {
|
|
35
|
+
const logger = getLogger();
|
|
36
|
+
if (!fs.existsSync(sourceDir)) {
|
|
37
|
+
throw new Error(`Source directory not found: ${sourceDir}`);
|
|
38
|
+
}
|
|
39
|
+
if (!fs.statSync(sourceDir).isDirectory()) {
|
|
40
|
+
throw new Error(`Source must be a directory: ${sourceDir}`);
|
|
41
|
+
}
|
|
42
|
+
const manifest = readManifest(sourceDir);
|
|
43
|
+
if (!manifest.id || !manifest.version) {
|
|
44
|
+
throw new Error('commerce-app.json must have "id" and "version" fields');
|
|
45
|
+
}
|
|
46
|
+
const archiveDirName = `${manifest.id}-v${manifest.version}`;
|
|
47
|
+
const zipFilename = `${archiveDirName}.zip`;
|
|
48
|
+
// Determine output path
|
|
49
|
+
let outputZipPath;
|
|
50
|
+
if (!options.outputPath) {
|
|
51
|
+
outputZipPath = path.resolve(process.cwd(), zipFilename);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const resolved = path.resolve(options.outputPath);
|
|
55
|
+
if (resolved.endsWith('.zip')) {
|
|
56
|
+
outputZipPath = resolved;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
outputZipPath = path.join(resolved, zipFilename);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Ensure output directory exists
|
|
63
|
+
await fs.promises.mkdir(path.dirname(outputZipPath), { recursive: true });
|
|
64
|
+
logger.debug({ sourceDir, outputPath: outputZipPath }, `Packaging CAP: ${archiveDirName}`);
|
|
65
|
+
const zip = new JSZip();
|
|
66
|
+
const rootFolder = zip.folder(archiveDirName);
|
|
67
|
+
await addDirectoryToZip(rootFolder, sourceDir);
|
|
68
|
+
const buffer = await zip.generateAsync({
|
|
69
|
+
type: 'nodebuffer',
|
|
70
|
+
compression: 'DEFLATE',
|
|
71
|
+
compressionOptions: { level: 9 },
|
|
72
|
+
});
|
|
73
|
+
await fs.promises.writeFile(outputZipPath, buffer);
|
|
74
|
+
logger.debug({ outputPath: outputZipPath }, `CAP packaged to: ${outputZipPath}`);
|
|
75
|
+
return { outputPath: outputZipPath, manifest };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=package.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.js","sourceRoot":"","sources":["../../../../src/operations/cap/package.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { B2CInstance } from '../../instance/index.js';
|
|
2
|
+
import type { CommerceFeatureState } from './list.js';
|
|
3
|
+
export interface PullCommerceAppsOptions {
|
|
4
|
+
outputDir?: string;
|
|
5
|
+
}
|
|
6
|
+
export type PullSource = 'instance' | 'github';
|
|
7
|
+
export interface PulledApp {
|
|
8
|
+
featureName: string;
|
|
9
|
+
version: string;
|
|
10
|
+
domain: string;
|
|
11
|
+
source: PullSource;
|
|
12
|
+
extractedPath: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PullCommerceAppsResult {
|
|
15
|
+
pulled: PulledApp[];
|
|
16
|
+
failed: Array<{
|
|
17
|
+
featureName: string;
|
|
18
|
+
error: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function pullCommerceApps(instance: B2CInstance, features: CommerceFeatureState[], options?: PullCommerceAppsOptions): Promise<PullCommerceAppsResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import JSZip from 'jszip';
|
|
9
|
+
import { getLogger } from '../../logging/logger.js';
|
|
10
|
+
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/SalesforceCommerceCloud/commerce-apps/main';
|
|
11
|
+
export async function pullCommerceApps(instance, features, options = {}) {
|
|
12
|
+
const logger = getLogger();
|
|
13
|
+
const outputDir = path.resolve(options.outputDir ?? 'commerce-apps');
|
|
14
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
15
|
+
const pulled = [];
|
|
16
|
+
const failed = [];
|
|
17
|
+
for (const feature of features) {
|
|
18
|
+
const { featureName, featureVersionId, featureDomain } = feature;
|
|
19
|
+
if (!featureVersionId) {
|
|
20
|
+
failed.push({ featureName, error: 'No version available' });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const zipFilename = `${featureName}-v${featureVersionId}.zip`;
|
|
24
|
+
const webdavPath = `Impex/commerce-apps/${zipFilename}`;
|
|
25
|
+
let zipData = null;
|
|
26
|
+
let source = 'instance';
|
|
27
|
+
// Try instance first
|
|
28
|
+
if (await instance.webdav.exists(webdavPath)) {
|
|
29
|
+
logger.debug({ path: webdavPath }, `Downloading ${zipFilename} from instance`);
|
|
30
|
+
zipData = Buffer.from(await instance.webdav.get(webdavPath));
|
|
31
|
+
}
|
|
32
|
+
// Fall back to GitHub
|
|
33
|
+
if (!zipData) {
|
|
34
|
+
const githubUrl = `${GITHUB_RAW_BASE}/${featureDomain}/${featureName}/${zipFilename}`;
|
|
35
|
+
logger.warn({ url: githubUrl }, `${zipFilename} not found on instance, trying GitHub`);
|
|
36
|
+
source = 'github';
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(githubUrl);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
failed.push({ featureName, error: `Not found on instance or GitHub (${githubUrl})` });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
zipData = Buffer.from(await response.arrayBuffer());
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
failed.push({ featureName, error: `GitHub download failed: ${msg}` });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Extract zip
|
|
52
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
53
|
+
const extractDir = path.join(outputDir, featureName);
|
|
54
|
+
await fs.promises.mkdir(extractDir, { recursive: true });
|
|
55
|
+
for (const [filePath, entry] of Object.entries(zip.files)) {
|
|
56
|
+
if (entry.dir)
|
|
57
|
+
continue;
|
|
58
|
+
// Strip the top-level archive directory (e.g. "avalara-tax-v1.1.0/...")
|
|
59
|
+
const parts = filePath.split('/');
|
|
60
|
+
const relativePath = parts.length > 1 ? parts.slice(1).join('/') : filePath;
|
|
61
|
+
const fullPath = path.join(extractDir, relativePath);
|
|
62
|
+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
63
|
+
const content = await entry.async('nodebuffer');
|
|
64
|
+
await fs.promises.writeFile(fullPath, content);
|
|
65
|
+
}
|
|
66
|
+
logger.debug({ featureName, extractDir }, `Extracted ${featureName} to ${extractDir}`);
|
|
67
|
+
pulled.push({
|
|
68
|
+
featureName,
|
|
69
|
+
version: featureVersionId,
|
|
70
|
+
domain: featureDomain,
|
|
71
|
+
source,
|
|
72
|
+
extractedPath: extractDir,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { pulled, failed };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=pull.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull.js","sourceRoot":"","sources":["../../../../src/operations/cap/pull.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commerce App Package (CAP) uninstallation.
|
|
3
|
+
*
|
|
4
|
+
* Runs the sfcc-uninstall-commerce-app system job to remove an installed CAP.
|
|
5
|
+
*/
|
|
6
|
+
import { B2CInstance } from '../../instance/index.js';
|
|
7
|
+
import { type JobExecution, type WaitForJobOptions } from '../jobs/run.js';
|
|
8
|
+
/**
|
|
9
|
+
* Options for CAP uninstallation.
|
|
10
|
+
*/
|
|
11
|
+
export interface CommerceAppUninstallOptions {
|
|
12
|
+
/** Target site ID to uninstall the app from. */
|
|
13
|
+
siteId: string;
|
|
14
|
+
/** Wait options for job completion. */
|
|
15
|
+
waitOptions?: WaitForJobOptions;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result of a CAP uninstallation.
|
|
19
|
+
*/
|
|
20
|
+
export interface CommerceAppUninstallResult {
|
|
21
|
+
/** Job execution details. */
|
|
22
|
+
execution: JobExecution;
|
|
23
|
+
/** App name that was uninstalled. */
|
|
24
|
+
appName: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Uninstalls a Commerce App from a B2C Commerce instance.
|
|
28
|
+
*
|
|
29
|
+
* Executes the sfcc-uninstall-commerce-app system job which removes cartridges,
|
|
30
|
+
* IMPEX data, and configuration associated with the app from the target site.
|
|
31
|
+
*
|
|
32
|
+
* @param instance - B2C instance to uninstall from
|
|
33
|
+
* @param appName - App ID (from commerce-app.json "id" field, e.g. "avalara-tax")
|
|
34
|
+
* @param appDomain - App domain (e.g. "tax", "shipping")
|
|
35
|
+
* @param options - Uninstall options including required siteId
|
|
36
|
+
* @returns Uninstall result with job execution details
|
|
37
|
+
* @throws JobExecutionError if the uninstall job fails
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const result = await commerceAppUninstall(instance, 'avalara-tax', 'tax', {
|
|
42
|
+
* siteId: 'RefArch',
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function commerceAppUninstall(instance: B2CInstance, appName: string, appDomain: string, options: CommerceAppUninstallOptions): Promise<CommerceAppUninstallResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getLogger } from '../../logging/logger.js';
|
|
2
|
+
import { waitForJob, JobExecutionError, getJobLog } from '../jobs/run.js';
|
|
3
|
+
import { normalizeSiteId } from './install.js';
|
|
4
|
+
const UNINSTALL_JOB_ID = 'sfcc-uninstall-commerce-app';
|
|
5
|
+
/**
|
|
6
|
+
* Uninstalls a Commerce App from a B2C Commerce instance.
|
|
7
|
+
*
|
|
8
|
+
* Executes the sfcc-uninstall-commerce-app system job which removes cartridges,
|
|
9
|
+
* IMPEX data, and configuration associated with the app from the target site.
|
|
10
|
+
*
|
|
11
|
+
* @param instance - B2C instance to uninstall from
|
|
12
|
+
* @param appName - App ID (from commerce-app.json "id" field, e.g. "avalara-tax")
|
|
13
|
+
* @param appDomain - App domain (e.g. "tax", "shipping")
|
|
14
|
+
* @param options - Uninstall options including required siteId
|
|
15
|
+
* @returns Uninstall result with job execution details
|
|
16
|
+
* @throws JobExecutionError if the uninstall job fails
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const result = await commerceAppUninstall(instance, 'avalara-tax', 'tax', {
|
|
21
|
+
* siteId: 'RefArch',
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function commerceAppUninstall(instance, appName, appDomain, options) {
|
|
26
|
+
const logger = getLogger();
|
|
27
|
+
const { siteId: rawSiteId, waitOptions } = options;
|
|
28
|
+
const siteId = normalizeSiteId(rawSiteId);
|
|
29
|
+
logger.debug({ jobId: UNINSTALL_JOB_ID, appName, siteId }, `Executing ${UNINSTALL_JOB_ID} job`);
|
|
30
|
+
let execution;
|
|
31
|
+
// Try direct body format first (standard OCAPI format)
|
|
32
|
+
const { data, error } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
|
|
33
|
+
params: { path: { job_id: UNINSTALL_JOB_ID } },
|
|
34
|
+
body: {
|
|
35
|
+
app_name: appName,
|
|
36
|
+
app_domain: appDomain,
|
|
37
|
+
site_id: siteId,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
if (error?.fault?.type === 'UnknownPropertyException' &&
|
|
41
|
+
error.fault.arguments?.document === 'job_execution_request') {
|
|
42
|
+
// Retry with parameters format (internal/support users)
|
|
43
|
+
logger.warn('Retrying with parameters format for internal users');
|
|
44
|
+
const { data: retryData, error: retryError } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
|
|
45
|
+
params: { path: { job_id: UNINSTALL_JOB_ID } },
|
|
46
|
+
body: {
|
|
47
|
+
parameters: [
|
|
48
|
+
{ name: 'AppName', value: appName },
|
|
49
|
+
{ name: 'AppDomain', value: appDomain },
|
|
50
|
+
{ name: 'SiteId', value: siteId },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (retryError || !retryData) {
|
|
55
|
+
throw new Error(retryError?.fault?.message ?? 'Failed to start uninstall job');
|
|
56
|
+
}
|
|
57
|
+
execution = retryData;
|
|
58
|
+
}
|
|
59
|
+
else if (error || !data) {
|
|
60
|
+
throw new Error(error?.fault?.message ?? 'Failed to start uninstall job');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
execution = data;
|
|
64
|
+
}
|
|
65
|
+
logger.debug({ jobId: UNINSTALL_JOB_ID, executionId: execution.id }, `Uninstall job started: ${execution.id}`);
|
|
66
|
+
let finalExecution;
|
|
67
|
+
try {
|
|
68
|
+
finalExecution = await waitForJob(instance, UNINSTALL_JOB_ID, execution.id, waitOptions);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof JobExecutionError) {
|
|
72
|
+
try {
|
|
73
|
+
const log = await getJobLog(instance, err.execution);
|
|
74
|
+
logger.error({ jobId: UNINSTALL_JOB_ID, log }, `Job log:\n${log}`);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
logger.error({ jobId: UNINSTALL_JOB_ID }, 'Could not retrieve job log');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
execution: finalExecution,
|
|
84
|
+
appName,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=uninstall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uninstall.js","sourceRoot":"","sources":["../../../../src/operations/cap/uninstall.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest from commerce-app.json.
|
|
3
|
+
*/
|
|
4
|
+
export interface CommerceAppManifest {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
domain: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
publisher?: {
|
|
11
|
+
name: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
support?: string;
|
|
14
|
+
};
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result of CAP validation.
|
|
19
|
+
*/
|
|
20
|
+
export interface CapValidationResult {
|
|
21
|
+
/** Whether the CAP is valid (no errors). */
|
|
22
|
+
valid: boolean;
|
|
23
|
+
/** Blocking errors — a CAP with errors cannot be installed. */
|
|
24
|
+
errors: string[];
|
|
25
|
+
/** Advisory warnings — a CAP with warnings can still be installed. */
|
|
26
|
+
warnings: string[];
|
|
27
|
+
/** Parsed manifest from commerce-app.json (if parseable). */
|
|
28
|
+
manifest?: CommerceAppManifest;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validates a Commerce App Package (CAP) directory or zip file.
|
|
32
|
+
*
|
|
33
|
+
* Checks required files, manifest schema, and cartridge structure rules.
|
|
34
|
+
* This is a purely local operation — no B2C instance required.
|
|
35
|
+
*
|
|
36
|
+
* @param target - Path to a CAP directory or .zip file
|
|
37
|
+
* @returns Validation result with errors and warnings
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const result = await validateCap('./my-commerce-app-v1.0.0');
|
|
42
|
+
* if (!result.valid) {
|
|
43
|
+
* console.error('Validation errors:', result.errors);
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateCap(target: string): Promise<CapValidationResult>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validation logic for Commerce App Packages (CAPs).
|
|
8
|
+
*
|
|
9
|
+
* Performs structural and schema validation of a CAP directory or zip file
|
|
10
|
+
* without requiring a live B2C instance.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import JSZip from 'jszip';
|
|
16
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
17
|
+
/**
|
|
18
|
+
* Validates a Commerce App Package (CAP) directory or zip file.
|
|
19
|
+
*
|
|
20
|
+
* Checks required files, manifest schema, and cartridge structure rules.
|
|
21
|
+
* This is a purely local operation — no B2C instance required.
|
|
22
|
+
*
|
|
23
|
+
* @param target - Path to a CAP directory or .zip file
|
|
24
|
+
* @returns Validation result with errors and warnings
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const result = await validateCap('./my-commerce-app-v1.0.0');
|
|
29
|
+
* if (!result.valid) {
|
|
30
|
+
* console.error('Validation errors:', result.errors);
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export async function validateCap(target) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
const warnings = [];
|
|
37
|
+
let manifest;
|
|
38
|
+
if (!fs.existsSync(target)) {
|
|
39
|
+
return { valid: false, errors: [`Target not found: ${target}`], warnings };
|
|
40
|
+
}
|
|
41
|
+
const stat = fs.statSync(target);
|
|
42
|
+
let capDir;
|
|
43
|
+
let tempDir;
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
capDir = target;
|
|
46
|
+
}
|
|
47
|
+
else if (stat.isFile() && target.endsWith('.zip')) {
|
|
48
|
+
// Extract zip to temp dir for inspection
|
|
49
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'b2c-cap-validate-'));
|
|
50
|
+
try {
|
|
51
|
+
await extractZipToDir(target, tempDir);
|
|
52
|
+
// The zip root should be a single directory
|
|
53
|
+
const entries = fs.readdirSync(tempDir);
|
|
54
|
+
if (entries.length === 1 && fs.statSync(path.join(tempDir, entries[0])).isDirectory()) {
|
|
55
|
+
capDir = path.join(tempDir, entries[0]);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
capDir = tempDir;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
cleanupTemp(tempDir);
|
|
64
|
+
return { valid: false, errors: [`Failed to read zip: ${msg}`], warnings };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return { valid: false, errors: [`Target must be a directory or .zip file: ${target}`], warnings };
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
// --- commerce-app.json ---
|
|
72
|
+
const manifestPath = path.join(capDir, 'commerce-app.json');
|
|
73
|
+
if (!fs.existsSync(manifestPath)) {
|
|
74
|
+
errors.push('Missing required file: commerce-app.json');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
try {
|
|
78
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
79
|
+
const requiredFields = ['id', 'name', 'version', 'domain'];
|
|
80
|
+
for (const field of requiredFields) {
|
|
81
|
+
if (!raw[field] || typeof raw[field] !== 'string') {
|
|
82
|
+
errors.push(`commerce-app.json: missing or invalid field "${field}"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (raw.version && typeof raw.version === 'string' && !SEMVER_RE.test(raw.version)) {
|
|
86
|
+
errors.push(`commerce-app.json: "version" must be a valid semver string (got "${raw.version}")`);
|
|
87
|
+
}
|
|
88
|
+
if (errors.filter((e) => e.startsWith('commerce-app.json:')).length === 0) {
|
|
89
|
+
manifest = raw;
|
|
90
|
+
}
|
|
91
|
+
// Check root dir naming convention
|
|
92
|
+
const dirName = path.basename(capDir);
|
|
93
|
+
if (manifest && dirName !== '.' && dirName !== tempDir) {
|
|
94
|
+
const expectedName = `${manifest.id}-v${manifest.version}`;
|
|
95
|
+
if (dirName !== expectedName) {
|
|
96
|
+
warnings.push(`Root directory "${dirName}" does not match expected convention "${expectedName}"`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
errors.push('commerce-app.json: file exists but is not valid JSON');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// --- README.md ---
|
|
105
|
+
if (!fs.existsSync(path.join(capDir, 'README.md'))) {
|
|
106
|
+
errors.push('Missing required file: README.md');
|
|
107
|
+
}
|
|
108
|
+
// --- app-configuration/tasksList.json ---
|
|
109
|
+
const tasksListPath = path.join(capDir, 'app-configuration', 'tasksList.json');
|
|
110
|
+
if (!fs.existsSync(tasksListPath)) {
|
|
111
|
+
errors.push('Missing required file: app-configuration/tasksList.json');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
try {
|
|
115
|
+
const tasks = JSON.parse(fs.readFileSync(tasksListPath, 'utf-8'));
|
|
116
|
+
if (!Array.isArray(tasks)) {
|
|
117
|
+
errors.push('app-configuration/tasksList.json: must be a JSON array');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
121
|
+
const task = tasks[i];
|
|
122
|
+
for (const field of ['taskNumber', 'name', 'description', 'link']) {
|
|
123
|
+
if (!task[field]) {
|
|
124
|
+
errors.push(`app-configuration/tasksList.json: task[${i}] missing field "${field}"`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
errors.push('app-configuration/tasksList.json: file exists but is not valid JSON');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// --- At least one substantive directory ---
|
|
135
|
+
const hasCartridges = fs.existsSync(path.join(capDir, 'cartridges'));
|
|
136
|
+
const hasStorefrontNext = fs.existsSync(path.join(capDir, 'storefront-next'));
|
|
137
|
+
const hasImpex = fs.existsSync(path.join(capDir, 'impex'));
|
|
138
|
+
if (!hasCartridges && !hasStorefrontNext && !hasImpex) {
|
|
139
|
+
errors.push('CAP must contain at least one of: cartridges/, storefront-next/, impex/');
|
|
140
|
+
}
|
|
141
|
+
// --- Cartridge rules ---
|
|
142
|
+
if (hasCartridges) {
|
|
143
|
+
const cartridgesDir = path.join(capDir, 'cartridges');
|
|
144
|
+
validateCartridges(cartridgesDir, errors);
|
|
145
|
+
}
|
|
146
|
+
// --- Optional warnings ---
|
|
147
|
+
if (!fs.existsSync(path.join(capDir, 'icons', 'icon.png'))) {
|
|
148
|
+
warnings.push('icons/icon.png not found (recommended for marketplace listing)');
|
|
149
|
+
}
|
|
150
|
+
if (hasImpex && !fs.existsSync(path.join(capDir, 'impex', 'uninstall'))) {
|
|
151
|
+
warnings.push('impex/uninstall/ not found (recommended for clean removal)');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
cleanupTemp(tempDir);
|
|
156
|
+
}
|
|
157
|
+
return { valid: errors.length === 0, errors, warnings, manifest };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Validates cartridge structure and rules.
|
|
161
|
+
* - No pipeline/ directories
|
|
162
|
+
* - No *.ds pipeline descriptor files
|
|
163
|
+
* - Site cartridges must not have controllers/
|
|
164
|
+
*/
|
|
165
|
+
function validateCartridges(cartridgesDir, errors) {
|
|
166
|
+
// Check entire cartridges tree for pipelines
|
|
167
|
+
walkDir(cartridgesDir, (filePath, name, isDir) => {
|
|
168
|
+
if (isDir && name === 'pipeline') {
|
|
169
|
+
const rel = path.relative(cartridgesDir, filePath);
|
|
170
|
+
errors.push(`Pipelines not allowed in CAPs: cartridges/${rel}`);
|
|
171
|
+
}
|
|
172
|
+
if (!isDir && name.endsWith('.ds')) {
|
|
173
|
+
const rel = path.relative(cartridgesDir, filePath);
|
|
174
|
+
errors.push(`Pipeline descriptor files not allowed in CAPs: cartridges/${rel}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Site cartridges must not contain controllers/
|
|
178
|
+
const siteCartridgesDir = path.join(cartridgesDir, 'site_cartridges');
|
|
179
|
+
if (fs.existsSync(siteCartridgesDir)) {
|
|
180
|
+
walkDir(siteCartridgesDir, (filePath, name, isDir) => {
|
|
181
|
+
if (isDir && name === 'controllers') {
|
|
182
|
+
const rel = path.relative(siteCartridgesDir, filePath);
|
|
183
|
+
errors.push(`Site cartridges must not contain controllers/ (use BM cartridges instead): site_cartridges/${rel}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Walks a directory tree, calling callback for each entry.
|
|
190
|
+
* Stops recursing into a directory if callback returns false.
|
|
191
|
+
*/
|
|
192
|
+
function walkDir(dir, cb) {
|
|
193
|
+
if (!fs.existsSync(dir))
|
|
194
|
+
return;
|
|
195
|
+
let entries;
|
|
196
|
+
try {
|
|
197
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
const fullPath = path.join(dir, entry.name);
|
|
204
|
+
cb(fullPath, entry.name, entry.isDirectory());
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
walkDir(fullPath, cb);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function extractZipToDir(zipPath, destDir) {
|
|
211
|
+
const data = await fs.promises.readFile(zipPath);
|
|
212
|
+
const zip = await JSZip.loadAsync(data);
|
|
213
|
+
for (const [relPath, entry] of Object.entries(zip.files)) {
|
|
214
|
+
const fullPath = path.join(destDir, relPath);
|
|
215
|
+
if (entry.dir) {
|
|
216
|
+
await fs.promises.mkdir(fullPath, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
220
|
+
const content = await entry.async('nodebuffer');
|
|
221
|
+
await fs.promises.writeFile(fullPath, content);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function cleanupTemp(tempDir) {
|
|
226
|
+
if (tempDir) {
|
|
227
|
+
try {
|
|
228
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// ignore cleanup errors
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=validate.js.map
|