ui5_easy_use
Version:
CLI tool for SAP ui5 and SAPUI5 projects to initialize apps, generate pages, insert form and table components, manage routing, and automate i18n bindings
1,750 lines (1,417 loc) • 423 kB
JavaScript
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 5004
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
class ProjectContextResolver {
constructor(fileRepository, logger = new StructuredLogger(), options = {}) {
if (!fileRepository) {
throw new Error("ProjectContextResolver requires a fileRepository.");
}
this.fileRepository = fileRepository;
this.logger = logger;
this.routerConfigPath = options.routerConfigPath || "webapp/ez5/initapp/config.json";
this.projectRoot = null;
this.webappRoot = null;
this.artifactPaths = null;
}
load() {
this.validateProjectStructure();
this.projectRoot = this.fileRepository.rootPath;
this.webappRoot = this._resolveWebappRoot();
this.artifactPaths = this._buildArtifactMap();
this.logger.info("Project context loaded", {
projectRoot: this.projectRoot,
webappRoot: this.webappRoot,
});
return {
projectRoot: this.projectRoot,
webappRoot: this.webappRoot,
artifactPaths: this.artifactPaths,
};
}
validateProjectStructure() {
const requiredPaths = ["webapp", "webapp/manifest.json"];
for (const requiredPath of requiredPaths) {
if (!this.fileRepository.pathExists(requiredPath)) {
throw new Error(`Missing required UI5 project artifact: ${requiredPath}`);
}
}
}
getProjectRoot() {
if (!this.projectRoot) {
this.load();
}
return this.projectRoot;
}
getArtifactPaths() {
if (!this.artifactPaths) {
this.load();
}
return this.artifactPaths;
}
_resolveWebappRoot() {
return this.fileRepository.resolvePath("webapp");
}
_buildArtifactMap() {
const resolveArtifact = (relativePath) => this.fileRepository.resolvePath(relativePath);
const i18nPrimaryPath = this._resolvePrimaryI18nPath();
return {
manifestPath: resolveArtifact("webapp/manifest.json"),
navListPath: resolveArtifact("webapp/model/navList.json"),
rulesNavListPath: resolveArtifact("webapp/model/rulesNavList.json"),
routerConfigPath: resolveArtifact(this.routerConfigPath),
controllerRoot: resolveArtifact("webapp/controller"),
viewRoot: resolveArtifact("webapp/view"),
fragmentRoot: resolveArtifact("webapp/fragment"),
modelRoot: resolveArtifact("webapp/model"),
ez5Root: resolveArtifact("webapp/ez5"),
i18nDirectory: resolveArtifact("webapp/i18n"),
i18nPrimaryPath,
};
}
_resolvePrimaryI18nPath() {
const candidates = [
"webapp/i18n/i18n_en.properties",
"webapp/i18n/i18n.properties",
"webapp/i18n/i18n_en_US.properties",
"webapp/i18n/i18n_ar.properties",
];
for (const candidate of candidates) {
if (this.fileRepository.pathExists(candidate)) {
return this.fileRepository.resolvePath(candidate);
}
}
if (this.fileRepository.pathExists("webapp/i18n")) {
const propertiesFile = this.fileRepository
.getAllFilesRecursively("webapp/i18n")
.find((filePath) => filePath.endsWith(".properties"));
if (propertiesFile) {
return this.fileRepository.resolvePath(propertiesFile);
}
}
return this.fileRepository.resolvePath("webapp/i18n/i18n_en.properties");
}
}
module.exports = {
ProjectContextResolver,
};
/***/ },
/***/ 4045
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { JsonDocumentRepository } = __webpack_require__(1118);
const { StructuredLogger } = __webpack_require__(7692);
const DEFAULT_ROUTER_CLASS = "sap.m.routing.Router";
const SUPPORTED_ROUTER_CLASSES = new Set([
"sap.m.routing.Router",
"sap.f.routing.Router",
]);
class RouterConfigRepository extends JsonDocumentRepository {
constructor(fileRepository, documentPath = "webapp/ez5/initapp/config.json", logger = new StructuredLogger()) {
super(fileRepository, documentPath, logger);
}
getDefaultDocument() {
return {
routerClass: DEFAULT_ROUTER_CLASS,
};
}
getRouterMode() {
const document = this.readDocument();
return this._normalizeRouterClass(document.routerClass);
}
setRouterMode(routerClass) {
const normalizedRouterClass = this._normalizeRouterClass(routerClass);
this.writeDocument({
routerClass: normalizedRouterClass,
});
this.logger.info("Router mode persisted", {
documentPath: this.documentPath,
routerClass: normalizedRouterClass,
});
return normalizedRouterClass;
}
_normalizeRouterClass(routerClass) {
if (!routerClass) {
return DEFAULT_ROUTER_CLASS;
}
if (!SUPPORTED_ROUTER_CLASSES.has(routerClass)) {
throw new Error(`Unsupported router class: ${routerClass}`);
}
return routerClass;
}
}
module.exports = {
DEFAULT_ROUTER_CLASS,
RouterConfigRepository,
SUPPORTED_ROUTER_CLASSES,
};
/***/ },
/***/ 1009
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
class FileSelectionService {
constructor(promptAdapter, fileRepository, logger = new StructuredLogger()) {
if (!promptAdapter) {
throw new Error("FileSelectionService requires a promptAdapter.");
}
if (!fileRepository) {
throw new Error("FileSelectionService requires a fileRepository.");
}
this.promptAdapter = promptAdapter;
this.fileRepository = fileRepository;
this.logger = logger;
}
async selectPaths(basePath = "") {
try {
const selectedPaths = await this.selectRecursively(basePath);
return this._filterSelectedFiles(selectedPaths);
} catch (error) {
this.logger.warn("File selection failed; returning empty selection", {
basePath,
error: error.message,
});
return [];
}
}
async selectRecursively(currentPath = "") {
const items = this.fileRepository.listItemsWithMetadata(currentPath);
if (items.length === 0) {
return [];
}
const selectedValues = await this.promptAdapter.selectMany(
items.map((item) => ({
name: item.name,
value: item.path,
short: item.name,
})),
`Select files or folders inside: ${currentPath || "root"}`
);
return this._expandSelection(items, selectedValues || []);
}
async _expandSelection(items, selectedValues) {
if (!Array.isArray(selectedValues) || selectedValues.length === 0) {
return [];
}
const selectedFolders = selectedValues.filter((selectedValue) => (
items.some((item) => item.path === selectedValue && item.isFolder)
));
const selectedFiles = selectedValues.filter((selectedValue) => (
items.some((item) => item.path === selectedValue && item.isFile)
));
if (selectedValues.length === 1 && selectedFolders.length === 1) {
return this.selectRecursively(selectedFolders[0]);
}
const resolvedPaths = [...selectedFiles];
for (const selectedFolder of selectedFolders) {
resolvedPaths.push(...this.fileRepository.getAllFilesRecursively(selectedFolder));
}
return resolvedPaths;
}
_filterSelectedFiles(selectedPaths) {
return Array.from(new Set(
(selectedPaths || []).filter((selectedPath) => (
Boolean(selectedPath) && this.fileRepository.pathExists(selectedPath)
))
));
}
}
module.exports = {
FileSelectionService,
};
/***/ },
/***/ 2699
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const path = __webpack_require__(6928);
const { StructuredLogger } = __webpack_require__(7692);
class ViewTargetSelectionService {
constructor(options = {}) {
const {
promptAdapter,
fileSelectionService,
projectFileRepository,
xmlTransformationService,
logger = new StructuredLogger(),
} = options;
if (!promptAdapter) {
throw new Error("ViewTargetSelectionService requires a promptAdapter.");
}
if (!fileSelectionService) {
throw new Error("ViewTargetSelectionService requires a fileSelectionService.");
}
if (!projectFileRepository) {
throw new Error("ViewTargetSelectionService requires a projectFileRepository.");
}
if (!xmlTransformationService) {
throw new Error("ViewTargetSelectionService requires an xmlTransformationService.");
}
this.promptAdapter = promptAdapter;
this.fileSelectionService = fileSelectionService;
this.projectFileRepository = projectFileRepository;
this.xmlTransformationService = xmlTransformationService;
this.logger = logger;
}
async selectViewMode(componentType) {
return this.promptAdapter.selectOne([
{ name: "Select Position", value: "select-position" },
{ name: "Change View", value: "change-view" },
], `Select target view mode for ${componentType}`);
}
async resolveTargetForController(controllerPath, viewMode) {
const viewPath = viewMode === "change-view"
? await this.selectManualViewPath()
: this.resolveMatchingViewPath(controllerPath);
if (!viewPath) {
this.logger.warn("Component target view could not be resolved", {
controllerPath,
viewMode,
});
return null;
}
const insertionPath = await this.selectInsertionPath(viewPath);
if (!insertionPath) {
this.logger.warn("Component insertion path was not selected", {
controllerPath,
viewPath,
});
return null;
}
return {
controllerPath,
insertionPath,
viewPath,
};
}
resolveMatchingViewPath(controllerPath) {
const artifactName = path
.basename(controllerPath, path.extname(controllerPath))
.replace(/\.controller$/, "");
const candidatePath = `webapp/view/${artifactName}.view.xml`;
return this.projectFileRepository.pathExists(candidatePath) ? candidatePath : null;
}
async selectManualViewPath(basePath = "webapp") {
const selectedPaths = await this.fileSelectionService.selectPaths(basePath);
const xmlPaths = Array.from(new Set(
(selectedPaths || []).filter((selectedPath) => selectedPath.endsWith(".xml"))
));
if (xmlPaths.length === 0) {
return null;
}
if (xmlPaths.length === 1) {
return xmlPaths[0];
}
return this.promptAdapter.selectOne(
xmlPaths.map((xmlPath) => ({
name: xmlPath,
value: xmlPath,
short: path.basename(xmlPath),
})),
"Select target view file"
);
}
async selectInsertionPath(viewPath) {
const hierarchyOptions = this.getHierarchyOptions(viewPath);
if (hierarchyOptions.length === 0) {
return null;
}
return this.promptAdapter.selectOne(
hierarchyOptions,
`Select insertion position inside: ${viewPath}`
);
}
getHierarchyOptions(viewPath) {
const xmlString = this.projectFileRepository.readText(viewPath);
const document = this.xmlTransformationService.xmlDocumentAdapter.parse(xmlString);
const hierarchyEntries = this.xmlTransformationService.xmlDocumentAdapter.listHierarchy(document);
return hierarchyEntries.map((entry) => {
const indent = " ".repeat(entry.depth);
const nodeId = entry.attributes.id ? ` #${entry.attributes.id}` : "";
return {
name: `${indent}${entry.name}${nodeId}`,
value: entry.path,
short: entry.name,
};
});
}
}
module.exports = {
ViewTargetSelectionService,
};
/***/ },
/***/ 465
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
class I18nPropertyService {
constructor(fileRepository, logger = new StructuredLogger()) {
if (!fileRepository) {
throw new Error("I18nPropertyService requires a fileRepository.");
}
this.fileRepository = fileRepository;
this.logger = logger;
}
appendMissingKeys(filePath, keyPairs, contextLabel) {
const missingKeyPairs = this.filterExistingKeys(filePath, keyPairs);
if (missingKeyPairs.length === 0) {
return [];
}
const sectionHeader = this.formatSectionHeader(contextLabel);
const currentContent = this.fileRepository.pathExists(filePath)
? this.fileRepository.readText(filePath)
: "";
const lines = currentContent.length > 0 ? currentContent.split(/\r?\n/) : [];
let headerIndex = lines.findIndex((line) => line.trim() === sectionHeader);
if (headerIndex === -1) {
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
lines.push("");
}
lines.push(sectionHeader);
headerIndex = lines.length - 1;
}
lines.splice(headerIndex + 1, 0, ...missingKeyPairs);
const nextContent = `${lines.join("\n").replace(/\n*$/, "")}\n`;
this.fileRepository.writeWithFolders(filePath, nextContent);
return missingKeyPairs;
}
filterExistingKeys(filePath, keyPairs) {
const normalizedKeyPairs = this._normalizeKeyPairs(keyPairs);
if (normalizedKeyPairs.length === 0) {
return [];
}
const existingKeys = this._readExistingKeys(filePath);
const seenKeys = new Set();
return normalizedKeyPairs.filter((keyPair) => {
const key = keyPair.slice(0, keyPair.indexOf("="));
if (existingKeys.has(key) || seenKeys.has(key)) {
return false;
}
seenKeys.add(key);
return true;
});
}
formatSectionHeader(contextLabel) {
const normalizedLabel = String(contextLabel || "Generated").trim() || "Generated";
return `# ${normalizedLabel}`;
}
_readExistingKeys(filePath) {
if (!this.fileRepository.pathExists(filePath)) {
return new Set();
}
const content = this.fileRepository.readText(filePath);
const lines = content.split(/\r?\n/);
const keys = new Set();
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith("#") || !trimmedLine.includes("=")) {
continue;
}
const separatorIndex = trimmedLine.indexOf("=");
const key = trimmedLine.slice(0, separatorIndex).trim();
if (key) {
keys.add(key);
}
}
return keys;
}
_normalizeKeyPairs(keyPairs) {
const lines = Array.isArray(keyPairs)
? keyPairs
: String(keyPairs || "").split(/\r?\n/);
return lines
.map((line) => String(line).trim())
.filter((line) => line.includes("="))
.map((line) => {
const separatorIndex = line.indexOf("=");
const key = line.slice(0, separatorIndex).trim();
const value = line.slice(separatorIndex + 1).trim();
return key ? `${key}=${value}` : "";
})
.filter(Boolean);
}
}
module.exports = {
I18nPropertyService,
};
/***/ },
/***/ 1296
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
const MAIN_MENU_ACTIONS = [
{ name: "## Create Init App 🌟", value: "init-app" },
{ name: "## Add Page 📄", value: "page-management" },
{ name: "## Insert Components 📝", value: "component-insert" },
{ name: "## Translate_i18n 🌐", value: "i18n-translation" },
{ name: "## Import Templates 🧩", value: "template-import" },
{ name: "## Exit ❌", value: "exit" },
];
class WorkflowRegistry {
constructor(logger = new StructuredLogger()) {
this.logger = logger;
this.workflows = new Map();
this.mainMenuActions = MAIN_MENU_ACTIONS.map((action) => ({ ...action }));
}
register(actionKey, workflow) {
this._validateWorkflow(actionKey, workflow);
this.workflows.set(actionKey, workflow);
return workflow;
}
resolve(actionKey) {
return this.workflows.get(actionKey) || null;
}
getMainMenuActions() {
return this.mainMenuActions.filter((action) => (
action.value === "exit" || this.workflows.has(action.value)
));
}
_validateWorkflow(actionKey, workflow) {
if (!actionKey) {
throw new Error("WorkflowRegistry.register requires an actionKey.");
}
if (!workflow || typeof workflow.execute !== "function") {
throw new Error(`WorkflowRegistry.register requires a workflow with execute(): ${actionKey}`);
}
}
}
module.exports = {
WorkflowRegistry,
};
/***/ },
/***/ 6552
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const fs = __webpack_require__(9896);
const path = __webpack_require__(6928);
const { ProjectContextResolver } = __webpack_require__(5004);
const { RouterConfigRepository } = __webpack_require__(4045);
const { FileSelectionService } = __webpack_require__(1009);
const { ViewTargetSelectionService } = __webpack_require__(2699);
const { I18nPropertyService } = __webpack_require__(465);
const { CliSessionShell } = __webpack_require__(9980);
const { NavigationArtifactRepository } = __webpack_require__(5389);
const { RouteRoleArtifactRepository } = __webpack_require__(5394);
const { RoutingArtifactRepository } = __webpack_require__(569);
const { Ui5ControllerMetadataExtractor } = __webpack_require__(3786);
const { Ui5JavaScriptTransformer } = __webpack_require__(9963);
const { Ui5XmlTransformationService } = __webpack_require__(8450);
const { XmlDocumentAdapter } = __webpack_require__(9589);
const { ComponentInsertionWorkflow } = __webpack_require__(7240);
const { InitAppWorkflow } = __webpack_require__(8894);
const { I18nTranslationWorkflow } = __webpack_require__(3354);
const { PageManagementWorkflow } = __webpack_require__(2478);
const { TemplateImportWorkflow } = __webpack_require__(4898);
const { FileRepository } = __webpack_require__(5099);
const { PromptAdapter } = __webpack_require__(6700);
const { TemplateCatalog } = __webpack_require__(8188);
const { StructuredLogger } = __webpack_require__(7692);
const { WorkflowRegistry } = __webpack_require__(1296);
const DEFAULT_ENVIRONMENT = "prd";
const SUPPORTED_ENVIRONMENTS = new Set(["dev", "prd"]);
function createCliRuntime(options = {}) {
const environmentName = normalizeEnvironmentName(
options.environmentName || options.envName || process.env.EZ5_ENV
);
const currentWorkingDirectory = path.resolve(options.cwd || process.cwd());
const projectRoot = path.resolve(
options.projectRoot || resolveTargetProjectRoot(currentWorkingDirectory, environmentName)
);
const packageRoot = options.packageRoot || resolvePackageRoot(options.runtimeDirectory || __dirname);
const filesystemRoot = path.parse(projectRoot).root;
const logger = options.logger || new StructuredLogger(options.loggerOptions);
const promptAdapter = options.promptAdapter || new PromptAdapter({
logger,
promptRuntime: options.promptRuntime,
});
const projectFileRepository = new FileRepository(projectRoot, logger);
const packageFileRepository = new FileRepository(packageRoot, logger);
const workspaceFileRepository = new FileRepository(filesystemRoot, logger);
const projectContextResolver = new ProjectContextResolver(projectFileRepository, logger);
const templateCatalog = new TemplateCatalog(packageFileRepository, logger, {
customTemplateRepository: projectFileRepository,
});
const routerConfigRepository = new RouterConfigRepository(projectFileRepository, undefined, logger);
const routingArtifactRepository = new RoutingArtifactRepository(projectFileRepository, undefined, logger);
const navigationArtifactRepository = new NavigationArtifactRepository(projectFileRepository, undefined, logger);
const routeRoleArtifactRepository = new RouteRoleArtifactRepository(projectFileRepository, undefined, logger);
const fileSelectionService = new FileSelectionService(promptAdapter, projectFileRepository, logger);
const metadataExtractor = new Ui5ControllerMetadataExtractor(workspaceFileRepository, logger);
const javaScriptTransformer = new Ui5JavaScriptTransformer(workspaceFileRepository, logger, metadataExtractor);
const xmlTransformationService = new Ui5XmlTransformationService(new XmlDocumentAdapter(logger), logger);
const viewTargetSelectionService = new ViewTargetSelectionService({
promptAdapter,
fileSelectionService,
projectFileRepository,
xmlTransformationService,
logger,
});
const i18nPropertyService = new I18nPropertyService(projectFileRepository, logger);
const initAppWorkflow = new InitAppWorkflow({
promptAdapter,
projectContextResolver,
templateCatalog,
projectFileRepository,
workspaceFileRepository,
routerConfigRepository,
routingArtifactRepository,
javaScriptTransformer,
xmlTransformationService,
logger,
});
const pageManagementWorkflow = new PageManagementWorkflow({
promptAdapter,
projectContextResolver,
templateCatalog,
projectFileRepository,
workspaceFileRepository,
javaScriptTransformer,
routerConfigRepository,
routingArtifactRepository,
navigationArtifactRepository,
routeRoleArtifactRepository,
logger,
});
const componentInsertionWorkflow = new ComponentInsertionWorkflow({
promptAdapter,
fileSelectionService,
viewTargetSelectionService,
templateCatalog,
projectContextResolver,
projectFileRepository,
workspaceFileRepository,
javaScriptTransformer,
metadataExtractor,
xmlTransformationService,
logger,
});
const i18nTranslationWorkflow = new I18nTranslationWorkflow({
promptAdapter,
fileSelectionService,
templateCatalog,
projectContextResolver,
projectFileRepository,
xmlTransformationService,
i18nPropertyService,
logger,
});
const templateImportWorkflow = new TemplateImportWorkflow({
promptAdapter,
projectContextResolver,
templateCatalog,
projectFileRepository,
logger,
});
const workflowRegistry = new WorkflowRegistry(logger);
workflowRegistry.register("init-app", initAppWorkflow);
workflowRegistry.register("page-management", pageManagementWorkflow);
workflowRegistry.register("component-insert", componentInsertionWorkflow);
workflowRegistry.register("i18n-translation", i18nTranslationWorkflow);
workflowRegistry.register("template-import", templateImportWorkflow);
const cliSessionShell = new CliSessionShell(promptAdapter, workflowRegistry, logger);
return {
cliSessionShell,
environmentName,
logger,
paths: {
sourcePath: packageRoot,
targetPath: projectRoot,
},
promptAdapter,
projectContextResolver,
workflowRegistry,
workflows: {
componentInsertionWorkflow,
i18nTranslationWorkflow,
initAppWorkflow,
pageManagementWorkflow,
templateImportWorkflow,
},
services: {
fileSelectionService,
i18nPropertyService,
javaScriptTransformer,
metadataExtractor,
navigationArtifactRepository,
projectFileRepository,
routeRoleArtifactRepository,
routerConfigRepository,
routingArtifactRepository,
templateCatalog,
viewTargetSelectionService,
workspaceFileRepository,
xmlTransformationService,
},
};
}
function resolveTargetProjectRoot(currentWorkingDirectory, environmentName) {
if (environmentName === "dev") {
return path.resolve(currentWorkingDirectory, "../projectxxtest4");
}
return currentWorkingDirectory;
}
function normalizeEnvironmentName(environmentName) {
const normalizedEnvironmentName = String(environmentName || DEFAULT_ENVIRONMENT).trim().toLowerCase();
return SUPPORTED_ENVIRONMENTS.has(normalizedEnvironmentName)
? normalizedEnvironmentName
: DEFAULT_ENVIRONMENT;
}
function resolvePackageRoot(runtimeDirectory) {
const runtimeRoot = path.resolve(runtimeDirectory || __dirname);
const candidateRoots = [
path.resolve(runtimeRoot, "..", "..", ".."),
runtimeRoot,
];
for (const candidateRoot of candidateRoots) {
if (fs.existsSync(path.join(candidateRoot, "ez5", "templates"))) {
return candidateRoot;
}
}
return candidateRoots[0];
}
module.exports = {
createCliRuntime,
DEFAULT_ENVIRONMENT,
SUPPORTED_ENVIRONMENTS,
resolvePackageRoot,
};
/***/ },
/***/ 9980
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
class CliSessionShell {
constructor(promptAdapter, workflowRegistry, logger = new StructuredLogger()) {
if (!promptAdapter) {
throw new Error("CliSessionShell requires a promptAdapter.");
}
if (!workflowRegistry) {
throw new Error("CliSessionShell requires a workflowRegistry.");
}
this.promptAdapter = promptAdapter;
this.workflowRegistry = workflowRegistry;
this.logger = logger;
this.navigationState = "main";
this.isPromptActive = false;
}
async start() {
try {
while (this.navigationState !== "exit") {
await this.showMainMenu();
}
} catch (error) {
await this._handleWorkflowError("start", error);
throw error;
}
}
async showMainMenu() {
this.navigationState = "main";
const actionKey = await this._selectOne(
this.workflowRegistry.getMainMenuActions(),
"\n-----------\nSelect your main action:"
);
if (!actionKey) {
this.exit();
return;
}
await this.navigate(actionKey);
}
async navigate(actionKey) {
const resolvedAction = this._resolveAction(actionKey);
if (!resolvedAction) {
this.logger.error("Unknown CLI action selected", { actionKey });
this.returnToMainMenu();
return;
}
try {
await resolvedAction();
} catch (error) {
await this._handleWorkflowError(actionKey, error);
this.returnToMainMenu();
}
}
returnToMainMenu() {
this.navigationState = "main";
}
exit() {
this.navigationState = "exit";
}
_resolveAction(actionKey) {
if (actionKey === "exit") {
return async () => {
this.exit();
};
}
if (actionKey === "init-app") {
return () => this._showInitAppMenu();
}
if (actionKey === "component-insert") {
return () => this._showComponentMenu();
}
if (actionKey === "i18n-translation") {
return () => this._showI18nMenu();
}
return () => this._executeRegisteredWorkflow(actionKey);
}
async _executeRegisteredWorkflow(actionKey, ...args) {
const workflow = this.workflowRegistry.resolve(actionKey);
if (!workflow) {
this.logger.error("No workflow registered for action", { actionKey });
return;
}
await workflow.execute(...args);
this.returnToMainMenu();
}
async _showInitAppMenu() {
this.navigationState = "init-app";
const selectedAction = await this._selectOne([
{ name: "Full Features (Full Project)", value: "full-project" },
{ name: "Back 🔙", value: "back" },
{ name: "Exit ❌", value: "exit" },
], "\n-----------\nSelect Action");
if (selectedAction === "exit") {
this.exit();
return;
}
if (!selectedAction || selectedAction === "back") {
this.returnToMainMenu();
return;
}
await this._executeRegisteredWorkflow("init-app");
}
async _showComponentMenu() {
this.navigationState = "component-insert";
const selectedComponentType = await this._selectOne([
{ name: "📄 Form Menu:", value: "form" },
{ name: "📊 Table Menu:", value: "table" },
{ name: "📈 Chart Menu:", value: "chart" },
{ name: "Back 🔙", value: "back" },
{ name: "Exit ❌", value: "exit" },
], "\n-----------\nSelect Action");
if (selectedComponentType === "exit") {
this.exit();
return;
}
if (!selectedComponentType || selectedComponentType === "back") {
this.returnToMainMenu();
return;
}
await this._executeRegisteredWorkflow("component-insert", selectedComponentType);
}
async _showI18nMenu() {
this.navigationState = "i18n-translation";
const selectedAction = await this._selectOne([
{ name: "Auto Binding", value: "auto-binding" },
{ name: "Back 🔙", value: "back" },
{ name: "Exit ❌", value: "exit" },
], "\n-----------\nSelect Action:");
if (selectedAction === "exit") {
this.exit();
return;
}
if (!selectedAction || selectedAction === "back") {
this.returnToMainMenu();
return;
}
await this._executeRegisteredWorkflow("i18n-translation");
}
async _handleWorkflowError(actionKey, error) {
this.logger.error("CLI workflow execution failed", {
actionKey,
error: error?.message || String(error),
});
}
async _selectOne(options, message) {
this.isPromptActive = true;
try {
return await this.promptAdapter.selectOne(options, message);
} finally {
this.isPromptActive = false;
}
}
}
module.exports = {
CliSessionShell,
};
/***/ },
/***/ 1118
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { StructuredLogger } = __webpack_require__(7692);
class JsonDocumentRepository {
constructor(fileRepository, documentPath, logger = new StructuredLogger()) {
if (!fileRepository) {
throw new Error("JsonDocumentRepository requires a fileRepository.");
}
if (!documentPath) {
throw new Error("JsonDocumentRepository requires a documentPath.");
}
this.fileRepository = fileRepository;
this.documentPath = documentPath;
this.logger = logger;
}
getDefaultDocument() {
return {};
}
readDocument() {
if (!this.fileRepository.pathExists(this.documentPath)) {
return this._clone(this.getDefaultDocument());
}
const document = this.fileRepository.readJson(this.documentPath);
if (!document || typeof document !== "object" || Array.isArray(document)) {
throw new Error(`Invalid JSON document shape: ${this.documentPath}`);
}
return document;
}
writeDocument(document) {
if (!document || typeof document !== "object" || Array.isArray(document)) {
throw new Error(`Cannot persist invalid JSON document: ${this.documentPath}`);
}
this.fileRepository.writeJson(this.documentPath, document);
return document;
}
updateDocument(mutator) {
const document = this.readDocument();
const result = typeof mutator === "function" ? mutator(document) : document;
return this.writeDocument(result === undefined ? document : result);
}
_clone(value) {
return JSON.parse(JSON.stringify(value));
}
}
module.exports = {
JsonDocumentRepository,
};
/***/ },
/***/ 5389
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { JsonDocumentRepository } = __webpack_require__(1118);
class NavigationArtifactRepository extends JsonDocumentRepository {
constructor(fileRepository, documentPath = "webapp/model/navList.json", logger) {
super(fileRepository, documentPath, logger);
}
getDefaultDocument() {
return {
navigation: [],
additionalNavigation: [],
};
}
getEntries() {
const document = this.readDocument();
document.navigation = Array.isArray(document.navigation) ? document.navigation : [];
return document.navigation;
}
upsertEntry(pageName, icon = "sap-icon://settings") {
return this.updateDocument((document) => {
document.navigation = Array.isArray(document.navigation) ? document.navigation : [];
document.additionalNavigation = Array.isArray(document.additionalNavigation)
? document.additionalNavigation
: [];
const key = `Route${pageName}`;
const existingIndex = document.navigation.findIndex((entry) => entry.key === key);
const nextEntry = {
title: pageName,
icon,
key,
};
if (existingIndex === -1) {
document.navigation.push(nextEntry);
} else {
document.navigation[existingIndex] = {
...document.navigation[existingIndex],
...nextEntry,
};
}
return document;
});
}
clearEntries() {
return this.writeDocument(this.getDefaultDocument());
}
}
module.exports = {
NavigationArtifactRepository,
};
/***/ },
/***/ 5394
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { JsonDocumentRepository } = __webpack_require__(1118);
class RouteRoleArtifactRepository extends JsonDocumentRepository {
constructor(fileRepository, documentPath = "webapp/model/rulesNavList.json", logger) {
super(fileRepository, documentPath, logger);
}
getDefaultDocument() {
return {
roles: {},
};
}
getRoles(routeKey) {
const document = this.readDocument();
document.roles = document.roles || {};
return document.roles[routeKey] || [];
}
addRole(routeKey, role) {
if (!routeKey || !role) {
throw new Error("addRole requires both routeKey and role.");
}
return this.updateDocument((document) => {
document.roles = document.roles || {};
document.roles[routeKey] = Array.isArray(document.roles[routeKey]) ? document.roles[routeKey] : [];
if (!document.roles[routeKey].includes(role)) {
document.roles[routeKey].push(role);
}
return document;
});
}
setRoles(routeKey, roles) {
return this.updateDocument((document) => {
document.roles = document.roles || {};
document.roles[routeKey] = Array.from(new Set(Array.isArray(roles) ? roles : []));
return document;
});
}
clearRoles(routeKey) {
return this.updateDocument((document) => {
document.roles = document.roles || {};
delete document.roles[routeKey];
return document;
});
}
clearAll() {
return this.writeDocument(this.getDefaultDocument());
}
}
module.exports = {
RouteRoleArtifactRepository,
};
/***/ },
/***/ 569
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const { JsonDocumentRepository } = __webpack_require__(1118);
class RoutingArtifactRepository extends JsonDocumentRepository {
constructor(fileRepository, documentPath = "webapp/manifest.json", logger) {
super(fileRepository, documentPath, logger);
}
getDefaultDocument() {
return {
"sap.app": {
id: "",
},
"sap.ui5": {
routing: {
config: {
routerClass: "sap.m.routing.Router",
controlAggregation: "pages",
},
routes: [],
targets: {},
},
},
};
}
getAppName() {
const document = this.readDocument();
return document["sap.app"]?.id || "";
}
configureRouter(routerClass) {
return this.updateDocument((document) => {
const routing = this._ensureRoutingStructures(document);
routing.config.routerClass = this._normalizeRouterClass(routerClass);
routing.config.controlAggregation = routing.config.routerClass === "sap.f.routing.Router"
? "beginColumnPages"
: "pages";
this._syncRootViewId(document, routing.config.routerClass);
return document;
});
}
clearRoutesAndTargets() {
return this.updateDocument((document) => {
const routing = this._ensureRoutingStructures(document);
routing.routes = [];
routing.targets = {};
return document;
});
}
upsertPageRoute(pageName, routeArgs = {}) {
if (!pageName) {
throw new Error("upsertPageRoute requires a pageName.");
}
return this.updateDocument((document) => {
const routing = this._ensureRoutingStructures(document);
const routerClass = this._normalizeRouterClass(routeArgs.routerClass || routeArgs.type);
routing.config.routerClass = routerClass;
routing.config.controlAggregation = routeArgs.controlAggregation
|| (routerClass === "sap.f.routing.Router" ? "beginColumnPages" : "pages");
this._syncRootViewId(document, routerClass);
const routeDefinition = this._buildRouteDefinition(pageName, routeArgs);
const routeIndex = routing.routes.findIndex((route) => route.name === routeDefinition.name);
if (routeIndex === -1) {
routing.routes.push(routeDefinition);
} else {
routing.routes[routeIndex] = routeDefinition;
}
const targetDefinitions = this._buildTargetDefinition(pageName, routeArgs, routerClass);
routing.targets = routing.targets || {};
Object.assign(routing.targets, targetDefinitions);
return document;
});
}
_buildRouteDefinition(pageName, routeArgs) {
const explicitTargetOrder = Array.isArray(routeArgs.targetsOrdered) ? routeArgs.targetsOrdered : null;
const additionalTargets = Array.isArray(routeArgs.targets) ? routeArgs.targets : [];
const targetList = explicitTargetOrder
? Array.from(new Set(explicitTargetOrder))
: Array.from(new Set([`Target${pageName}`, ...additionalTargets]));
return {
name: routeArgs.routeName || `Route${pageName}`,
pattern: routeArgs.pattern !== undefined
? routeArgs.pattern
: (pageName === "Home" ? "" : `Route${pageName}${routeArgs.parameters || routeArgs.parms || ""}`),
target: targetList,
};
}
_buildTargetDefinition(pageName, routeArgs, routerClass) {
if (routeArgs.targetDefinitions && typeof routeArgs.targetDefinitions === "object") {
return routeArgs.targetDefinitions;
}
const targetDefinition = {
viewType: "XML",
transition: "slide",
clearControlAggregation: false,
viewId: pageName,
viewName: pageName,
};
if (routerClass === "sap.f.routing.Router" || routeArgs.controlAggregation) {
targetDefinition.controlAggregation = routeArgs.controlAggregation || "beginColumnPages";
}
return {
[`Target${pageName}`]: targetDefinition,
};
}
_ensureRoutingStructures(document) {
document["sap.app"] = document["sap.app"] || { id: "" };
document["sap.ui5"] = document["sap.ui5"] || {};
document["sap.ui5"].routing = document["sap.ui5"].routing || {};
document["sap.ui5"].routing.config = document["sap.ui5"].routing.config || {};
document["sap.ui5"].routing.routes = document["sap.ui5"].routing.routes || [];
document["sap.ui5"].routing.targets = document["sap.ui5"].routing.targets || {};
return document["sap.ui5"].routing;
}
_syncRootViewId(document, routerClass) {
const rootView = document["sap.ui5"]?.rootView;
if (!rootView || typeof rootView !== "object") {
return;
}
if (routerClass === "sap.f.routing.Router") {
delete rootView.id;
}
}
_normalizeRouterClass(routerClass) {
if (!routerClass) {
return "sap.m.routing.Router";
}
if (routerClass !== "sap.m.routing.Router" && routerClass !== "sap.f.routing.Router") {
throw new Error(`Unsupported router class: ${routerClass}`);
}
return routerClass;
}
}
module.exports = {
RoutingArtifactRepository,
};
/***/ },
/***/ 3786
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const acorn = __webpack_require__(8630);
const walk = __webpack_require__(2268);
const { StructuredLogger } = __webpack_require__(7692);
class Ui5ControllerMetadataExtractor {
constructor(fileRepository, logger = new StructuredLogger(), parserAdapter = acorn) {
if (!fileRepository) {
throw new Error("Ui5ControllerMetadataExtractor requires a fileRepository.");
}
this.fileRepository = fileRepository;
this.logger = logger;
this.parserAdapter = parserAdapter;
}
extractVariableFromFunction(controllerPath, functionName = "initialForm", variableName = "this.autoG") {
const content = this.fileRepository.readText(controllerPath);
try {
const ast = this._parseModule(content);
const functionNode = this._findFunctionNode(ast, functionName);
if (functionNode) {
return this._extractDeclarations(functionNode, content, variableName);
}
} catch (error) {
this.logger.debug("Falling back to regex metadata extraction", {
controllerPath,
functionName,
error: error.message,
});
}
return this._extractVariableFromFunctionText(content, functionName, variableName);
}
extractAutoG(controllerPath, functionName = "initialForm") {
const rawValue = this.extractVariableFromFunction(controllerPath, functionName, "this.autoG");
return rawValue ? this.parseAutoG(rawValue) : null;
}
parseAutoG(rawValue) {
if (rawValue == null) {
return null;
}
if (Array.isArray(rawValue) || (typeof rawValue === "object" && rawValue !== null)) {
return rawValue;
}
if (typeof rawValue !== "string") {
return null;
}
try {
return Function(`"use strict"; return (${rawValue});`)();
} catch (error) {
this.logger.warn("Failed to parse autoG value", { error: error.message });
return null;
}
}
_parseModule(content) {
try {
return this.parserAdapter.parse(content, {
ecmaVersion: "latest",
sourceType: "script",
});
} catch (error) {
return this.parserAdapter.parse(content, {
ecmaVersion: "latest",
sourceType: "module",
});
}
}
_findFunctionNode(ast, functionName) {
let foundNode = null;
walk.simple(ast, {
MethodDefinition: (node) => {
if (foundNode) {
return;
}
const methodName = node.key?.name || node.key?.value;
if (methodName === functionName && node.value) {
foundNode = node.value;
}
},
CallExpression: (node) => {
if (foundNode) {
return;
}
if (node.callee?.type !== "MemberExpression" || node.callee.property?.name !== "extend") {
return;
}
const objectExpression = node.arguments[1];
if (!objectExpression || objectExpression.type !== "ObjectExpression") {
return;
}
for (const property of objectExpression.properties) {
const propertyName = property.key?.name || property.key?.value;
if (
property.type === "Property"
&& propertyName === functionName
&& (
property.value.type === "FunctionExpression"
|| property.value.type === "ArrowFunctionExpression"
)
) {
foundNode = property.value;
return;
}
}
},
});
return foundNode;
}
_extractDeclarations(functionNode, content, variableName) {
const statements = functionNode.body?.body || [];
const isThisAssignment = variableName.startsWith("this.");
const targetPropertyName = isThisAssignment ? variableName.slice(5) : variableName;
for (const statement of statements) {
if (
isThisAssignment
&& statement.type === "ExpressionStatement"
&& statement.expression.type === "AssignmentExpression"
&& statement.expression.left.type === "MemberExpression"
&& statement.expression.left.object.type === "ThisExpression"
&& statement.expression.left.property?.name === targetPropertyName
) {
return content.slice(statement.expression.right.start, statement.expression.right.end);
}
if (statement.type === "VariableDeclaration") {
for (const declaration of statement.declarations) {
if (declaration.id?.name === targetPropertyName && declaration.init) {
return content.slice(declaration.init.start, declaration.init.end);
}
}
}
}
return null;
}
_extractVariableFromFunctionText(content, functionName, variableName) {
const functionBody = this._extractFunctionTextBody(content, functionName);
if (!functionBody) {
return null;
}
const targetName = variableName.startsWith("this.") ? variableName.slice(5) : variableName;
const assignmentMatch = functionBody.match(new RegExp(`\\bthis\\.${this._escapeRegExp(targetName)}\\s*=`));
if (!assignmentMatch) {
return null;
}
const expressionStart = assignmentMatch.index + assignmentMatch[0].length;
return this._readExpressionUntilSemicolon(functionBody, expressionStart);
}
_extractFunctionTextBody(content, functionName) {
const functionPattern = new RegExp(`(?:${this._escapeRegExp(functionName)}\\s*:\\s*(?:async\\s*)?function|(?:async\\s+)?${this._escapeRegExp(functionName)})\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`);
const match = content.match(functionPattern);
if (!match) {
return "";
}
const openingBrace = match.index + match[0].lastIndexOf("{");
const closingBrace = this._findMatchingBrace(content, openingBrace);
if (closingBrace === -1) {
return "";
}
return content.slice(openingBrace + 1, closingBrace);
}
_readExpressionUntilSemicolon(content, expressionStart) {
let quote = null;
let escaped = false;
let squareDepth = 0;
let curlyDepth = 0;
let parenDepth = 0;
for (let index = expressionStart; index < content.length; index++) {
const character = content[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}
if (character === "\"" || character === "'" || character === "`") {
quote = character;
continue;
}
if (character === "[") {
squareDepth += 1;
} else if (character === "]") {
squareDepth -= 1;
} else if (character === "{") {
curlyDepth += 1;
} else if (character === "}") {
curlyDepth -= 1;
} else if (character === "(") {
parenDepth += 1;
} else if (character === ")") {
parenDepth -= 1;
} else if (character === ";" && squareDepth === 0 && curlyDepth === 0 && parenDepth === 0) {
return content.slice(expressionStart, index).trim();
}
}
return content.slice(expressionStart).trim();
}
_findMatchingBrace(content, openingBraceIndex) {
let depth = 0;
let quote = null;
let escaped = false;
for (let index = openingBraceIndex; index < content.length; index++) {
const character = content[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}
if (character === "\"" || character === "'" || character === "`") {
quote = character;
continue;
}
if (character === "{") {
depth += 1;
} else if (character === "}") {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
return -1;
}
_escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}
module.exports = {
Ui5ControllerMetadataExtractor,
};
/***/ },
/***/ 9963
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
const acorn = __webpack_require__(8630);
const walk = __webpack_require__(2268);
const { StructuredLogger } = __webpack_require__(7692);
const { Ui5ControllerMetadataExtractor } = __webpack_require__(3786);
class Ui5JavaScriptTransformer {
constructor(fileRepository, logger = new StructuredLogger(), metadataExtractor = null) {
if (!fileRepository) {
throw new Error("Ui5JavaScriptTransformer requires a fileRepository.");
}
this.fileRepository = fileRepository;
this.logger = logger;
this.metadataExtractor = metadataExtractor || new Ui5ControllerMetadataExtractor(fileRepository, logger);
}
syncTemplate(sourceTemplatePath, destinationPath, placeholderMap = {}) {
const { sourceContent, destinationContent } = this._readArtifacts(sourceTemplatePath, destinationPath);
const resolvedSourceContent = this._replacePlaceholders(sourceContent, placeholderMap);
if (this._isTypeScriptController(destinationPath, destinationContent)) {
const updatedContent = this.syncTemplateIntoTypeScriptController(resolvedSourceContent, destinationContent);
this.updateDestination(destinationPath, updatedContent);
return updatedContent;
}
let updatedContent = this.mergeDependencies(resolvedSourceContent, destinationContent);
updatedContent = this.mergeFunctions(resolvedSourceContent, updatedContent);
this.updateDestination(destinationPath, updatedContent);
return updatedContent;
}
syncTemplateIntoTypeScriptController(sourceContent, destinationContent) {
const sourceSignature = this._extractDefineSignature(sourceContent);
const sourceFunctions = this._extractFunctionProperties(sourceContent);
const classRange = this._findTypeScriptControllerClassRange(destinationContent);
if (!classRange) {
throw new Error(
"TypeScript controller insertion requires a class-based controller that extends Controller."
);
}
let nextContent = this._mergeTypeScriptImports(destinationContent, sourceSignature);
nextContent = this._mergeTypeScriptClassMembers(nextContent, sourceFunctions);
return this._ensureTypeScriptNoCheck(nextContent);
}
mergeDependencies(sourceContent, destinationContent) {
const sourceSignature = this._extractDefineSignature(sourceContent);
const destinationSignature = this._extractDefineSignature(destinationContent);
if (!sourceSignature || !destinationSignature) {
return destinationContent;
}
const mergedSignature = this._mergeDefineSignatures(destinationSignature, sourceSignature);
return this._replaceDefineSignature(destinationContent, mergedSignature.dependencies, mergedSignature.params);
}
mergeFunctions(sourceContent, destinationContent) {
const sourceFunctions = this._extractFunctionProperties(sourceContent);
const destinationFunctions = this._e