Skip to content

Commit 3e3ae0d

Browse files
authored
feat: preview site command updates (#51)
1 parent fe99de1 commit 3e3ae0d

File tree

5 files changed

+927
-571
lines changed

5 files changed

+927
-571
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
"@salesforce/kit": "^3.1.2",
1111
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.1",
1212
"@salesforce/sf-plugins-core": "^9.0.14",
13+
"@inquirer/select": "^2.3.5",
1314
"tar": "^7.2.0",
1415
"lwc": "6.6.4",
15-
"lwr": "0.13.0-alpha.12",
16-
"@lwrjs/api": "0.13.0-alpha.12"
16+
"lwr": "0.13.0-alpha.19",
17+
"@lwrjs/api": "0.13.0-alpha.19"
1718
},
1819
"devDependencies": {
1920
"@oclif/plugin-command-snapshot": "^5.1.9",

src/commands/lightning/preview/site.ts

Lines changed: 62 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
*/
77
import fs from 'node:fs';
88
import path from 'node:path';
9-
// import zlib from 'node:zlib';
10-
// import { pipeline } from 'node:stream';
11-
// import { promisify } from 'node:util';
12-
// eslint-disable-next-line import/no-extraneous-dependencies
13-
import * as tar from 'tar';
149
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
15-
import { Messages } from '@salesforce/core';
16-
import { expDev } from '@lwrjs/api';
10+
import { Messages, SfError } from '@salesforce/core';
11+
import { expDev, setupDev } from '@lwrjs/api';
12+
import { PromptUtils } from '../../../shared/prompt.js';
13+
import { OrgUtils } from '../../../shared/orgUtils.js';
1714

1815
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1916
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.site');
@@ -42,113 +39,79 @@ export default class LightningPreviewSite extends SfCommand<LightningPreviewSite
4239

4340
public async run(): Promise<LightningPreviewSiteResult> {
4441
const { flags } = await this.parse(LightningPreviewSite);
45-
46-
// 1. Collect Flags
47-
let siteName = flags.name ?? 'B2C_CodeCept';
42+
// Connect to Org
43+
const connection = flags['target-org'].getConnection();
44+
45+
// If we don't have a site to use, promp the user for one
46+
let siteName = flags.name;
47+
if (!siteName) {
48+
this.log('No site name was specified, pick one');
49+
// Query for the list of possible sites
50+
const siteList = await OrgUtils.retrieveSites(connection);
51+
siteName = await PromptUtils.promptUserToSelectSite(siteList);
52+
}
4853
this.log(`Setting up local development for: ${siteName}`);
49-
siteName = siteName.trim().replace(' ', '_');
50-
// TODO don't redownload the app
51-
if (!fs.existsSync('app')) {
52-
// this.log('getting org connection');
5354

54-
// 2. Connect to Org
55-
const connection = flags['target-org'].getConnection();
56-
// 3. Check if the site exists
57-
// this.log('checking site exists');
58-
// TODO cleanup query
55+
siteName = siteName.trim().replace(' ', '_');
56+
const siteDir = path.join('__local_dev__', siteName);
57+
if (!fs.existsSync(path.join(siteDir, 'ssr.js'))) {
58+
// Ensure local dev dir is created
59+
fs.mkdirSync('__local_dev__');
60+
// 3. Check if the site has been published
5961
const result = await connection.query<{ Id: string; Name: string; LastModifiedDate: string }>(
60-
"SELECT Id, Name, LastModifiedDate FROM StaticResource WHERE Name LIKE 'MRT%" + siteName + "' LIMIT 1"
62+
"SELECT Id, Name, LastModifiedDate FROM StaticResource WHERE Name LIKE 'MRT%" + siteName + "'"
6163
);
6264

63-
// 4. Download the static resource
64-
if (result.records[0]) {
65-
const resourceName = result.records[0].Name;
66-
// this.log(`Found Site: ${resourceName}`);
67-
68-
this.log('Downloading Site...');
69-
const staticresource = await connection.metadata.read('StaticResource', resourceName);
70-
71-
if (staticresource?.content) {
72-
// 5a. Save the resource
73-
// const { contentType } = staticresource;
74-
const buffer = Buffer.from(staticresource.content, 'base64');
75-
// // const path = `${resourceName}.${contentType.split('/')[1]}`;
76-
const resourcePath = `${resourceName}.gz`;
77-
this.log(`Extracting -> '${resourcePath}'`);
78-
// this.log(`Writing file to path: ${resourcePath}`);
79-
fs.writeFileSync(resourcePath, buffer);
80-
81-
// Cleanup old directories
82-
fs.rmSync('app', { recursive: true, force: true });
83-
fs.rmSync('bld', { recursive: true, force: true });
84-
85-
// Extract to specific directory
86-
// Ensure output directory exists
87-
// fs.mkdirSync('app', { recursive: true });
88-
89-
// 5b. Extracting static resource
90-
await tar.x({
91-
file: resourcePath,
92-
});
93-
94-
fs.renameSync('bld', 'app');
95-
// fs.unlinkSync(tempPath); // Clean up the temporary file
96-
97-
// Setup the stream pipeline for unzipping
98-
// const pipe = promisify(pipeline);
99-
// const gunzip = zlib.createGunzip();
100-
// const inputStream = fs.createReadStream(resourcePath);
101-
// const output = fs.createWriteStream(path.join('app', 'bld'));
102-
// await pipe(inputStream, gunzip, output);
103-
104-
// 5c. Temp - copy a proxy file
105-
// TODO query for the url if we need to
106-
// const newResult = await connection.query<{ Name: string; UrlPathPrefix: string }>(
107-
// `SELECT Name, UrlPathPrefix FROM Network WHERE Name = '${siteName}'`
108-
// );
65+
let resourceName;
66+
// Pick the site you want if there is more than one
67+
if (result?.totalSize > 1) {
68+
const chooseFromList = result.records.map((record) => record.Name);
69+
resourceName = await PromptUtils.promptUserToSelectSite(chooseFromList);
70+
} else if (result?.totalSize === 1) {
71+
resourceName = result.records[0].Name;
72+
} else {
73+
throw new SfError(
74+
`Couldnt find your site: ${siteName}. Please navigate to the builder and publish your site with the Local Development preference enabled in your org.`
75+
);
76+
}
10977

110-
// TODO should be included with bundle
111-
const proxyPath = path.join('app', 'config', '_proxy');
112-
// fs.writeFileSync(
113-
// proxyPath,
114-
// '/services https://dsg000007tzqk2ak.test1.my.pc-rnd.site.com' +
115-
// '\n/sfsites https://dsg000007tzqk2ak.test1.my.pc-rnd.site.com' +
116-
// '\n/webruntime https://dsg000007tzqk2ak.test1.my.pc-rnd.site.com'
117-
// );
118-
// Temp write proxy file
119-
fs.writeFileSync(
120-
proxyPath,
121-
'/services https://dsg00000ayyw92ap.test1.my.pc-rnd.site.com' +
122-
'\n/sfsites https://dsg00000ayyw92ap.test1.my.pc-rnd.site.com' +
123-
'\n/webruntime https://dsg00000ayyw92ap.test1.my.pc-rnd.site.com' +
124-
'\n/mobify/proxy/core https://dsg00000ayyw92ap.test1.my.pc-rnd.site.com'
125-
);
126-
} else {
127-
this.error(`Static Resource for ${siteName} not found.`);
128-
}
78+
// Download the static resource
79+
this.log('Downloading Site...');
80+
const staticresource = await connection.metadata.read('StaticResource', resourceName);
81+
const resourcePath = path.join('__local_dev__', `${resourceName}.gz`);
82+
if (staticresource?.content) {
83+
// Save the static resource
84+
const buffer = Buffer.from(staticresource.content, 'base64');
85+
this.log(`Writing file to path: ${resourcePath}`);
86+
fs.writeFileSync(resourcePath, buffer);
12987
} else {
130-
throw new Error(`Couldnt find your site: ${siteName}`);
88+
throw new SfError(`Error occured downloading your site: ${siteName}`);
13189
}
90+
91+
const domains = await OrgUtils.getDomains(connection);
92+
const domain = await PromptUtils.promptUserToSelectDomain(domains);
93+
const urlPrefix = await OrgUtils.getSitePathPrefix(connection, siteName);
94+
const fullProxyUrl = `https://${domain}${urlPrefix}`;
95+
96+
// Setup Local Dev
97+
await setupDev({ mrtBundle: resourcePath, mrtDir: siteDir, proxyUrl: fullProxyUrl, npmInstall: false });
98+
this.log('Setup Complete!');
13299
} else {
133-
// this.log('Site already configured!');
100+
// If we do have the site setup already, don't do anything / TODO prompt the user if they want to get latest?
134101
}
135-
// Demo: Temp write Live Reload CSP
136-
// const filepath = './app/experience/site-metadata.json';
137-
// const csp = fs.readFileSync(filepath, 'utf-8');
138-
// if (!csp.includes('ws://127.0.0.1:35729/livereload')) {
139-
// const newContent = csp.replace(
140-
// /https:\/\/js\.stripe\.com;/g,
141-
// 'https://js.stripe.com ws://127.0.0.1:35729/livereload;'
142-
// );
143-
// fs.writeFileSync(filepath, newContent);
144-
// }
145-
this.log('Setup Complete!');
146102

147103
// 6. Start the dev server
148104
this.log('Starting local development server...');
149105
// TODO add additional args
150106
// eslint-disable-next-line unicorn/numeric-separators-style
151-
await expDev({ open: false, port: 3000, timeout: 30000, sandbox: false, logLevel: 'error' });
107+
await expDev({
108+
open: false,
109+
port: 3000,
110+
timeout: 30000,
111+
sandbox: false,
112+
logLevel: 'error',
113+
mrtBundleRoot: siteDir,
114+
});
152115
// const name = flags.name ?? 'world';
153116
// this.log(`hello ${name} from /Users/nkruk/git/plugin-lightning-dev/src/commands/lightning/preview/site.ts`);
154117
return {

src/shared/orgUtils.ts

Lines changed: 35 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

8-
import { Connection } from '@salesforce/core';
8+
import { Connection, SfError } from '@salesforce/core';
99

1010
export class OrgUtils {
1111
/**
@@ -38,4 +38,38 @@ export class OrgUtils {
3838

3939
return undefined;
4040
}
41+
42+
public static async retrieveSites(conn: Connection): Promise<string[]> {
43+
const result = await conn.query<{ Name: string; UrlPathPrefix: string; SiteType: string; Status: string }>(
44+
'SELECT Name, UrlPathPrefix, SiteType, Status FROM Site'
45+
);
46+
if (!result.records.length) {
47+
throw new SfError('No sites found.');
48+
}
49+
const siteNames = result.records.map((record) => record.Name).sort();
50+
return siteNames;
51+
}
52+
53+
/**
54+
* Given a site name, it queries the org to find the matching site.
55+
*
56+
* @param connection the connection to the org
57+
* @param siteName the name of the app
58+
* @returns the site prefix or empty string if no match is found
59+
*/
60+
public static async getSitePathPrefix(connection: Connection, siteName: string): Promise<string> {
61+
// TODO seems like there are 2 copies of each site? ask about this - as the #1 is apended to our site type
62+
const devNameQuery = `SELECT Id, Name, SiteType, UrlPathPrefix FROM Site WHERE Name LIKE '${siteName}1'`;
63+
const result = await connection.query<{ UrlPathPrefix: string }>(devNameQuery);
64+
if (result.totalSize > 0) {
65+
return '/' + result.records[0].UrlPathPrefix;
66+
}
67+
return '';
68+
}
69+
70+
public static async getDomains(connection: Connection): Promise<string[]> {
71+
const devNameQuery = 'SELECT Id, Domain, LastModifiedDate FROM Domain';
72+
const results = await connection.query<{ Domain: string }>(devNameQuery);
73+
return results.records.map((result) => result.Domain);
74+
}
4175
}

src/shared/prompt.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
import select from '@inquirer/select';
8+
9+
export class PromptUtils {
10+
public static async promptUserToSelectSite(sites: string[]): Promise<string> {
11+
const choices = sites.map((site) => ({ value: site }));
12+
const response = await select({
13+
message: 'Select a site:',
14+
choices,
15+
});
16+
17+
return response;
18+
}
19+
20+
public static async promptUserToSelectDomain(domains: string[]): Promise<string> {
21+
const choices = domains.map((domain) => ({ value: domain }));
22+
const response = await select({
23+
message: 'Select a Domain:',
24+
choices,
25+
});
26+
27+
return response;
28+
}
29+
}

0 commit comments

Comments
 (0)