@salesforce/storefront-next-dev 0.1.1 → 0.2.0-alpha.1
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/README.md +45 -36
- package/bin/run.js +12 -0
- package/dist/bundle.js +83 -0
- package/dist/cartridge-services/index.d.ts +2 -26
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +3 -336
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/create-bundle.js +107 -0
- package/dist/commands/create-instructions.js +174 -0
- package/dist/commands/create-storefront.js +210 -0
- package/dist/commands/deploy-cartridge.js +52 -0
- package/dist/commands/dev.js +122 -0
- package/dist/commands/extensions/create.js +38 -0
- package/dist/commands/extensions/install.js +44 -0
- package/dist/commands/extensions/list.js +21 -0
- package/dist/commands/extensions/remove.js +38 -0
- package/dist/commands/generate-cartridge.js +35 -0
- package/dist/commands/prepare-local.js +30 -0
- package/dist/commands/preview.js +101 -0
- package/dist/commands/push.js +139 -0
- package/dist/config.js +87 -0
- package/dist/configs/react-router.config.js +3 -1
- package/dist/configs/react-router.config.js.map +1 -1
- package/dist/dependency-utils.js +314 -0
- package/dist/entry/client.d.ts +1 -0
- package/dist/entry/client.js +28 -0
- package/dist/entry/client.js.map +1 -0
- package/dist/entry/server.d.ts +15 -0
- package/dist/entry/server.d.ts.map +1 -0
- package/dist/entry/server.js +35 -0
- package/dist/entry/server.js.map +1 -0
- package/dist/flags.js +11 -0
- package/dist/generate-cartridge.js +620 -0
- package/dist/hooks/init.js +47 -0
- package/dist/index.d.ts +9 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +413 -621
- package/dist/index.js.map +1 -1
- package/dist/local-dev-setup.js +176 -0
- package/dist/logger.js +105 -0
- package/dist/manage-extensions.js +329 -0
- package/dist/mrt/ssr.mjs +21 -21
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +28 -28
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/server.js +425 -0
- package/dist/utils.js +126 -0
- package/package.json +44 -9
- package/dist/cli.js +0 -3393
- /package/{LICENSE.txt → LICENSE} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { a as printServerInfo, c as warn, i as printServerConfig, n as error, o as printShutdownMessage, r as info } from "../logger.js";
|
|
2
|
+
import { c as loadEnvFile } from "../utils.js";
|
|
3
|
+
import { n as getCommerceCloudApiUrl, r as loadProjectConfig, t as createServer } from "../server.js";
|
|
4
|
+
import { t as commonFlags } from "../flags.js";
|
|
5
|
+
import { Command, Flags } from "@oclif/core";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
|
|
11
|
+
//#region src/lib/preview.ts
|
|
12
|
+
/**
|
|
13
|
+
* Start the preview server with production build
|
|
14
|
+
*/
|
|
15
|
+
async function preview(options = {}) {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
const projectDir = path.resolve(options.projectDirectory || process.cwd());
|
|
18
|
+
const port = options.port || 3e3;
|
|
19
|
+
process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
|
|
20
|
+
process.env.EXTERNAL_DOMAIN_NAME = process.env.EXTERNAL_DOMAIN_NAME ?? `localhost:${port}`;
|
|
21
|
+
loadEnvFile(projectDir);
|
|
22
|
+
const buildPath = path.join(projectDir, "build", "server", "index.js");
|
|
23
|
+
if (!fs.existsSync(buildPath)) {
|
|
24
|
+
warn("Production build not found. Building project...");
|
|
25
|
+
info("Running: pnpm build");
|
|
26
|
+
try {
|
|
27
|
+
execSync("pnpm build", {
|
|
28
|
+
cwd: projectDir,
|
|
29
|
+
stdio: "inherit"
|
|
30
|
+
});
|
|
31
|
+
info("Build completed successfully");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
error(`Build failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (!fs.existsSync(buildPath)) {
|
|
37
|
+
error(`Build still not found at ${buildPath} after running build command`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
info(`Loading production build from ${buildPath}`);
|
|
42
|
+
const build = (await import(pathToFileURL(buildPath).href)).default;
|
|
43
|
+
const config = await loadProjectConfig(projectDir);
|
|
44
|
+
const server = (await createServer({
|
|
45
|
+
mode: "preview",
|
|
46
|
+
projectDirectory: projectDir,
|
|
47
|
+
config,
|
|
48
|
+
port,
|
|
49
|
+
build
|
|
50
|
+
})).listen(port, () => {
|
|
51
|
+
printServerInfo("preview", port, startTime, projectDir);
|
|
52
|
+
printServerConfig({
|
|
53
|
+
mode: "preview",
|
|
54
|
+
port,
|
|
55
|
+
enableProxy: true,
|
|
56
|
+
enableStaticServing: true,
|
|
57
|
+
enableCompression: true,
|
|
58
|
+
proxyPath: config.commerce.api.proxy,
|
|
59
|
+
proxyHost: getCommerceCloudApiUrl(config.commerce.api.shortCode),
|
|
60
|
+
shortCode: config.commerce.api.shortCode,
|
|
61
|
+
organizationId: config.commerce.api.organizationId,
|
|
62
|
+
clientId: config.commerce.api.clientId,
|
|
63
|
+
siteId: config.commerce.api.siteId
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
["SIGTERM", "SIGINT"].forEach((signal) => {
|
|
67
|
+
process.once(signal, () => {
|
|
68
|
+
printShutdownMessage();
|
|
69
|
+
server?.close(() => {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/commands/preview.ts
|
|
78
|
+
/**
|
|
79
|
+
* Preview server command - starts the preview server with production build.
|
|
80
|
+
*/
|
|
81
|
+
var Preview = class Preview extends Command {
|
|
82
|
+
static description = "Start preview server with production build (auto-builds if needed)";
|
|
83
|
+
static examples = ["<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> -d ./my-project -p 4000"];
|
|
84
|
+
static flags = {
|
|
85
|
+
...commonFlags,
|
|
86
|
+
port: Flags.integer({
|
|
87
|
+
char: "p",
|
|
88
|
+
description: "Port number (default: 3000)"
|
|
89
|
+
})
|
|
90
|
+
};
|
|
91
|
+
async run() {
|
|
92
|
+
const { flags } = await this.parse(Preview);
|
|
93
|
+
await preview({
|
|
94
|
+
projectDirectory: flags["project-directory"],
|
|
95
|
+
port: flags.port
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { Preview as default };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import "../logger.js";
|
|
2
|
+
import { n as getDefaultBuildDir, r as getDefaultMessage } from "../utils.js";
|
|
3
|
+
import { t as createBundle } from "../bundle.js";
|
|
4
|
+
import { a as buildMrtConfig, i as SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR, n as GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH, r as SFNEXT_BASE_CARTRIDGE_NAME, t as CARTRIDGES_BASE_DIR } from "../config.js";
|
|
5
|
+
import { t as generateMetadata } from "../generate-cartridge.js";
|
|
6
|
+
import { Flags } from "@oclif/core";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
import { MrtCommand } from "@salesforce/b2c-tooling-sdk/cli";
|
|
10
|
+
import { resolveConfig } from "@salesforce/b2c-tooling-sdk/config";
|
|
11
|
+
import { uploadCartridges } from "@salesforce/b2c-tooling-sdk/operations/code";
|
|
12
|
+
import { uploadBundle, waitForEnv } from "@salesforce/b2c-tooling-sdk/operations/mrt";
|
|
13
|
+
import { DEFAULT_MRT_ORIGIN, createMrtClient } from "@salesforce/b2c-tooling-sdk/clients";
|
|
14
|
+
|
|
15
|
+
//#region src/commands/push.ts
|
|
16
|
+
/**
|
|
17
|
+
* MRT Push command - builds and pushes bundle to Managed Runtime.
|
|
18
|
+
*
|
|
19
|
+
* Inherits MRT flags from MrtCommand:
|
|
20
|
+
* - --api-key: MRT API key (env: SFCC_MRT_API_KEY)
|
|
21
|
+
* - --project/-p: MRT project slug (env: SFCC_MRT_PROJECT)
|
|
22
|
+
* - --environment/-e: MRT target environment (env: SFCC_MRT_ENVIRONMENT)
|
|
23
|
+
* - --cloud-origin: MRT cloud origin URL (env: SFCC_MRT_CLOUD_ORIGIN)
|
|
24
|
+
* - --credentials-file: Path to MRT credentials file (env: MRT_CREDENTIALS_FILE)
|
|
25
|
+
* - --config: Path to dw.json config file (env: SFCC_CONFIG)
|
|
26
|
+
* - --instance/-i: Named instance from config (env: SFCC_INSTANCE)
|
|
27
|
+
*/
|
|
28
|
+
var Push = class Push extends MrtCommand {
|
|
29
|
+
static description = "Build and push bundle to Managed Runtime";
|
|
30
|
+
static examples = [
|
|
31
|
+
"<%= config.bin %> <%= command.id %>",
|
|
32
|
+
"<%= config.bin %> <%= command.id %> --project-directory ./my-project",
|
|
33
|
+
"<%= config.bin %> <%= command.id %> --project my-project --environment staging",
|
|
34
|
+
"<%= config.bin %> <%= command.id %> --wait"
|
|
35
|
+
];
|
|
36
|
+
static flags = {
|
|
37
|
+
...MrtCommand.baseFlags,
|
|
38
|
+
"build-directory": Flags.string({
|
|
39
|
+
char: "b",
|
|
40
|
+
description: "Build directory to push (default: auto-detected)"
|
|
41
|
+
}),
|
|
42
|
+
message: Flags.string({
|
|
43
|
+
char: "m",
|
|
44
|
+
description: "Bundle message (default: git branch:commit)"
|
|
45
|
+
}),
|
|
46
|
+
wait: Flags.boolean({
|
|
47
|
+
char: "w",
|
|
48
|
+
description: "Wait for deployment to complete",
|
|
49
|
+
default: false
|
|
50
|
+
}),
|
|
51
|
+
"project-slug": Flags.string({
|
|
52
|
+
char: "s",
|
|
53
|
+
description: "DEPRECATED: Use --project instead",
|
|
54
|
+
hidden: true
|
|
55
|
+
}),
|
|
56
|
+
target: Flags.string({
|
|
57
|
+
char: "t",
|
|
58
|
+
description: "DEPRECATED: Use --environment instead",
|
|
59
|
+
hidden: true
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
async run() {
|
|
63
|
+
const { flags } = await this.parse(Push);
|
|
64
|
+
const projectDirectory = flags["project-directory"] || process.cwd();
|
|
65
|
+
if (flags["project-slug"]) this.warn("Flag --project-slug is deprecated. Use --project instead.");
|
|
66
|
+
if (flags.target) this.warn("Flag --target is deprecated. Use --environment instead.");
|
|
67
|
+
const target = flags.environment || flags.target || this.resolvedConfig.values.mrtEnvironment;
|
|
68
|
+
if (flags.wait && !target) this.error("You must provide a target environment when using --wait (via --environment flag, SFCC_MRT_ENVIRONMENT env var, or dw.json)");
|
|
69
|
+
if (!fs.existsSync(projectDirectory)) this.error(`Project directory "${projectDirectory}" does not exist!`);
|
|
70
|
+
const projectSlug = flags.project || flags["project-slug"] || this.resolvedConfig.values.mrtProject;
|
|
71
|
+
if (!projectSlug || projectSlug.trim() === "") this.error("Project slug is required. Provide --project, set SFCC_MRT_PROJECT env var, or configure mrtProject in dw.json");
|
|
72
|
+
const buildDirectory = flags["build-directory"] ?? getDefaultBuildDir(projectDirectory);
|
|
73
|
+
if (!fs.existsSync(buildDirectory)) this.error(`Build directory "${buildDirectory}" does not exist!`);
|
|
74
|
+
if (GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH) await this.generateAndDeployCartridge(projectDirectory);
|
|
75
|
+
if (target) process.env.DEPLOY_TARGET = target;
|
|
76
|
+
this.requireMrtCredentials();
|
|
77
|
+
const config = buildMrtConfig(buildDirectory, projectDirectory);
|
|
78
|
+
const message = flags.message ?? getDefaultMessage(projectDirectory);
|
|
79
|
+
this.log(`Creating bundle for project: ${projectSlug}`);
|
|
80
|
+
if (target) this.log(`Target environment: ${target}`);
|
|
81
|
+
const bundle = await createBundle({
|
|
82
|
+
message,
|
|
83
|
+
ssr_parameters: config.ssrParameters,
|
|
84
|
+
ssr_only: config.ssrOnly,
|
|
85
|
+
ssr_shared: config.ssrShared,
|
|
86
|
+
buildDirectory,
|
|
87
|
+
projectDirectory,
|
|
88
|
+
projectSlug
|
|
89
|
+
});
|
|
90
|
+
const origin = this.resolvedConfig.values.mrtOrigin || DEFAULT_MRT_ORIGIN;
|
|
91
|
+
const client = createMrtClient({ origin }, this.getMrtAuth());
|
|
92
|
+
this.log(`Beginning upload to ${origin}`);
|
|
93
|
+
const result = await uploadBundle(client, projectSlug, bundle, target);
|
|
94
|
+
if (flags.wait && target) {
|
|
95
|
+
this.log("Bundle uploaded - waiting for deployment to complete");
|
|
96
|
+
await waitForEnv({
|
|
97
|
+
projectSlug,
|
|
98
|
+
slug: target,
|
|
99
|
+
origin
|
|
100
|
+
}, this.getMrtAuth());
|
|
101
|
+
this.log("Deployment complete!");
|
|
102
|
+
} else this.log("Bundle uploaded successfully!");
|
|
103
|
+
this.log(`Bundle ID: ${result.bundleId}`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generate and deploy cartridge metadata to B2C instance.
|
|
107
|
+
* This is a pre-MRT-push step that ensures Page Designer metadata is current.
|
|
108
|
+
*/
|
|
109
|
+
async generateAndDeployCartridge(projectDirectory) {
|
|
110
|
+
const metadataDir = path.join(projectDirectory, CARTRIDGES_BASE_DIR, SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR);
|
|
111
|
+
try {
|
|
112
|
+
this.log("Generating cartridge metadata before MRT push...");
|
|
113
|
+
if (!fs.existsSync(metadataDir)) fs.mkdirSync(metadataDir, { recursive: true });
|
|
114
|
+
await generateMetadata(projectDirectory, metadataDir);
|
|
115
|
+
this.log("Cartridge metadata generated successfully!");
|
|
116
|
+
this.log("Deploying cartridge to Commerce Cloud...");
|
|
117
|
+
const b2cConfig = resolveConfig({}, { workingDirectory: projectDirectory });
|
|
118
|
+
if (!b2cConfig.hasB2CInstanceConfig()) {
|
|
119
|
+
this.warn("B2C instance not configured, skipping cartridge deployment");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!b2cConfig.values.codeVersion) {
|
|
123
|
+
this.warn("Code version not configured, skipping cartridge deployment");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await uploadCartridges(b2cConfig.createB2CInstance(), [{
|
|
127
|
+
name: SFNEXT_BASE_CARTRIDGE_NAME,
|
|
128
|
+
src: path.join(projectDirectory, CARTRIDGES_BASE_DIR, SFNEXT_BASE_CARTRIDGE_NAME),
|
|
129
|
+
dest: SFNEXT_BASE_CARTRIDGE_NAME
|
|
130
|
+
}]);
|
|
131
|
+
this.log("Cartridge deployed successfully!");
|
|
132
|
+
} catch (cartridgeError) {
|
|
133
|
+
this.warn(`Failed to generate or deploy cartridge: ${cartridgeError.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
export { Push as default };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//#region src/mrt/utils.ts
|
|
2
|
+
const MRT_BUNDLE_TYPE_SSR = "ssr";
|
|
3
|
+
const MRT_STREAMING_ENTRY_FILE = "streamingHandler";
|
|
4
|
+
/**
|
|
5
|
+
* Gets the MRT entry file for the given mode
|
|
6
|
+
* @param mode - The mode to get the MRT entry file for
|
|
7
|
+
* @returns The MRT entry file for the given mode
|
|
8
|
+
*/
|
|
9
|
+
const getMrtEntryFile = (mode) => {
|
|
10
|
+
return process.env.MRT_BUNDLE_TYPE !== MRT_BUNDLE_TYPE_SSR && mode === "production" ? MRT_STREAMING_ENTRY_FILE : MRT_BUNDLE_TYPE_SSR;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/config.ts
|
|
15
|
+
const CARTRIDGES_BASE_DIR = "cartridges";
|
|
16
|
+
const SFNEXT_BASE_CARTRIDGE_NAME = "app_storefrontnext_base";
|
|
17
|
+
const SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR = `${SFNEXT_BASE_CARTRIDGE_NAME}/cartridge/experience`;
|
|
18
|
+
/**
|
|
19
|
+
* When enabled, automatically generates and deploys cartridge metadata before an MRT push.
|
|
20
|
+
* This is useful for keeping Page Designer metadata in sync with component changes.
|
|
21
|
+
*
|
|
22
|
+
* When enabled:
|
|
23
|
+
* 1. Generates cartridge metadata from decorated components
|
|
24
|
+
* 2. Deploys the cartridge to Commerce Cloud (requires dw.json configuration)
|
|
25
|
+
* 3. Proceeds with the MRT push
|
|
26
|
+
*
|
|
27
|
+
* To enable: Set this to `true` in your local config.ts
|
|
28
|
+
* Default: false (manual cartridge generation/deployment via `sfnext generate-cartridge` and `sfnext deploy-cartridge`)
|
|
29
|
+
*/
|
|
30
|
+
const GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH = false;
|
|
31
|
+
/**
|
|
32
|
+
* Build MRT SSR configuration for bundle deployment
|
|
33
|
+
*
|
|
34
|
+
* Defines which files should be:
|
|
35
|
+
* - Server-only (ssrOnly): Deployed only to Lambda functions
|
|
36
|
+
* - Shared (ssrShared): Deployed to both Lambda and CDN
|
|
37
|
+
*
|
|
38
|
+
* @param buildDirectory - Path to the build output directory
|
|
39
|
+
* @param projectDirectory - Path to the project root (reserved for future use)
|
|
40
|
+
* @returns MRT SSR configuration with glob patterns
|
|
41
|
+
*/
|
|
42
|
+
const buildMrtConfig = (_buildDirectory, _projectDirectory) => {
|
|
43
|
+
const ssrEntryPoint = getMrtEntryFile("production");
|
|
44
|
+
return {
|
|
45
|
+
ssrOnly: [
|
|
46
|
+
"server/**/*",
|
|
47
|
+
"loader.js",
|
|
48
|
+
`${ssrEntryPoint}.{js,mjs,cjs}`,
|
|
49
|
+
`${ssrEntryPoint}.{js,mjs,cjs}.map`,
|
|
50
|
+
"!static/**/*",
|
|
51
|
+
"sfnext-server-*.mjs",
|
|
52
|
+
"!**/*.stories.tsx",
|
|
53
|
+
"!**/*.stories.ts",
|
|
54
|
+
"!**/*-snapshot.tsx",
|
|
55
|
+
"!.storybook/**/*",
|
|
56
|
+
"!storybook-static/**/*",
|
|
57
|
+
"!**/__mocks__/**/*",
|
|
58
|
+
"!**/__snapshots__/**/*"
|
|
59
|
+
],
|
|
60
|
+
ssrShared: [
|
|
61
|
+
"client/**/*",
|
|
62
|
+
"static/**/*",
|
|
63
|
+
"**/*.css",
|
|
64
|
+
"**/*.png",
|
|
65
|
+
"**/*.jpg",
|
|
66
|
+
"**/*.jpeg",
|
|
67
|
+
"**/*.gif",
|
|
68
|
+
"**/*.svg",
|
|
69
|
+
"**/*.ico",
|
|
70
|
+
"**/*.woff",
|
|
71
|
+
"**/*.woff2",
|
|
72
|
+
"**/*.ttf",
|
|
73
|
+
"**/*.eot",
|
|
74
|
+
"!**/*.stories.tsx",
|
|
75
|
+
"!**/*.stories.ts",
|
|
76
|
+
"!**/*-snapshot.tsx",
|
|
77
|
+
"!.storybook/**/*",
|
|
78
|
+
"!storybook-static/**/*",
|
|
79
|
+
"!**/__mocks__/**/*",
|
|
80
|
+
"!**/__snapshots__/**/*"
|
|
81
|
+
],
|
|
82
|
+
ssrParameters: { ssrFunctionNodeVersion: "24.x" }
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { buildMrtConfig as a, SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR as i, GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH as n, SFNEXT_BASE_CARTRIDGE_NAME as r, CARTRIDGES_BASE_DIR as t };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Users cannot override these values - they will be validated and an error will be thrown if modified.
|
|
6
6
|
*/
|
|
7
7
|
function storefrontNextPreset() {
|
|
8
|
+
const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;
|
|
8
9
|
const presetConfig = {
|
|
9
10
|
appDirectory: "./src",
|
|
10
11
|
buildDirectory: "build",
|
|
@@ -14,7 +15,8 @@ function storefrontNextPreset() {
|
|
|
14
15
|
future: {
|
|
15
16
|
v8_middleware: true,
|
|
16
17
|
v8_viteEnvironmentApi: true
|
|
17
|
-
}
|
|
18
|
+
},
|
|
19
|
+
...sfwFalconInstance && { allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`] }
|
|
18
20
|
};
|
|
19
21
|
return {
|
|
20
22
|
name: "storefront-next-preset",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/configs/react-router.config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { Preset } from '@react-router/dev/config';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n */\nexport function storefrontNextPreset(): Preset {\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n },\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,uBAA+B;CAC3C,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GAC1B;EACJ;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAE3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,QAAO,KACH,kCAAkC,aAAa,eAAe,KAAK,UAAU,kBAAkB,gBAAgB,KAAK,GACvH;AAGL,OAAI,kBAAkB,uBAAuB,aAAa,mBACtD,QAAO,KACH,iCAAiC,aAAa,mBAAmB,UAAU,kBAAkB,mBAAmB,GACnH;AAGL,OAAI,kBAAkB,QAAQ,aAAa,IACvC,QAAO,KAAK,iBAAiB,aAAa,IAAI,QAAQ,kBAAkB,MAAM;AAGlF,OAAI,kBAAkB,QAAQ,kBAAkB,aAAa,OAAO,cAChE,QAAO,KACH,kCAAkC,aAAa,OAAO,cAAc,QAAQ,kBAAkB,QAAQ,gBACzG;AAGL,OAAI,kBAAkB,QAAQ,0BAA0B,aAAa,OAAO,sBACxE,QAAO,KACH,0CAA0C,aAAa,OAAO,sBAAsB,QAAQ,kBAAkB,QAAQ,wBACzH;AAGL,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
|
|
1
|
+
{"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/configs/react-router.config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { Preset } from '@react-router/dev/config';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n },\n // Allow workspace proxy domain for CSRF protection on form actions\n ...(sfwFalconInstance && {\n allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`],\n }),\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CAEtC,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GAC1B;EAED,GAAI,qBAAqB,EACrB,sBAAsB,CAAC,kCAAkC,kBAAkB,cAAc,EAC5F;EACJ;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAE3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,QAAO,KACH,kCAAkC,aAAa,eAAe,KAAK,UAAU,kBAAkB,gBAAgB,KAAK,GACvH;AAGL,OAAI,kBAAkB,uBAAuB,aAAa,mBACtD,QAAO,KACH,iCAAiC,aAAa,mBAAmB,UAAU,kBAAkB,mBAAmB,GACnH;AAGL,OAAI,kBAAkB,QAAQ,aAAa,IACvC,QAAO,KAAK,iBAAiB,aAAa,IAAI,QAAQ,kBAAkB,MAAM;AAGlF,OAAI,kBAAkB,QAAQ,kBAAkB,aAAa,OAAO,cAChE,QAAO,KACH,kCAAkC,aAAa,OAAO,cAAc,QAAQ,kBAAkB,QAAQ,gBACzG;AAGL,OAAI,kBAAkB,QAAQ,0BAA0B,aAAa,OAAO,sBACxE,QAAO,KACH,0CAA0C,aAAa,OAAO,sBAAsB,QAAQ,kBAAkB,QAAQ,wBACzH;AAGL,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
//#region src/extensibility/path-util.ts
|
|
5
|
+
const FILE_EXTENSIONS = [
|
|
6
|
+
".tsx",
|
|
7
|
+
".ts",
|
|
8
|
+
".d.ts"
|
|
9
|
+
];
|
|
10
|
+
function isSupportedFileExtension(fileName) {
|
|
11
|
+
return FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/extensibility/trim-extensions.ts
|
|
16
|
+
const SINGLE_LINE_MARKER = "@sfdc-extension-line";
|
|
17
|
+
const BLOCK_MARKER_START = "@sfdc-extension-block-start";
|
|
18
|
+
const BLOCK_MARKER_END = "@sfdc-extension-block-end";
|
|
19
|
+
const FILE_MARKER = "@sfdc-extension-file";
|
|
20
|
+
let verbose = false;
|
|
21
|
+
function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
verbose = verboseOverride ?? false;
|
|
24
|
+
const configuredExtensions = extensionConfig?.extensions || {};
|
|
25
|
+
const extensions = {};
|
|
26
|
+
Object.keys(configuredExtensions).forEach((targetKey) => {
|
|
27
|
+
extensions[targetKey] = Boolean(selectedExtensions?.[targetKey]) || false;
|
|
28
|
+
});
|
|
29
|
+
if (Object.keys(extensions).length === 0) {
|
|
30
|
+
if (verbose) console.log("No targets found, skipping trim");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const processDirectory = (dir) => {
|
|
34
|
+
fs.readdirSync(dir).forEach((file) => {
|
|
35
|
+
const filePath = path.join(dir, file);
|
|
36
|
+
const stats = fs.statSync(filePath);
|
|
37
|
+
if (!filePath.includes("node_modules")) {
|
|
38
|
+
if (stats.isDirectory()) processDirectory(filePath);
|
|
39
|
+
else if (isSupportedFileExtension(file)) processFile(filePath, extensions);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
processDirectory(directory);
|
|
44
|
+
if (extensionConfig?.extensions) {
|
|
45
|
+
deleteExtensionFolders(directory, extensions, extensionConfig);
|
|
46
|
+
updateExtensionConfig(directory, extensions);
|
|
47
|
+
}
|
|
48
|
+
const endTime = Date.now();
|
|
49
|
+
if (verbose) console.log(`Trim extensions took ${endTime - startTime}ms`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Update the extension config file to only include the selected extensions.
|
|
53
|
+
* @param projectDirectory - The project directory
|
|
54
|
+
* @param extensionSelections - The selected extensions
|
|
55
|
+
*/
|
|
56
|
+
function updateExtensionConfig(projectDirectory, extensionSelections) {
|
|
57
|
+
const extensionConfigPath = path.join(projectDirectory, "src", "extensions", "config.json");
|
|
58
|
+
const extensionConfig = JSON.parse(fs.readFileSync(extensionConfigPath, "utf8"));
|
|
59
|
+
Object.keys(extensionConfig.extensions).forEach((extensionKey) => {
|
|
60
|
+
if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
|
|
61
|
+
});
|
|
62
|
+
fs.writeFileSync(extensionConfigPath, JSON.stringify({ extensions: extensionConfig.extensions }, null, 4), "utf8");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Process a file to trim extension-specific code based on markers.
|
|
66
|
+
* @param filePath - The file path to process
|
|
67
|
+
* @param extensions - The extension selections
|
|
68
|
+
*/
|
|
69
|
+
function processFile(filePath, extensions) {
|
|
70
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
71
|
+
if (source.includes(FILE_MARKER)) {
|
|
72
|
+
const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
|
|
73
|
+
const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
|
|
74
|
+
if (!extMatch) {
|
|
75
|
+
if (verbose) console.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
|
|
76
|
+
} else if (extensions[extMatch] === false) {
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(filePath);
|
|
79
|
+
if (verbose) console.log(`Deleted file ${filePath}`);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
const error = e;
|
|
82
|
+
console.error(`Error deleting file ${filePath}: ${error.message}`);
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const extKeys = Object.keys(extensions);
|
|
89
|
+
if (new RegExp(extKeys.join("|"), "g").test(source)) {
|
|
90
|
+
const lines = source.split("\n");
|
|
91
|
+
const newLines = [];
|
|
92
|
+
const blockMarkers = [];
|
|
93
|
+
let skippingBlock = false;
|
|
94
|
+
let i = 0;
|
|
95
|
+
while (i < lines.length) {
|
|
96
|
+
const line = lines[i];
|
|
97
|
+
if (line.includes(SINGLE_LINE_MARKER)) {
|
|
98
|
+
const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
|
|
99
|
+
if (matchingExtension && extensions[matchingExtension] === false) {
|
|
100
|
+
i += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
} else if (line.includes(BLOCK_MARKER_START)) {
|
|
104
|
+
const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
|
|
105
|
+
if (matchingExtension) {
|
|
106
|
+
blockMarkers.push({
|
|
107
|
+
extension: matchingExtension,
|
|
108
|
+
line: i
|
|
109
|
+
});
|
|
110
|
+
skippingBlock = extensions[matchingExtension] === false;
|
|
111
|
+
} else if (verbose) console.warn(`Warning: Unknown marker found in ${filePath} at line ${i}: \n${line}`);
|
|
112
|
+
} else if (line.includes(BLOCK_MARKER_END)) {
|
|
113
|
+
if (Object.keys(extensions).find((extension) => line.includes(extension))) {
|
|
114
|
+
const extension = Object.keys(extensions).find((p) => line.includes(p));
|
|
115
|
+
if (blockMarkers.length === 0) throw new Error(`Block marker mismatch in ${filePath}, encountered end marker ${extension} without a matching start marker at line ${i}:\n${lines[i]}`);
|
|
116
|
+
const startMarker = blockMarkers.pop();
|
|
117
|
+
if (!extension || startMarker.extension !== extension) throw new Error(`Block marker mismatch in ${filePath}, expected end marker for ${startMarker.extension} but got ${extension} at line ${i}:\n${lines[i]}`);
|
|
118
|
+
if (extensions[extension] === false) {
|
|
119
|
+
skippingBlock = false;
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!skippingBlock) newLines.push(line);
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
if (blockMarkers.length > 0) throw new Error(`Unclosed end marker found in ${filePath}: ${blockMarkers[blockMarkers.length - 1].extension}`);
|
|
129
|
+
const newSource = newLines.join("\n");
|
|
130
|
+
if (newSource !== source) try {
|
|
131
|
+
fs.writeFileSync(filePath, newSource);
|
|
132
|
+
if (verbose) console.log(`Updated file ${filePath}`);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
const error = e;
|
|
135
|
+
console.error(`Error updating file ${filePath}: ${error.message}`);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Delete extension folders for disabled extensions.
|
|
142
|
+
* @param projectRoot - The project root directory
|
|
143
|
+
* @param extensions - The extension selections
|
|
144
|
+
* @param extensionConfig - The extension configuration
|
|
145
|
+
*/
|
|
146
|
+
function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
|
|
147
|
+
const extensionsDir = path.join(projectRoot, "src", "extensions");
|
|
148
|
+
if (!fs.existsSync(extensionsDir)) return;
|
|
149
|
+
const configuredExtensions = extensionConfig.extensions;
|
|
150
|
+
Object.keys(extensions).filter((ext) => extensions[ext] === false).forEach((extKey) => {
|
|
151
|
+
const extensionMeta = configuredExtensions[extKey];
|
|
152
|
+
if (extensionMeta?.folder) {
|
|
153
|
+
const extensionFolderPath = path.join(extensionsDir, extensionMeta.folder);
|
|
154
|
+
if (fs.existsSync(extensionFolderPath)) try {
|
|
155
|
+
fs.rmSync(extensionFolderPath, {
|
|
156
|
+
recursive: true,
|
|
157
|
+
force: true
|
|
158
|
+
});
|
|
159
|
+
if (verbose) console.log(`Deleted extension folder: ${extensionFolderPath}`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const error = err;
|
|
162
|
+
if (error.code === "EPERM") console.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
|
|
163
|
+
else console.error(`Error deleting ${extensionFolderPath}: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/extensibility/dependency-utils.ts
|
|
171
|
+
/**
|
|
172
|
+
* Resolve full transitive dependency chain in topological order (dependencies first).
|
|
173
|
+
* Example: resolveDependencies('BOPIS', config) → ['Store Locator', 'BOPIS']
|
|
174
|
+
*
|
|
175
|
+
* @param extensionKey - The extension key to resolve dependencies for
|
|
176
|
+
* @param config - The extension configuration
|
|
177
|
+
* @returns Array of extension keys in topological order (dependencies first, then the extension itself)
|
|
178
|
+
*/
|
|
179
|
+
function resolveDependencies(extensionKey, config) {
|
|
180
|
+
const visited = /* @__PURE__ */ new Set();
|
|
181
|
+
const result = [];
|
|
182
|
+
function visit(key) {
|
|
183
|
+
if (visited.has(key)) return;
|
|
184
|
+
visited.add(key);
|
|
185
|
+
const extension = config.extensions[key];
|
|
186
|
+
if (!extension) return;
|
|
187
|
+
const dependencies = extension.dependencies || [];
|
|
188
|
+
for (const dep of dependencies) visit(dep);
|
|
189
|
+
result.push(key);
|
|
190
|
+
}
|
|
191
|
+
visit(extensionKey);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reverse lookup: find immediate extensions that depend on this one.
|
|
196
|
+
* Example: getDependents('Store Locator', config) → ['BOPIS']
|
|
197
|
+
*
|
|
198
|
+
* @param extensionKey - The extension key to find dependents for
|
|
199
|
+
* @param config - The extension configuration
|
|
200
|
+
* @returns Array of extension keys that directly depend on this extension
|
|
201
|
+
*/
|
|
202
|
+
function getDependents(extensionKey, config) {
|
|
203
|
+
const dependents = [];
|
|
204
|
+
for (const [key, extension] of Object.entries(config.extensions)) if ((extension.dependencies || []).includes(extensionKey)) dependents.push(key);
|
|
205
|
+
return dependents;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve full transitive dependent chain in reverse topological order (dependents first).
|
|
209
|
+
* Example: resolveDependents('Store Locator', config) → ['BOPIS', 'Store Locator']
|
|
210
|
+
*
|
|
211
|
+
* @param extensionKey - The extension key to resolve dependents for
|
|
212
|
+
* @param config - The extension configuration
|
|
213
|
+
* @returns Array of extension keys in reverse topological order (dependents first, then the extension itself)
|
|
214
|
+
*/
|
|
215
|
+
function resolveDependents(extensionKey, config) {
|
|
216
|
+
const visited = /* @__PURE__ */ new Set();
|
|
217
|
+
const result = [];
|
|
218
|
+
function visit(key) {
|
|
219
|
+
if (visited.has(key)) return;
|
|
220
|
+
visited.add(key);
|
|
221
|
+
const dependents = getDependents(key, config);
|
|
222
|
+
for (const dep of dependents) visit(dep);
|
|
223
|
+
result.push(key);
|
|
224
|
+
}
|
|
225
|
+
visit(extensionKey);
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Validate that no circular dependencies exist in the configuration.
|
|
230
|
+
* Throws a descriptive error if a cycle is found.
|
|
231
|
+
*
|
|
232
|
+
* @param config - The extension configuration to validate
|
|
233
|
+
* @throws Error if a circular dependency is detected
|
|
234
|
+
*/
|
|
235
|
+
function validateNoCycles(config) {
|
|
236
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
237
|
+
const visited = /* @__PURE__ */ new Set();
|
|
238
|
+
function visit(key, path$1) {
|
|
239
|
+
if (visited.has(key)) return;
|
|
240
|
+
if (visiting.has(key)) {
|
|
241
|
+
const cycleStart = path$1.indexOf(key);
|
|
242
|
+
const cyclePath = [...path$1.slice(cycleStart), key];
|
|
243
|
+
throw new Error(`Circular dependency detected: ${cyclePath.join(" -> ")}`);
|
|
244
|
+
}
|
|
245
|
+
visiting.add(key);
|
|
246
|
+
path$1.push(key);
|
|
247
|
+
const extension = config.extensions[key];
|
|
248
|
+
if (extension) {
|
|
249
|
+
const dependencies = extension.dependencies || [];
|
|
250
|
+
for (const dep of dependencies) visit(dep, path$1);
|
|
251
|
+
}
|
|
252
|
+
path$1.pop();
|
|
253
|
+
visiting.delete(key);
|
|
254
|
+
visited.add(key);
|
|
255
|
+
}
|
|
256
|
+
for (const key of Object.keys(config.extensions)) visit(key, []);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Filter resolved dependencies to only those not yet installed.
|
|
260
|
+
* Returns dependencies in topological order (install order).
|
|
261
|
+
*
|
|
262
|
+
* @param extensionKey - The extension key to check dependencies for
|
|
263
|
+
* @param installedExtensions - Array of already installed extension keys
|
|
264
|
+
* @param config - The extension configuration
|
|
265
|
+
* @returns Array of missing extension keys in topological order (install order)
|
|
266
|
+
*/
|
|
267
|
+
function getMissingDependencies(extensionKey, installedExtensions, config) {
|
|
268
|
+
const allDependencies = resolveDependencies(extensionKey, config);
|
|
269
|
+
const installedSet = new Set(installedExtensions);
|
|
270
|
+
return allDependencies.filter((key) => !installedSet.has(key));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Resolve dependencies for multiple extensions, merging and deduplicating the results.
|
|
274
|
+
* Returns all dependencies in topological order.
|
|
275
|
+
*
|
|
276
|
+
* @param extensionKeys - Array of extension keys to resolve dependencies for
|
|
277
|
+
* @param config - The extension configuration
|
|
278
|
+
* @returns Array of all extension keys in topological order (dependencies first)
|
|
279
|
+
*/
|
|
280
|
+
function resolveDependenciesForMultiple(extensionKeys, config) {
|
|
281
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
282
|
+
const result = [];
|
|
283
|
+
for (const key of extensionKeys) {
|
|
284
|
+
const deps = resolveDependencies(key, config);
|
|
285
|
+
for (const dep of deps) if (!allDeps.has(dep)) {
|
|
286
|
+
allDeps.add(dep);
|
|
287
|
+
result.push(dep);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolve dependents for multiple extensions, merging and deduplicating the results.
|
|
294
|
+
* Returns all dependents in reverse topological order (uninstall order).
|
|
295
|
+
*
|
|
296
|
+
* @param extensionKeys - Array of extension keys to resolve dependents for
|
|
297
|
+
* @param config - The extension configuration
|
|
298
|
+
* @returns Array of all extension keys in reverse topological order (dependents first)
|
|
299
|
+
*/
|
|
300
|
+
function resolveDependentsForMultiple(extensionKeys, config) {
|
|
301
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
302
|
+
const result = [];
|
|
303
|
+
for (const key of extensionKeys) {
|
|
304
|
+
const deps = resolveDependents(key, config);
|
|
305
|
+
for (const dep of deps) if (!allDeps.has(dep)) {
|
|
306
|
+
allDeps.add(dep);
|
|
307
|
+
result.push(dep);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
//#endregion
|
|
314
|
+
export { trimExtensions as a, validateNoCycles as i, resolveDependenciesForMultiple as n, resolveDependentsForMultiple as r, getMissingDependencies as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|