Skip to content

Commit f31cddd

Browse files
committed
feat: config
1 parent eb409d7 commit f31cddd

File tree

7 files changed

+242
-23
lines changed

7 files changed

+242
-23
lines changed

messages/prompts.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ Which Experience Cloud Site would you like to preview (Use arrow keys)
66

77
An updated site bundle is available for "%s". Do you want to download and apply the update?
88

9+
# site.auth.prompt
10+
11+
Please enter your authentication token:
12+
13+
# site.auth.error
14+
15+
Authentication token cannot be empty
16+
917
# lightning-experience-app.title
1018

1119
Which Lightning Experience App do you want to use for the preview?

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.9",
1717
"@salesforce/sf-plugins-core": "^11.2.4",
1818
"axios": "^1.7.9",
19+
"dotenv": "^16.4.7",
1920
"glob": "^10.4.5",
2021
"lwc": "~8.12.5",
2122
"node-fetch": "^3.3.2"

src/commands/lightning/dev/site.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { expDev, SitesLocalDevOptions, setupDev } from '@lwrjs/api';
1111
import { OrgUtils } from '../../../shared/orgUtils.js';
1212
import { PromptUtils } from '../../../shared/promptUtils.js';
1313
import { ExperienceSite } from '../../../shared/experience/expSite.js';
14+
import 'dotenv/config';
1415

