Skip to content

Commit d61cffe

Browse files
authored
feat: update sites locally after publish (#136)
1 parent b12ac30 commit d61cffe

File tree

6 files changed

+262
-145
lines changed

6 files changed

+262
-145
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.7",
1515
"@salesforce/sf-plugins-core": "^11.2.4",
1616
"@inquirer/select": "^2.4.7",
17+
"@inquirer/prompts": "^5.3.8",
1718
"chalk": "^5.3.0",
1819
"lwc": "7.1.3",
1920
"lwr": "0.14.0",

src/commands/lightning/dev/site.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,16 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
// import fs from 'node:fs';
8-
// import path from 'node:path';
7+
import fs from 'node:fs';
98
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
109
import { Messages } from '@salesforce/core';
1110
import { expDev } from '@lwrjs/api';
1211
import { PromptUtils } from '../../../shared/prompt.js';
13-
// import { OrgUtils } from '../../../shared/orgUtils.js';
1412
import { ExperienceSite } from '../../../shared/experience/expSite.js';
1513

1614
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1715
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.site');
1816

19-
export type LightningDevSiteResult = {
20-
path: string;
21-
};
22-
2317
export default class LightningDevSite extends SfCommand<void> {
2418
public static readonly summary = messages.getMessage('summary');
2519
public static readonly description = messages.getMessage('description');
@@ -40,50 +34,49 @@ export default class LightningDevSite extends SfCommand<void> {
4034
public async run(): Promise<void> {
4135
const { flags } = await this.parse(LightningDevSite);
4236

43-
// TODO short circuit all this if user specifies a site name and it exists locally
44-
4537
try {
46-
// 1. Connect to Org
4738
const org = flags['target-org'];
4839
let siteName = flags.name;
4940

50-
// 2. If we don't have a site to use, prompt the user for one
41+
// If user doesn't specify a site, prompt the user for one
5142
if (!siteName) {
52-
this.log('No site was specified');
53-
// Allow user to pick a site
54-
const siteList = await ExperienceSite.getAllExpSites(org.getConnection());
55-
siteName = await PromptUtils.promptUserToSelectSite(siteList);
43+
const allSites = await ExperienceSite.getAllExpSites(org);
44+
siteName = await PromptUtils.promptUserToSelectSite(allSites);
5645
}
5746

58-
// 3. Setup local dev directory structure: '.localdev/${site}'
59-
this.log(`Setting up Local Development for: ${siteName}`);
6047
const selectedSite = new ExperienceSite(org, siteName);
61-
let siteZip;
48+
let siteZip: string | undefined;
49+
6250
if (!selectedSite.isSiteSetup()) {
63-
// TODO Verify the bundle has been published and download
64-
this.log('Downloading Site...');
51+
this.log(`[local-dev] initializing: ${siteName}`);
6552
siteZip = await selectedSite.downloadSite();
6653
} else {
67-
// If we do have the site setup already, don't do anything / TODO prompt the user if they want to get latest?
68-
// Check if the site has been published
69-
// const result = await connection.query<{ Id: string; Name: string; LastModifiedDate: string }>(
70-
// "SELECT Id, Name, LastModifiedDate FROM StaticResource WHERE Name LIKE 'MRT%" + siteName + "'"
71-
// );
72-
// this.log('Setup already complete!');
54+
// If local-dev is already setup, check if an updated site has been published to download
55+
const updateAvailable = await selectedSite.isUpdateAvailable();
56+
if (updateAvailable) {
57+
const shouldUpdate = await PromptUtils.promptUserToConfirmUpdate(siteName);
58+
if (shouldUpdate) {
59+
this.log(`[local-dev] updating: ${siteName}`);
60+
siteZip = await selectedSite.downloadSite();
61+
// delete oldSitePath recursive
62+
const oldSitePath = selectedSite.getExtractDirectory();
63+
if (fs.existsSync(oldSitePath)) {
64+
fs.rmdirSync(oldSitePath, { recursive: true });
65+
}
66+
}
67+
}
7368
}
7469

75-
// 6. Start the dev server
76-
this.log('Starting local development server...');
70+
// Start the dev server
7771
await expDev({
78-
open: false,
72+
open: true,
7973
port: 3000,
8074
logLevel: 'error',
8175
mode: 'dev',
8276
siteZip,
8377
siteDir: selectedSite.getSiteDirectory(),
8478
});
8579
} catch (e) {
86-
// this.error(e);
8780
this.log('Local Development setup failed', e);
8881
}
8982
}

src/shared/experience/expSite.ts

Lines changed: 108 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@
66
*/
77
import fs from 'node:fs';
88
import path from 'node:path';
9-
import { Connection, Org, SfError } from '@salesforce/core';
9+
import { Org, SfError } from '@salesforce/core';
10+
11+
export type SiteMetadata = {
12+
bundleName: string;
13+
bundleLastModified: string;
14+
};
15+
16+
export type SiteMetadataCache = {
17+
[key: string]: SiteMetadata;
18+
};
1019

1120
/**
1221
* Experience Site class.
22+
* https://developer.salesforce.com/docs/platform/lwc/guide/get-started-test-components.html#enable-local-dev
1323
*
1424
* @param {string} siteName - The name of the experience site.
1525
* @param {string} status - The status of the experience site.
@@ -19,19 +29,13 @@ import { Connection, Org, SfError } from '@salesforce/core';
1929
export class ExperienceSite {
2030
public siteDisplayName: string;
2131
public siteName: string;
22-
public status: string;
23-
2432
private org: Org;
25-
private bundleName: string;
26-
private bundleLastModified: string;
33+
private metadataCache: SiteMetadataCache = {};
2734

28-
public constructor(org: Org, siteName: string, status?: string, bundleName?: string, bundleLastModified?: string) {
35+
public constructor(org: Org, siteName: string) {
2936
this.org = org;
3037
this.siteDisplayName = siteName.trim();
3138
this.siteName = this.siteDisplayName.replace(' ', '_');
32-
this.status = status ?? '';
33-
this.bundleName = bundleName ?? '';
34-
this.bundleLastModified = bundleLastModified ?? '';
3539
}
3640

3741
/**
@@ -45,7 +49,6 @@ export class ExperienceSite {
4549
* @returns
4650
*/
4751
public static getLocalExpSite(siteName: string): ExperienceSite {
48-
// TODO cleanup
4952
const siteJsonPath = path.join('.localdev', siteName.trim().replace(' ', '_'), 'site.json');
5053
const siteJson = fs.readFileSync(siteJsonPath, 'utf8');
5154
const site = JSON.parse(siteJson) as ExperienceSite;
@@ -67,14 +70,7 @@ export class ExperienceSite {
6770

6871
// Example of creating ExperienceSite instances
6972
const experienceSites: ExperienceSite[] = result.records.map(
70-
(record) =>
71-
new ExperienceSite(
72-
org,
73-
getSiteNameFromStaticResource(record.Name),
74-
'live',
75-
record.Name,
76-
record.LastModifiedDate
77-
)
73+
(record) => new ExperienceSite(org, getSiteNameFromStaticResource(record.Name))
7874
);
7975

8076
return experienceSites;
@@ -86,8 +82,8 @@ export class ExperienceSite {
8682
* @param {Connection} conn - Salesforce connection object.
8783
* @returns {Promise<string[]>} - List of experience sites.
8884
*/
89-
public static async getAllExpSites(conn: Connection): Promise<string[]> {
90-
const result = await conn.query<{
85+
public static async getAllExpSites(org: Org): Promise<string[]> {
86+
const result = await org.getConnection().query<{
9187
Id: string;
9288
Name: string;
9389
LastModifiedDate: string;
@@ -98,42 +94,84 @@ export class ExperienceSite {
9894
return experienceSites;
9995
}
10096

101-
public isSiteSetup(): boolean {
102-
return fs.existsSync(path.join(this.getExtractDirectory(), 'ssr.js'));
97+
public async isUpdateAvailable(): Promise<boolean> {
98+
const localMetadata = this.getLocalMetadata();
99+
if (!localMetadata) {
100+
return true; // If no local metadata, assume update is available
101+
}
102+
103+
const remoteMetadata = await this.getRemoteMetadata();
104+
if (!remoteMetadata) {
105+
return false; // If no org bundle found, no update available
106+
}
107+
108+
return new Date(remoteMetadata.bundleLastModified) > new Date(localMetadata.bundleLastModified);
103109
}
104110

105-
public isSitePublished(): boolean {
106-
// TODO
111+
// Is the site extracted locally
112+
public isSiteSetup(): boolean {
107113
return fs.existsSync(path.join(this.getExtractDirectory(), 'ssr.js'));
108114
}
109115

110-
public async getBundleName(): Promise<string> {
111-
if (!this.bundleName) {
112-
await this.initBundle();
116+
// Is the static resource available on the server
117+
public async isSitePublished(): Promise<boolean> {
118+
const remoteMetadata = await this.getRemoteMetadata();
119+
if (!remoteMetadata) {
120+
return false;
113121
}
114-
115-
return this.bundleName;
122+
return true;
116123
}
117124

118-
public async getBundleLastModified(): Promise<string> {
119-
if (!this.bundleLastModified) {
120-
await this.initBundle();
125+
// Is there a local gz file of the site
126+
public isSiteDownloaded(): boolean {
127+
const metadata = this.getLocalMetadata();
128+
if (!metadata) {
129+
return false;
121130
}
122-
return this.bundleLastModified;
131+
return fs.existsSync(this.getSiteZipPath(metadata));
123132
}
124133

125-
/**
126-
* Save the site metadata to the file system.
127-
*/
128-
public save(): void {
134+
public saveMetadata(metadata: SiteMetadata): void {
129135
const siteJsonPath = path.join(this.getSiteDirectory(), 'site.json');
130-
const siteJson = JSON.stringify(this, null, 4);
131-
132-
// write out the site metadata
133-
fs.mkdirSync(this.getSiteDirectory(), { recursive: true });
136+
const siteJson = JSON.stringify(metadata, null, 2);
134137
fs.writeFileSync(siteJsonPath, siteJson);
135138
}
136139

140+
public getLocalMetadata(): SiteMetadata | undefined {
141+
if (this.metadataCache.localMetadata) return this.metadataCache.localMetadata;
142+
const siteJsonPath = path.join(this.getSiteDirectory(), 'site.json');
143+
let siteJson;
144+
if (fs.existsSync(siteJsonPath)) {
145+
try {
146+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
147+
siteJson = JSON.parse(fs.readFileSync(siteJsonPath, 'utf-8')) as SiteMetadata;
148+
this.metadataCache.localMetadata = siteJson;
149+
} catch (error) {
150+
// eslint-disable-next-line no-console
151+
console.error('Error reading site.json file', error);
152+
}
153+
}
154+
return siteJson;
155+
}
156+
157+
public async getRemoteMetadata(): Promise<SiteMetadata | undefined> {
158+
if (this.metadataCache.remoteMetadata) return this.metadataCache.remoteMetadata;
159+
const result = await this.org
160+
.getConnection()
161+
.query<{ Name: string; LastModifiedDate: string }>(
162+
`SELECT Name, LastModifiedDate FROM StaticResource WHERE Name LIKE 'MRT_experience_%_${this.siteName}'`
163+
);
164+
if (result.records.length === 0) {
165+
return undefined;
166+
}
167+
const staticResource = result.records[0];
168+
this.metadataCache.remoteMetadata = {
169+
bundleName: staticResource.Name,
170+
bundleLastModified: staticResource.LastModifiedDate,
171+
};
172+
return this.metadataCache.remoteMetadata;
173+
}
174+
137175
/**
138176
* Get the local site directory path
139177
*
@@ -147,47 +185,45 @@ export class ExperienceSite {
147185
return path.join('.localdev', this.siteName, 'app');
148186
}
149187

188+
public getSiteZipPath(metadata: SiteMetadata): string {
189+
const lastModifiedDate = new Date(metadata.bundleLastModified);
190+
const timestamp = `${
191+
lastModifiedDate.getMonth() + 1
192+
}-${lastModifiedDate.getDate()}_${lastModifiedDate.getHours()}-${lastModifiedDate.getMinutes()}`;
193+
const fileName = `${metadata.bundleName}_${timestamp}.gz`;
194+
const resourcePath = path.join(this.getSiteDirectory(), fileName);
195+
return resourcePath;
196+
}
197+
150198
/**
151199
* Download and return the site resource bundle
152200
*
153201
* @returns path of downloaded site zip
154202
*/
155203
public async downloadSite(): Promise<string> {
156-
// 3a. Locate the site bundle
157-
const bundleName = await this.getBundleName();
158-
159-
// 3b. Download the site from static resources
160-
const resourcePath = path.join(this.getSiteDirectory(), `${bundleName}.gz`);
161-
162-
// TODO configure redownloading
163-
if (!fs.existsSync(resourcePath)) {
164-
const staticresource = await this.org.getConnection().metadata.read('StaticResource', bundleName);
165-
if (staticresource?.content) {
166-
fs.mkdirSync(this.getSiteDirectory(), { recursive: true });
167-
// Save the static resource
168-
const buffer = Buffer.from(staticresource.content, 'base64');
169-
// this.log(`Writing file to path: ${resourcePath}`);
170-
fs.writeFileSync(resourcePath, buffer);
171-
} else {
172-
throw new SfError(`Error occured downloading your site: ${this.siteDisplayName}`);
173-
}
204+
const remoteMetadata = await this.getRemoteMetadata();
205+
if (!remoteMetadata) {
206+
throw new SfError(`No published site found for: ${this.siteDisplayName}`);
174207
}
175-
return resourcePath;
176-
}
177208

178-
private async initBundle(): Promise<void> {
179-
const result = await this.org
180-
.getConnection()
181-
.query<{ Id: string; Name: string; LastModifiedDate: string }>(
182-
"SELECT Id, Name, LastModifiedDate FROM StaticResource WHERE Name LIKE 'MRT_experience_%_" + this.siteName + "'"
183-
);
184-
if (result.records.length === 0) {
185-
throw new Error(`No experience site found for siteName: ${this.siteDisplayName}`);
209+
// Download the site from static resources
210+
// eslint-disable-next-line no-console
211+
console.log('[local-dev] Downloading site...'); // TODO spinner
212+
const resourcePath = this.getSiteZipPath(remoteMetadata);
213+
const staticresource = await this.org.getConnection().metadata.read('StaticResource', remoteMetadata.bundleName);
214+
if (staticresource?.content) {
215+
// Save the static resource
216+
fs.mkdirSync(this.getSiteDirectory(), { recursive: true });
217+
const buffer = Buffer.from(staticresource.content, 'base64');
218+
fs.writeFileSync(resourcePath, buffer);
219+
220+
// Save the site's metadata
221+
this.saveMetadata(remoteMetadata);
222+
} else {
223+
throw new SfError(`Error occurred downloading your site: ${this.siteDisplayName}`);
186224
}
187225

188-
const staticResource = result.records[0];
189-
this.bundleName = staticResource.Name;
190-
this.bundleLastModified = staticResource.LastModifiedDate;
226+
return resourcePath;
191227
}
192228
}
193229

0 commit comments

Comments
 (0)