Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/generators/microfrontendGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright (c) 2026, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { camelCaseToTitleCase } from '@salesforce/kit';
import * as path from 'path';
import { nls } from '../i18n';
import { CreateUtil } from '../utils';
import { MicrofrontendOptions } from '../utils/types';
import { BaseGenerator } from './baseGenerator';

function isAllowedSrcUrl(src: string): boolean {
let parsed: URL;
try {
parsed = new URL(src);
} catch {
return false;
}
if (parsed.protocol === 'https:') {
return true;
}
if (parsed.protocol === 'http:') {
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
}
return false;
}

const VALID_SANDBOX_TOKENS = new Set([
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-storage-access-by-user-activation',
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
]);

export default class MicrofrontendGenerator extends BaseGenerator<MicrofrontendOptions> {
public validateOptions(): void {
CreateUtil.checkInputs(this.options.componentname);

const fileparts = path.resolve(this.outputdir).split(path.sep);
if (!this.options.internal && !fileparts.includes('lwc')) {
throw new Error(nls.localize('MissingLWCDir'));
}

if (!isAllowedSrcUrl(this.options.src)) {
throw new Error(nls.localize('InvalidMicrofrontendSrcUrl'));
}

if (!this.options.shellTitle || !this.options.shellTitle.trim()) {
throw new Error(nls.localize('MissingMicrofrontendShellTitle'));
}

const tokens = this.options.sandbox.split(/\s+/).filter(Boolean);
const invalid = tokens.filter((t) => !VALID_SANDBOX_TOKENS.has(t));
if (invalid.length) {
throw new Error(
nls.localize('InvalidMicrofrontendSandboxToken', [
invalid.join(', '),
[...VALID_SANDBOX_TOKENS].join(', '),
])
);
}
}

public async generate(): Promise<void> {
const { componentname, src, sandbox, shellTitle, internal } = this.options;

const pascalCaseComponentName = `${componentname
.substring(0, 1)
.toUpperCase()}${componentname.substring(1)}`;
const camelCaseComponentName = `${componentname
.substring(0, 1)
.toLowerCase()}${componentname.substring(1)}`;

this.sourceRootWithPartialPath(path.join('microfrontend', 'default'));

await this.render(
this.templatePath('default.html'),
this.destinationPath(
path.join(
this.outputdir,
camelCaseComponentName,
`${camelCaseComponentName}.html`
)
),
{ sandbox, shellTitle }
);

await this.render(
this.templatePath('default.js'),
this.destinationPath(
path.join(
this.outputdir,
camelCaseComponentName,
`${camelCaseComponentName}.js`
)
),
{ pascalCaseComponentName, src }
);

await this.render(
this.templatePath('default.css'),
this.destinationPath(
path.join(
this.outputdir,
camelCaseComponentName,
`${camelCaseComponentName}.css`
)
),
{}
);

if (!internal) {
const masterLabel = camelCaseToTitleCase(componentname)
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
await this.render(
this.templatePath('default.js-meta.xml'),
this.destinationPath(
path.join(
this.outputdir,
camelCaseComponentName,
`${camelCaseComponentName}.js-meta.xml`
)
),
{ apiVersion: this.apiversion, masterLabel }
);
}
}
}
9 changes: 9 additions & 0 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,13 @@ export const messages = {
'Failed to load the FlexiPage templates repository. Please verify the URL is correct and accessible.',

AlphaNumericValidationError: '%s must contain only alphanumeric characters.',

InvalidMicrofrontendSrcUrl:
'The --src flag must be an absolute https URL (e.g., https://app.example.com). Plain http is only allowed for localhost or 127.0.0.1.',
InvalidMicrofrontendSandboxToken:
'Invalid sandbox tokens: %s. Valid tokens are: %s.',
MissingMicrofrontendShellTitle:
'The --shell-title flag is required and must be a non-empty string used as the iframe accessible name.',
MicrofrontendBundle:
'A Lightning Web Component that wraps the lightning-mfe-shell base component.',
};
5 changes: 5 additions & 0 deletions src/templates/microfrontend/default/default.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host {
display: block;
width: 100%;
height: 100%;
}
7 changes: 7 additions & 0 deletions src/templates/microfrontend/default/default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<lightning-mfe-shell
src={widgetUrl}
sandbox="<%= sandbox %>"
shell-title="<%= shellTitle %>">
</lightning-mfe-shell>
</template>
5 changes: 5 additions & 0 deletions src/templates/microfrontend/default/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class <%= pascalCaseComponentName %> extends LightningElement {
widgetUrl = '<%= src %>';
}
12 changes: 12 additions & 0 deletions src/templates/microfrontend/default/default.js-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion><%= apiVersion %></apiVersion>
<isExposed>true</isExposed>
<masterLabel><%= masterLabel %></masterLabel>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
<target>lightningCommunity__Page</target>
</targets>
</LightningComponentBundle>
12 changes: 12 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ApexTriggerGenerator from '../generators/apexTriggerGenerator';
import FlexipageGenerator from '../generators/flexipageGenerator';
import LightningAppGenerator from '../generators/lightningAppGenerator';
import LightningComponentGenerator from '../generators/lightningComponentGenerator';
import MicrofrontendGenerator from '../generators/microfrontendGenerator';
import LightningEventGenerator from '../generators/lightningEventGenerator';
import LightningInterfaceGenerator from '../generators/lightningInterfaceGenerator';
import LightningTestGenerator from '../generators/lightningTestGenerator';
Expand Down Expand Up @@ -51,6 +52,7 @@ export type Generators =
| typeof LightningTestGenerator
| typeof LightningInterfaceGenerator
| typeof DigitalExperienceSiteGenerator
| typeof MicrofrontendGenerator
| typeof ProjectGenerator
| typeof StaticResourceGenerator
| typeof VisualforceComponentGenerator
Expand All @@ -75,6 +77,7 @@ export enum TemplateType {
LightningInterface,
LightningTest,
DigitalExperienceSite,
Microfrontend,
Project,
VisualforceComponent,
VisualforcePage,
Expand All @@ -93,6 +96,7 @@ export const generators = new Map<TemplateType, GeneratorClass<any>>([
[TemplateType.LightningInterface, LightningInterfaceGenerator],
[TemplateType.LightningTest, LightningTestGenerator],
[TemplateType.DigitalExperienceSite, DigitalExperienceSiteGenerator],
[TemplateType.Microfrontend, MicrofrontendGenerator],
[TemplateType.Project, ProjectGenerator],
[TemplateType.StaticResource, StaticResourceGenerator],
[TemplateType.VisualforceComponent, VisualforceComponentGenerator],
Expand Down Expand Up @@ -180,6 +184,14 @@ export interface LightningTestOptions extends TemplateOptions {
internal: boolean;
}

export interface MicrofrontendOptions extends TemplateOptions {
componentname: string;
src: string;
sandbox: string;
shellTitle: string;
internal?: boolean;
}

export interface ProjectOptions extends TemplateOptions {
projectname: string;
defaultpackagedir: string;
Expand Down
Loading