1516
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1617
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.site');
@@ -87,12 +88,10 @@ export default class LightningDevSite extends SfCommand<void> {
8788
// Start the dev server
8889
const port = parseInt(process.env.PORT ?? '3000', 10);
8990

90-
// Internal vs external mode
91-
const internalProject = !fs.existsSync('sfdx-project.json') && fs.existsSync('lwr.config.json');
9291
const logLevel = process.env.LOG_LEVEL ?? 'error';
9392

9493
const startupParams: SitesLocalDevOptions = {
95-
sfCLI: !internalProject,
94+
sfCLI: process.env.INTERNAL_DEVELOPER_MODE !== 'true',
9695
authToken,
9796
open: process.env.OPEN_BROWSER === 'false' ? false : true,
9897
port,

src/shared/experience/expSite.ts

Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,28 @@ import fs from 'node:fs';
88
import path from 'node:path';
99
import { Org, SfError } from '@salesforce/core';
1010
import axios from 'axios';
11+
import { PromptUtils } from '../promptUtils.js';
1112

13+
// TODO this format is what we want to be storing for each site
14+
export type NewSiteMetadata = {
15+
name: string;
16+
siteZip: string; // TODO do we want to store a list of ordered zip files we've downloaded? or just the most recent
17+
lastModified: Date;
18+
coreVersion: string;
19+
needsUpdate: boolean;
20+
users: AuthUserMap;
21+
};
22+
23+
export type AuthUserMap = {
24+
[username: string]: AuthToken;
25+
};
26+
27+
export type AuthToken = {
28+
token: string;
29+
issued: Date;
30+
};
31+
32+
// This is what we have been storing in the sites.json
1233
export type SiteMetadata = {
1334
bundleName: string;
1435
bundleLastModified: string;
@@ -33,16 +54,95 @@ export class ExperienceSite {
3354
public siteName: string;
3455
private org: Org;
3556
private metadataCache: SiteMetadataCache = {};
57+
private config;
3658

3759
public constructor(org: Org, siteName: string) {
3860
this.org = org;
3961
this.siteDisplayName = siteName.trim();
4062
this.siteName = this.siteDisplayName.replace(' ', '_');
4163
// Replace any special characters in site name with underscore
4264
this.siteName = this.siteName.replace(/[^a-zA-Z0-9]/g, '_');
65+
66+
// Backwards Compat
67+
if (process.env.SITE_GUEST_ACCESS === 'true') {
68+
process.env.PREVIEW_USER = 'Guest';
69+
}
70+
if (process.env.SID_TOKEN && !process.env.PREVIEW_USER) {
71+
process.env.PREVIEW_USER = 'Custom';
72+
}
73+
74+
// TODO the config handling code should move into its own file
75+
// Store variables in consumable config to limit use of env variables
76+
// Eventually these will be part of CLI interface or scrapped in favor of config
77+
// once they are no longer experimental
78+
this.config = {
79+
previewUser: process.env.PREVIEW_USER ?? 'Admin',
80+
previewToken: process.env.SID_TOKEN ?? '',
81+
apiStaticMode: process.env.API_STATIC_MODE === 'true' ? true : false,
82+
apiBundlingGroups: process.env.API_BUNDLING_GROUPS === 'true' ? true : false,
83+
apiVersion: process.env.API_VERSION ?? 'v64.0',
84+
apiSiteVersion: process.env.API_SITE_VERSION ?? 'published',
85+
};
86+
}
87+
88+
public get apiQueryParams(): string {
89+
const retVal = [];
90+
91+
// Preview is default. If we specify another mode, add it as a query parameter
92+
if (this.config.apiSiteVersion !== 'preview') {
93+
retVal.push(this.config.apiSiteVersion);
94+
}
95+
96+
// Bundling groups are off by default. Only add if enabled
97+
if (this.config.apiBundlingGroups) {
98+
retVal.push('bundlingGroups');
99+
}
100+
101+
// Metrics - TODO
102+
103+
// If we have query parameters, return them
104+
if (retVal.length) {
105+
return '?' + retVal.join('&');
106+
}
107+
108+
// Otherwise just return an empty string
109+
return '';
43110
}
44111

45112
/**
113+
* TODO this should use the connect api `{{orgInstance}}/services/data/v{{version}}/connect/communities`
114+
* Returns array of sites like:
115+
* communities[
116+
* {
117+
"allowChatterAccessWithoutLogin": true,
118+
"allowMembersToFlag": false,
119+
"builderBasedSnaEnabled": true,
120+
"builderUrl": "https://orgfarm-656f3290cc.test1.my.pc-rnd.salesforce.com/sfsites/picasso/core/config/commeditor.jsp?siteId=0DMSG000001lhVa",
121+
"contentSpaceId": "0ZuSG000001n1la0AA",
122+
"description": "D2C Codecept Murazik",
123+
"guestMemberVisibilityEnabled": false,
124+
"id": "0DBSG000001huWE4AY",
125+
"imageOptimizationCDNEnabled": true,
126+
"invitationsEnabled": false,
127+
"knowledgeableEnabled": false,
128+
"loginUrl": "https://orgfarm-656f3290cc.test1.my.pc-rnd.site.com/d2cbernadette/login",
129+
"memberVisibilityEnabled": false,
130+
"name": "D2C Codecept Murazik",
131+
"nicknameDisplayEnabled": true,
132+
"privateMessagesEnabled": false,
133+
"reputationEnabled": false,
134+
"sendWelcomeEmail": true,
135+
"siteAsContainerEnabled": true,
136+
"siteUrl": "https://orgfarm-656f3290cc.test1.my.pc-rnd.site.com/d2cbernadette",
137+
"status": "Live",
138+
"templateName": "D2C Commerce (LWR)",
139+
"url": "/services/data/v64.0/connect/communities/0DBSG000001huWE4AY",
140+
"urlPathPrefix": "d2cbernadettevforcesite"
141+
},
142+
...
143+
]
144+
*
145+
*
46146
* Fetches all current experience sites
47147
*
48148
* @param {Connection} conn - Salesforce connection object.
@@ -66,25 +166,37 @@ export class ExperienceSite {
66166
* @returns sid token for proxied site requests
67167
*/
68168
public async setupAuth(): Promise<string> {
69-
let sidToken = '';
70-
// Default to guest user access if specified
71-
if (process.env.SITE_GUEST_ACCESS === 'true') return sidToken;
169+
const previewUser = this.config.previewUser.toLocaleLowerCase();
72170

73-
// Use a provided token if specified in environment variables
74-
if (process.env.SID_TOKEN) return process.env.SID_TOKEN;
171+
// Preview as Guest User (no token)
172+
if (previewUser === 'guest') return '';
75173

76-
// Otherwise attempt to generate one based on the currently authenticated admin user
77-
try {
78-
const networkId = await this.getNetworkId();
79-
sidToken = await this.getNewSidToken(networkId);
80-
} catch (e) {
81-
// eslint-disable-next-line no-console
82-
console.error('Failed to establish authentication for site', e);
174+
// Preview with supplied user token
175+
if (this.config.previewToken) return this.config.previewToken;
176+
177+
// Preview as CLI Admin user (Default)
178+
if (previewUser === 'admin') {
179+
try {
180+
const networkId = await this.getNetworkId();
181+
const sidToken = await this.getNewSidToken(networkId);
182+
return sidToken;
183+
} catch (e) {
184+
// eslint-disable-next-line no-console
185+
console.error('Failed to establish authentication for site', e);
186+
}
83187
}
84188

189+
// TODO Check local metadata for token, if it doesn't exist, prompt the user
190+
191+
// Prompt user for token or re-use token already saved for the site
192+
const sidToken = await PromptUtils.promptUserForAuthToken();
193+
194+
// TODO If are supplied a token, we should store it in the local metadata to reuse later on
195+
85196
return sidToken;
86197
}
87198

199+
// TODO this doesn't work anymore, we should consider alternative strategies
88200
public async isUpdateAvailable(): Promise<boolean> {
89201
const localMetadata = this.getLocalMetadata();
90202
if (!localMetadata) {
@@ -148,6 +260,7 @@ export class ExperienceSite {
148260
return siteJson;
149261
}
150262

263+
// TODO rename to getStaticResourceMetadata()
151264
public async getRemoteMetadata(): Promise<SiteMetadata | undefined> {
152265
if (this.metadataCache.remoteMetadata) return this.metadataCache.remoteMetadata;
153266
const result = await this.org
@@ -197,7 +310,7 @@ export class ExperienceSite {
197310
*/
198311
public async downloadSite(): Promise<string> {
199312
let retVal;
200-
if (process.env.STATIC_MODE !== 'true') {
313+
if (!this.config.apiStaticMode) {
201314
// Use sites API to download the site bundle on demand
202315
retVal = await this.downloadSiteApi();
203316
} else {
@@ -225,6 +338,8 @@ export class ExperienceSite {
225338

226339
// Download the site via API
227340
const conn = this.org.getConnection();
341+
342+
// TODO update to the new metadata format
228343
const metadata = {
229344
bundleName: theSite.Name,
230345
bundleLastModified: theSite.LastModifiedDate,
@@ -239,12 +354,7 @@ export class ExperienceSite {
239354
}
240355
const resourcePath = this.getSiteZipPath(metadata);
241356
try {
242-
// Limit API to published sites for now until we have a patch for the issues with unpublished sites
243-
// TODO switch api back to preview mode after issues are addressed
244-
let apiUrl = `${instanceUrl}/services/data/v63.0/sites/${siteIdMinus3}/preview?published`;
245-
if (process.env.SITE_API_MODE === 'preview') {
246-
apiUrl = `${instanceUrl}/services/data/v63.0/sites/${siteIdMinus3}/preview`;
247-
}
357+
const apiUrl = `${instanceUrl}/services/data/${this.config.apiVersion}/sites/${siteIdMinus3}/preview${this.apiQueryParams}`;
248358

249359
const response = await axios.get(apiUrl, {
250360
headers: {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
export type ExperienceSiteConfig = {
8+
previewUser: string;
9+
previewToken: string;
10+
apiStaticMode: boolean;
11+
apiBundlingGroups: boolean;
12+
apiVersion: string;
13+
apiSiteVersion: string;
14+
};
15+
16+
export class ExperienceSiteConfigManager {
17+
private static instance: ExperienceSiteConfigManager;
18+
private config: ExperienceSiteConfig;
19+
20+
private constructor() {
21+
// Backwards Compat
22+
if (process.env.SITE_GUEST_ACCESS === 'true') {
23+
process.env.PREVIEW_USER = 'Guest';
24+
}
25+
if (process.env.SID_TOKEN && !process.env.PREVIEW_USER) {
26+
process.env.PREVIEW_USER = 'Custom';
27+
}
28+
29+
this.config = {
30+
previewUser: process.env.PREVIEW_USER ?? 'Admin',
31+
previewToken: process.env.SID_TOKEN ?? '',
32+
apiStaticMode: process.env.API_STATIC_MODE === 'true' ? true : false,
33+
apiBundlingGroups: process.env.API_BUNDLING_GROUPS === 'true' ? true : false,
34+
apiVersion: process.env.API_VERSION ?? 'v64.0',
35+
apiSiteVersion: process.env.API_SITE_VERSION ?? 'published',
36+
};
37+
}
38+
39+
public static getInstance(): ExperienceSiteConfigManager {
40+
if (!ExperienceSiteConfigManager.instance) {
41+
ExperienceSiteConfigManager.instance = new ExperienceSiteConfigManager();
42+
}
43+
return ExperienceSiteConfigManager.instance;
44+
}
45+
46+
public getConfig(): ExperienceSiteConfig {
47+
return { ...this.config };
48+
}
49+
50+
public updateConfig(partialConfig: Partial<ExperienceSiteConfig>): void {
51+
this.config = { ...this.config, ...partialConfig };
52+
}
53+
54+
public getApiQueryParams(): string {
55+
const retVal = [];
56+
57+
// Preview is default. If we specify another mode, add it as a query parameter
58+
if (this.config.apiSiteVersion !== 'preview') {
59+
retVal.push(this.config.apiSiteVersion);
60+
}
61+
62+
// Bundling groups are off by default. Only add if enabled
63+
if (this.config.apiBundlingGroups) {
64+
retVal.push('bundlingGroups');
65+
}
66+
67+
// If we have query parameters, return them
68+
if (retVal.length) {
69+
return '?' + retVal.join('&');
70+
}
71+
72+
// Otherwise just return an empty string
73+
return '';
74+
}
75+
}
76+
77+
// Export a convenience function to get the config
78+
export function getExperienceSiteConfig(): ExperienceSiteConfig {
79+
return ExperienceSiteConfigManager.getInstance().getConfig();
80+
}

src/shared/promptUtils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import select from '@inquirer/select';
8-
import { confirm } from '@inquirer/prompts';
8+
import { confirm, input } from '@inquirer/prompts';
99
import { Connection, Logger, Messages } from '@salesforce/core';
1010
import {
1111
AndroidDeviceManager,
@@ -30,6 +30,22 @@ export class PromptUtils {
3030
return response;
3131
}
3232

33+
// Add this method to the PromptUtils class
34+
public static async promptUserForAuthToken(defaultToken?: string): Promise<string> {
35+
const response = await input({
36+
message: messages.getMessage('site.auth.prompt'),
37+
default: defaultToken,
38+
validate: (value) => {
39+
if (!value.trim()) {
40+
return messages.getMessage('site.auth.error');
41+
}
42+
return true;
43+
},
44+
});
45+
46+
return response;
47+
}
48+
3349
public static async promptUserToConfirmUpdate(siteName: string): Promise<boolean> {
3450
return confirm({
3551
message: messages.getMessage('site.confirm-update', [siteName]),

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5566,6 +5566,11 @@ dot-prop@^5.1.0:
55665566
dependencies:
55675567
is-obj "^2.0.0"
55685568

5569+
dotenv@^16.4.7:
5570+
version "16.4.7"
5571+
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
5572+
integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
5573+
55695574
eastasianwidth@^0.2.0:
55705575
version "0.2.0"
55715576
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"

0 commit comments

Comments
 (0)