@sap-ux/create 1.4.18 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/cli/add/cds-plugin-ui.js +3 -1
- package/dist/cli/add/eslint-config.js +3 -1
- package/dist/cli/add/mockserver-config.js +5 -1
- package/dist/cli/convert/eslint-config.js +3 -1
- package/dist/cli/generate/adaptation-project.js +1 -1
- package/dist/cli/get/system.js +4 -20
- package/dist/cli/index.js +4 -1
- package/dist/cli/list/system.js +2 -28
- package/dist/cli/migrate/index.d.ts +8 -0
- package/dist/cli/migrate/index.js +618 -0
- package/dist/cli/migrate/traceability.d.ts +43 -0
- package/dist/cli/migrate/traceability.js +52 -0
- package/dist/cli/utils/system-store.d.ts +18 -0
- package/dist/cli/utils/system-store.js +38 -0
- package/dist/common/index.d.ts +1 -1
- package/dist/common/index.js +5 -3
- package/dist/translations/ux-create.i18n.json +60 -0
- package/package.json +22 -20
package/README.md
CHANGED
|
@@ -581,3 +581,18 @@ Options:
|
|
|
581
581
|
- `--client <string>` - SAP client number (optional).
|
|
582
582
|
- `--json` - Output as JSON, which is useful for automation and MCP integrations.
|
|
583
583
|
|
|
584
|
+
--------------------------------
|
|
585
|
+
|
|
586
|
+
## [`migrate`](#migrate)
|
|
587
|
+
|
|
588
|
+
Migrate legacy WebIDE Fiori project to modern Fiori tools format. In BAS, a destination is required. Outside BAS, hostname and client are required; destination is optional.
|
|
589
|
+
|
|
590
|
+
Options:
|
|
591
|
+
- `-d, --destination <name>` - SAP System destination name (required in BAS)
|
|
592
|
+
- `-s, --sap-system-name <name>` - SAP System name (alias for destination)
|
|
593
|
+
- `-H, --hostname <host>` - Backend hostname (required outside BAS unless resolved from a saved system)
|
|
594
|
+
- `-c, --client <client>` - SAP Client (required outside BAS unless resolved from destination or saved system)
|
|
595
|
+
- `-u, --ui5-version <version>` - UI5 version (defaults to source project version)
|
|
596
|
+
- `-f, --force` - Force migration even if project is already a Fiori tools project
|
|
597
|
+
- `-n, --skip-install` - Skip the `npm install` step after migration
|
|
598
|
+
|
|
@@ -48,7 +48,9 @@ async function addCdsPluginUi5(basePath, simulate, skipInstall) {
|
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
logger.debug('Running npm install command');
|
|
51
|
-
runNpmInstallCommand(basePath)
|
|
51
|
+
runNpmInstallCommand(basePath, [], { logger }).catch(() => {
|
|
52
|
+
// Error already logged by runNpmInstallCommand
|
|
53
|
+
});
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -48,7 +48,9 @@ async function addEslintConfig(basePath, simulate, config, skipInstall = false)
|
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
logger.info(`Executing \`npm install\`.`);
|
|
51
|
-
runNpmInstallCommand(basePath, undefined, { logger })
|
|
51
|
+
runNpmInstallCommand(basePath, undefined, { logger }).catch(() => {
|
|
52
|
+
// Error already logged by runNpmInstallCommand
|
|
53
|
+
});
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -62,7 +62,11 @@ async function addMockserverConfig(basePath, simulate, skipInstall, interactive)
|
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
64
|
logger.debug('Running npm install command');
|
|
65
|
-
runNpmInstallCommand(basePath, ['--save-dev', '@sap-ux/ui5-middleware-fe-mockserver']
|
|
65
|
+
runNpmInstallCommand(basePath, ['--save-dev', '@sap-ux/ui5-middleware-fe-mockserver'], {
|
|
66
|
+
logger
|
|
67
|
+
}).catch(() => {
|
|
68
|
+
// Error already logged by runNpmInstallCommand
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -60,7 +60,9 @@ async function convertEslintConfig(basePath, simulate, config, skipInstall = fal
|
|
|
60
60
|
});
|
|
61
61
|
logger.info('npm uninstall completed successfully.');
|
|
62
62
|
logger.info(`Executing \`npm install\`.`);
|
|
63
|
-
runNpmInstallCommand(basePath, undefined, { logger })
|
|
63
|
+
runNpmInstallCommand(basePath, undefined, { logger }).catch(() => {
|
|
64
|
+
// Error already logged by runNpmInstallCommand
|
|
65
|
+
});
|
|
64
66
|
}
|
|
65
67
|
catch (error) {
|
|
66
68
|
logger.error(`npm command failed. '${error.message}'`);
|
|
@@ -57,7 +57,7 @@ async function generateAdaptationProject(basePath, defaults, useDefaults, simula
|
|
|
57
57
|
if (!simulate) {
|
|
58
58
|
await new Promise((resolve) => fs.commit(resolve));
|
|
59
59
|
if (!skipInstall) {
|
|
60
|
-
runNpmInstallCommand(basePath);
|
|
60
|
+
await runNpmInstallCommand(basePath);
|
|
61
61
|
logger.info('Executed npm install');
|
|
62
62
|
}
|
|
63
63
|
}
|
package/dist/cli/get/system.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isAppStudio } from '@sap-ux/btp-utils';
|
|
2
|
-
import {
|
|
3
|
-
import { NullTransport, ToolsLogger } from '@sap-ux/logger';
|
|
2
|
+
import { BackendSystemKey } from '@sap-ux/store';
|
|
4
3
|
import { getLogger } from '../../tracing/index.js';
|
|
5
4
|
import { findSystemByUrl } from '../utils/system-lookup.js';
|
|
5
|
+
import { getSystemService, toPublicView } from '../utils/system-store.js';
|
|
6
6
|
/**
|
|
7
7
|
* Add the "get system" subcommand to a passed command.
|
|
8
8
|
* Retrieves details of a saved backend system identified by its URL and optional client.
|
|
@@ -38,14 +38,7 @@ async function getSystem(url, client, asJson) {
|
|
|
38
38
|
logger.error('System management using the CLI is not supported in SAP Business Application Studio. Use the built-in system management instead.');
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
// @sap-ux/store diagnostic messages do not appear on stdout before the
|
|
43
|
-
// JSON payload and break piped consumers.
|
|
44
|
-
const storeLogger = asJson ? new ToolsLogger({ transports: [new NullTransport()] }) : logger;
|
|
45
|
-
const service = await getService({
|
|
46
|
-
entityName: 'system',
|
|
47
|
-
logger: storeLogger
|
|
48
|
-
});
|
|
41
|
+
const service = await getSystemService(asJson);
|
|
49
42
|
// Use smart lookup to handle client mismatch scenarios
|
|
50
43
|
const system = await findSystemByUrl(url, client, service);
|
|
51
44
|
if (!system) {
|
|
@@ -53,16 +46,7 @@ async function getSystem(url, client, asJson) {
|
|
|
53
46
|
logger.error(`System not found: ${key.getId()}`);
|
|
54
47
|
return;
|
|
55
48
|
}
|
|
56
|
-
const publicView =
|
|
57
|
-
name: system.name,
|
|
58
|
-
url: system.url,
|
|
59
|
-
client: system.client,
|
|
60
|
-
systemType: system.systemType,
|
|
61
|
-
authenticationType: system.authenticationType,
|
|
62
|
-
connectionType: system.connectionType,
|
|
63
|
-
userDisplayName: system.userDisplayName,
|
|
64
|
-
hasSensitiveData: system.hasSensitiveData
|
|
65
|
-
};
|
|
49
|
+
const publicView = toPublicView(system);
|
|
66
50
|
if (asJson) {
|
|
67
51
|
// Use process.stdout.write so the output is pure JSON with no logger
|
|
68
52
|
// prefixes or timestamps — required for machine consumption and piping.
|
package/dist/cli/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getConvertCommands } from './convert/index.js';
|
|
|
13
13
|
import { getListCommands } from './list/index.js';
|
|
14
14
|
import { getGetCommands } from './get/index.js';
|
|
15
15
|
import { getUpdateCommands } from './update/index.js';
|
|
16
|
+
import { getMigrateCommand } from './migrate/index.js';
|
|
16
17
|
/*
|
|
17
18
|
* We've chosen 'commander' over 'minimist' and 'yargs' for this CLI implementation. Reasons:
|
|
18
19
|
* (if it still up: https://npmtrends.com/commander-vs-minimist-vs-yargs)
|
|
@@ -34,7 +35,7 @@ export async function handleCreateFioriCommand(argv) {
|
|
|
34
35
|
// Initialize i18n before executing commands
|
|
35
36
|
await initI18n();
|
|
36
37
|
const program = getCommanderProgram();
|
|
37
|
-
program.
|
|
38
|
+
await program.parseAsync(argv);
|
|
38
39
|
}
|
|
39
40
|
catch (error) {
|
|
40
41
|
// Commander report meaningful error messages, output only if log level is set to debug
|
|
@@ -116,6 +117,8 @@ function getCommanderProgram() {
|
|
|
116
117
|
Usage: \`npx --yes @sap-ux/create@latest get [subcommand] [options]\`
|
|
117
118
|
The available subcommands are: ${getFeatureSummary(getCommands.commands)}\n`);
|
|
118
119
|
program.addCommand(getCommands);
|
|
120
|
+
// Handler for create-fiori migrate ..
|
|
121
|
+
program.addCommand(getMigrateCommand());
|
|
119
122
|
return program;
|
|
120
123
|
}
|
|
121
124
|
/**
|
package/dist/cli/list/system.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isAppStudio } from '@sap-ux/btp-utils';
|
|
2
|
-
import { getService } from '@sap-ux/store';
|
|
3
|
-
import { NullTransport, ToolsLogger } from '@sap-ux/logger';
|
|
4
2
|
import { getLogger } from '../../tracing/index.js';
|
|
3
|
+
import { getSystemService, toPublicView } from '../utils/system-store.js';
|
|
5
4
|
/**
|
|
6
5
|
* Add the "list system" subcommand to a passed command.
|
|
7
6
|
* Lists all backend systems saved in the store (~/.fioritools).
|
|
@@ -33,14 +32,7 @@ async function listSystems(asJson) {
|
|
|
33
32
|
logger.error('System management using the CLI is not supported in SAP Business Application Studio. Use the built-in system management instead.');
|
|
34
33
|
return;
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
// @sap-ux/store diagnostic messages (e.g. "Using KeyStoreManager…") do
|
|
38
|
-
// not appear on stdout before the JSON payload and break piped consumers.
|
|
39
|
-
const storeLogger = asJson ? new ToolsLogger({ transports: [new NullTransport()] }) : logger;
|
|
40
|
-
const service = await getService({
|
|
41
|
-
entityName: 'system',
|
|
42
|
-
logger: storeLogger
|
|
43
|
-
});
|
|
35
|
+
const service = await getSystemService(asJson);
|
|
44
36
|
// Pass no filter so all systems are returned regardless of connection type
|
|
45
37
|
const systems = await service.getAll();
|
|
46
38
|
if (asJson) {
|
|
@@ -65,24 +57,6 @@ async function listSystems(asJson) {
|
|
|
65
57
|
logger.debug(error);
|
|
66
58
|
}
|
|
67
59
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Returns a view of a system with only non-sensitive fields.
|
|
70
|
-
*
|
|
71
|
-
* @param system - the backend system
|
|
72
|
-
* @returns non-sensitive system data
|
|
73
|
-
*/
|
|
74
|
-
function toPublicView(system) {
|
|
75
|
-
return {
|
|
76
|
-
name: system.name,
|
|
77
|
-
url: system.url,
|
|
78
|
-
client: system.client,
|
|
79
|
-
systemType: system.systemType,
|
|
80
|
-
authenticationType: system.authenticationType,
|
|
81
|
-
connectionType: system.connectionType,
|
|
82
|
-
userDisplayName: system.userDisplayName,
|
|
83
|
-
hasSensitiveData: system.hasSensitiveData
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
60
|
/**
|
|
87
61
|
* Prints a single system to the logger in human-readable format.
|
|
88
62
|
*
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve, basename } from 'node:path';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import { ProjectAccess, ProjectMigrator, initI18n as initMigrationI18n } from '@sap-ux/fiori-migration-writer';
|
|
6
|
+
import { DestinationProxyType, isAppStudio, listDestinations } from '@sap-ux/btp-utils';
|
|
7
|
+
import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle';
|
|
8
|
+
import { getService } from '@sap-ux/store';
|
|
9
|
+
import { execNpmCommand } from '@sap-ux/project-access';
|
|
10
|
+
import { validateClient as validateClientInput, validateSpecialChars as validateSpecialCharsInput } from '@sap-ux/project-input-validator';
|
|
11
|
+
import { getLogger } from '../../tracing/index.js';
|
|
12
|
+
import { findSystemByUrl } from '../utils/system-lookup.js';
|
|
13
|
+
import { t } from '../../i18n.js';
|
|
14
|
+
import { writeMigrationLog, getMigrationLogsDirectory } from './traceability.js';
|
|
15
|
+
/**
|
|
16
|
+
* Helper to convert validator result (boolean | string) to throw-on-error format.
|
|
17
|
+
* Project input validators return true or error message; this converts to exception.
|
|
18
|
+
*
|
|
19
|
+
* @param validatorResult - result from @sap-ux/project-input-validator function
|
|
20
|
+
* @param value - the validated value to return on success
|
|
21
|
+
* @returns validated value
|
|
22
|
+
* @throws Error if validation failed
|
|
23
|
+
*/
|
|
24
|
+
function enforceValidation(validatorResult, value) {
|
|
25
|
+
if (validatorResult === true) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
// If not true, must be false or an error message string
|
|
29
|
+
throw new Error(typeof validatorResult === 'string' ? validatorResult : 'Validation failed');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate a path to prevent directory traversal attacks.
|
|
33
|
+
*
|
|
34
|
+
* @param path - path to validate
|
|
35
|
+
* @returns validated absolute path
|
|
36
|
+
* @throws Error if path contains unsafe characters or doesn't exist
|
|
37
|
+
*/
|
|
38
|
+
function validatePath(path) {
|
|
39
|
+
const resolved = resolve(path);
|
|
40
|
+
// Reject control characters and shell metacharacters
|
|
41
|
+
if (/[\0\r\n`$|&;<>]/.test(resolved)) {
|
|
42
|
+
throw new Error(t('migrate.validation.pathUnsafe'));
|
|
43
|
+
}
|
|
44
|
+
// Ensure it's an existing directory
|
|
45
|
+
if (!existsSync(resolved)) {
|
|
46
|
+
throw new Error(t('migrate.validation.pathNotExists', { path }));
|
|
47
|
+
}
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a destination/system name to prevent injection attacks.
|
|
52
|
+
* Uses @sap-ux/project-input-validator for consistent validation.
|
|
53
|
+
*
|
|
54
|
+
* @param destination - destination name to validate
|
|
55
|
+
* @returns validated destination
|
|
56
|
+
* @throws Error if destination contains invalid characters
|
|
57
|
+
*/
|
|
58
|
+
function validateDestination(destination) {
|
|
59
|
+
// Allow alphanumeric, underscores, hyphens (typical SAP destination/system names)
|
|
60
|
+
return enforceValidation(validateSpecialCharsInput(destination, '^[a-zA-Z0-9_-]+$'), destination);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate a hostname to prevent URL injection.
|
|
64
|
+
*
|
|
65
|
+
* @param hostname - hostname to validate
|
|
66
|
+
* @returns validated hostname
|
|
67
|
+
* @throws Error if hostname contains unsafe characters
|
|
68
|
+
*/
|
|
69
|
+
function validateHostname(hostname) {
|
|
70
|
+
const strippedHostname = hostname.replace(/^https?:\/\//i, '');
|
|
71
|
+
// RFC-compliant hostname with an optional port.
|
|
72
|
+
// Simplified: alphanumeric + hyphens, no leading/trailing hyphens, optional port
|
|
73
|
+
if (!/^[a-zA-Z\d]([a-zA-Z\d-]*[a-zA-Z\d])?(\.[a-zA-Z\d]([a-zA-Z\d-]*[a-zA-Z\d])?)*(:\d+)?$/.test(strippedHostname)) {
|
|
74
|
+
throw new Error(t('migrate.validation.hostnameInvalid'));
|
|
75
|
+
}
|
|
76
|
+
return strippedHostname;
|
|
77
|
+
}
|
|
78
|
+
function getHostnameFromUrl(url) {
|
|
79
|
+
try {
|
|
80
|
+
return new URL(url).host;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate a SAP client number.
|
|
88
|
+
* Uses @sap-ux/project-input-validator for consistent validation.
|
|
89
|
+
*
|
|
90
|
+
* @param client - client number to validate
|
|
91
|
+
* @returns validated client
|
|
92
|
+
* @throws Error if client is not a 3-digit number
|
|
93
|
+
*/
|
|
94
|
+
function validateClient(client) {
|
|
95
|
+
// SAP clients are 3-digit numbers (000-999)
|
|
96
|
+
return enforceValidation(validateClientInput(client), client);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate a UI5 version string to prevent URL injection.
|
|
100
|
+
*
|
|
101
|
+
* @param version - version string to validate
|
|
102
|
+
* @returns validated version
|
|
103
|
+
* @throws Error if version contains unsafe characters
|
|
104
|
+
*/
|
|
105
|
+
function validateUI5Version(version) {
|
|
106
|
+
// Allow semantic version format: digits, dots, optional snapshot suffix (case-insensitive)
|
|
107
|
+
if (!/^\d+\.\d+\.\d+(-snapshot)?$/i.test(version)) {
|
|
108
|
+
throw new Error(t('migrate.validation.ui5VersionInvalid'));
|
|
109
|
+
}
|
|
110
|
+
return version;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Helper to create a required text validation function.
|
|
114
|
+
*
|
|
115
|
+
* @param fieldName - name of the field for error message
|
|
116
|
+
* @returns validation function
|
|
117
|
+
*/
|
|
118
|
+
function createRequiredValidator(fieldName) {
|
|
119
|
+
return (value) => (value ? true : t('migrate.validation.fieldRequired', { fieldName }));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Helper to prompt for a required text input.
|
|
123
|
+
*
|
|
124
|
+
* @param name - prompt name
|
|
125
|
+
* @param message - prompt message
|
|
126
|
+
* @param fieldName - field name for validation error
|
|
127
|
+
* @returns user input
|
|
128
|
+
*/
|
|
129
|
+
async function promptRequiredText(name, message, fieldName) {
|
|
130
|
+
const response = await prompts({
|
|
131
|
+
type: 'text',
|
|
132
|
+
name,
|
|
133
|
+
message,
|
|
134
|
+
validate: createRequiredValidator(fieldName)
|
|
135
|
+
});
|
|
136
|
+
if (response[name] === undefined) {
|
|
137
|
+
throw new Error(t('migrate.validation.operationCancelled'));
|
|
138
|
+
}
|
|
139
|
+
return response[name];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Helper to prompt for confirmation.
|
|
143
|
+
*
|
|
144
|
+
* @param name - prompt name
|
|
145
|
+
* @param message - prompt message
|
|
146
|
+
* @param initial - initial value
|
|
147
|
+
* @returns confirmation response
|
|
148
|
+
*/
|
|
149
|
+
async function promptConfirm(name, message, initial) {
|
|
150
|
+
const response = await prompts({ type: 'confirm', name, message, initial });
|
|
151
|
+
if (response[name] === undefined) {
|
|
152
|
+
throw new Error(t('migrate.validation.operationCancelled'));
|
|
153
|
+
}
|
|
154
|
+
return response[name];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Returns the 'migrate' command for the commander program.
|
|
158
|
+
*
|
|
159
|
+
* @returns - commander command
|
|
160
|
+
*/
|
|
161
|
+
export function getMigrateCommand() {
|
|
162
|
+
return new Command('migrate')
|
|
163
|
+
.argument('[project-path]')
|
|
164
|
+
.description(t('migrate.command.description'))
|
|
165
|
+
.option('-d, --destination <name>', t('migrate.command.options.destination'))
|
|
166
|
+
.option('-s, --sap-system-name <name>', t('migrate.command.options.sapSystemName'))
|
|
167
|
+
.option('-H, --hostname <host>', t('migrate.command.options.hostname'))
|
|
168
|
+
.option('-c, --client <client>', t('migrate.command.options.client'))
|
|
169
|
+
.option('-u, --ui5-version <version>', t('migrate.command.options.ui5Version'))
|
|
170
|
+
.option('-f, --force', t('migrate.command.options.force'))
|
|
171
|
+
.option('-n, --skip-install', t('migrate.command.options.skipInstall'))
|
|
172
|
+
.action(async (projectPath, options) => {
|
|
173
|
+
try {
|
|
174
|
+
await migrate(projectPath, options);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const logger = getLogger();
|
|
178
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get project path from user or use provided path.
|
|
185
|
+
*
|
|
186
|
+
* @param projectPath - optional project path from command line
|
|
187
|
+
* @returns resolved project path
|
|
188
|
+
*/
|
|
189
|
+
async function getProjectPath(projectPath) {
|
|
190
|
+
let resolvedPath = projectPath ? validatePath(projectPath) : process.cwd();
|
|
191
|
+
if (!projectPath) {
|
|
192
|
+
const confirmPath = await promptConfirm('confirmPath', t('migrate.prompts.confirmCurrentDir', { path: resolvedPath }), true);
|
|
193
|
+
if (!confirmPath) {
|
|
194
|
+
const customPath = await promptRequiredText('customPath', t('migrate.prompts.enterPath'), t('migrate.prompts.enterPathField'));
|
|
195
|
+
resolvedPath = validatePath(customPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return resolvedPath;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if project needs force flag for migration.
|
|
202
|
+
* Uses package.json devDependencies to detect Fiori tools projects.
|
|
203
|
+
*
|
|
204
|
+
* @param resolvedPath - project path
|
|
205
|
+
* @param force - force flag from options
|
|
206
|
+
* @returns true if migration should proceed, false otherwise
|
|
207
|
+
*/
|
|
208
|
+
async function checkForceRequired(resolvedPath, force) {
|
|
209
|
+
const logger = getLogger();
|
|
210
|
+
// Check for Fiori tools indicators in package.json
|
|
211
|
+
let isToolsProject = false;
|
|
212
|
+
try {
|
|
213
|
+
const packageJsonPath = resolve(resolvedPath, 'package.json');
|
|
214
|
+
if (existsSync(packageJsonPath)) {
|
|
215
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
216
|
+
const devDeps = packageJson.devDependencies || {};
|
|
217
|
+
// Fiori tools projects have @sap/ux-* dev dependencies
|
|
218
|
+
// Note: @ui5/* packages like @ui5/cli are generic build tools, not Fiori-specific
|
|
219
|
+
isToolsProject = Object.keys(devDeps).some((dep) => dep.startsWith('@sap/ux-'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// If we can't read package.json, assume not a tools project
|
|
224
|
+
}
|
|
225
|
+
if (isToolsProject && !force) {
|
|
226
|
+
logger.warn(t('migrate.prompts.alreadyMigrated'));
|
|
227
|
+
const confirmForce = await promptConfirm('confirmForce', t('migrate.prompts.forceConfirm'), false);
|
|
228
|
+
if (!confirmForce) {
|
|
229
|
+
logger.info(t('migrate.prompts.cancelled'));
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get destination or hostname from user or options.
|
|
237
|
+
*
|
|
238
|
+
* @param options - command options
|
|
239
|
+
* @returns object with destination and hostname
|
|
240
|
+
*/
|
|
241
|
+
async function getDestinationOrHostname(options) {
|
|
242
|
+
const appStudio = isAppStudio();
|
|
243
|
+
let destination = options.destination ?? options.sapSystemName;
|
|
244
|
+
let hostname = options.hostname ? validateHostname(options.hostname) : undefined;
|
|
245
|
+
// Validate destination if provided via CLI
|
|
246
|
+
if (destination) {
|
|
247
|
+
destination = validateDestination(destination);
|
|
248
|
+
}
|
|
249
|
+
if (appStudio) {
|
|
250
|
+
if (!destination) {
|
|
251
|
+
destination = validateDestination(await promptRequiredText('dest', t('migrate.prompts.enterBASDestination'), t('migrate.prompts.destinationField')));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (!hostname && !destination) {
|
|
255
|
+
const useDestination = await promptConfirm('useDestination', t('migrate.prompts.useDestination'), true);
|
|
256
|
+
if (useDestination) {
|
|
257
|
+
const dest = await promptRequiredText('dest', t('migrate.prompts.enterBTPDestination'), t('migrate.prompts.destinationField'));
|
|
258
|
+
destination = validateDestination(dest);
|
|
259
|
+
}
|
|
260
|
+
if (!destination) {
|
|
261
|
+
const host = await promptRequiredText('host', t('migrate.prompts.enterHostname'), t('migrate.prompts.hostnameField'));
|
|
262
|
+
hostname = validateHostname(host);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (!hostname && destination) {
|
|
266
|
+
// Outside BAS with destination but no hostname - prompt for hostname
|
|
267
|
+
const host = await promptRequiredText('host', t('migrate.prompts.enterHostname'), t('migrate.prompts.hostnameField'));
|
|
268
|
+
hostname = validateHostname(host);
|
|
269
|
+
}
|
|
270
|
+
return { destination, hostname };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get optional client parameter.
|
|
274
|
+
*
|
|
275
|
+
* @param optionClient - client from command options
|
|
276
|
+
* @returns client value or undefined
|
|
277
|
+
*/
|
|
278
|
+
async function getClient(optionClient) {
|
|
279
|
+
if (optionClient) {
|
|
280
|
+
return validateClient(optionClient);
|
|
281
|
+
}
|
|
282
|
+
if (!isAppStudio()) {
|
|
283
|
+
return validateClient(await promptRequiredText('clientValue', t('migrate.prompts.enterClient'), t('migrate.prompts.clientField')));
|
|
284
|
+
}
|
|
285
|
+
const response = await prompts({
|
|
286
|
+
type: 'text',
|
|
287
|
+
name: 'clientValue',
|
|
288
|
+
message: t('migrate.prompts.enterClientOptional'),
|
|
289
|
+
initial: ''
|
|
290
|
+
});
|
|
291
|
+
const client = response.clientValue;
|
|
292
|
+
return client ? validateClient(client) : undefined;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get UI5 version parameter.
|
|
296
|
+
*
|
|
297
|
+
* @param optionVersion - UI5 version from command options
|
|
298
|
+
* @returns UI5 version or undefined
|
|
299
|
+
*/
|
|
300
|
+
async function getUI5Version(optionVersion) {
|
|
301
|
+
if (optionVersion) {
|
|
302
|
+
return validateUI5Version(optionVersion);
|
|
303
|
+
}
|
|
304
|
+
// Non-interactive: use project default
|
|
305
|
+
if (!process.stdin.isTTY) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
const response = await prompts({
|
|
309
|
+
type: 'text',
|
|
310
|
+
name: 'version',
|
|
311
|
+
message: t('migrate.prompts.enterUI5Version'),
|
|
312
|
+
initial: ''
|
|
313
|
+
});
|
|
314
|
+
const version = response.version;
|
|
315
|
+
return version ? validateUI5Version(version) : undefined;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Resolve a legacy project's backend to a saved system or BAS destination.
|
|
319
|
+
*
|
|
320
|
+
* @param projectPath - legacy project root
|
|
321
|
+
* @returns matched system, if available
|
|
322
|
+
*/
|
|
323
|
+
async function findMigrationSystem(projectPath) {
|
|
324
|
+
const logger = getLogger();
|
|
325
|
+
try {
|
|
326
|
+
const { projectInfo } = await ProjectAccess.getProjectInfo(projectPath);
|
|
327
|
+
if (isAppStudio()) {
|
|
328
|
+
return await findBASDestination(projectInfo.destination, projectInfo.hostname);
|
|
329
|
+
}
|
|
330
|
+
if (!projectInfo.hostname) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const service = await getService({ entityName: 'system', logger });
|
|
334
|
+
const savedSystem = await findSystemByUrl(projectInfo.hostname, projectInfo.sapClient || undefined, service);
|
|
335
|
+
return savedSystem ? { ...savedSystem, scp: false } : undefined;
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
logger.debug(`Unable to resolve a migration system: ${error.message}`);
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Match the legacy project backend to a BAS destination.
|
|
344
|
+
*
|
|
345
|
+
* @param destinationName - destination parsed from the legacy project
|
|
346
|
+
* @param hostname - backend hostname parsed from the legacy project
|
|
347
|
+
* @returns matching BAS destination, if available
|
|
348
|
+
*/
|
|
349
|
+
async function findBASDestination(destinationName, hostname) {
|
|
350
|
+
const destinations = await listDestinations();
|
|
351
|
+
const systems = Object.values(destinations).map((destination) => toMigrationSystem(destination));
|
|
352
|
+
const destinationMatch = destinationName ? systems.find((system) => system.name === destinationName) : undefined;
|
|
353
|
+
if (destinationMatch) {
|
|
354
|
+
return destinationMatch;
|
|
355
|
+
}
|
|
356
|
+
if (!hostname) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const normalizedHostname = /^https?:\/\//i.test(hostname) ? hostname : `https://${hostname}`;
|
|
361
|
+
const sourceOrigin = new URL(normalizedHostname).origin;
|
|
362
|
+
const matches = systems.filter((system) => new URL(system.url).origin === sourceOrigin);
|
|
363
|
+
if (matches.length === 1) {
|
|
364
|
+
return matches[0];
|
|
365
|
+
}
|
|
366
|
+
if (matches.length > 1) {
|
|
367
|
+
const answer = await prompts({
|
|
368
|
+
type: 'select',
|
|
369
|
+
name: 'system',
|
|
370
|
+
message: t('migrate.prompts.selectBASDestination'),
|
|
371
|
+
choices: matches.map((system) => ({
|
|
372
|
+
title: t('migrate.prompts.destinationChoice', {
|
|
373
|
+
name: system.name,
|
|
374
|
+
client: system.client || t('migrate.prompts.defaultClient')
|
|
375
|
+
}),
|
|
376
|
+
value: system
|
|
377
|
+
}))
|
|
378
|
+
});
|
|
379
|
+
if (answer.system === undefined) {
|
|
380
|
+
throw new Error(t('migrate.validation.operationCancelled'));
|
|
381
|
+
}
|
|
382
|
+
return answer.system;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Re-throw cancellation errors so the user can abort
|
|
387
|
+
if (error.message === t('migrate.validation.operationCancelled')) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
// Invalid source or destination URLs cannot be matched safely.
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
function toMigrationSystem(destination) {
|
|
395
|
+
return {
|
|
396
|
+
name: destination.Name,
|
|
397
|
+
url: destination.Host,
|
|
398
|
+
client: destination['sap-client'] || undefined,
|
|
399
|
+
scp: destination.ProxyType !== DestinationProxyType.ON_PREMISE
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Execute npm install and capture errors, warnings, and security issues.
|
|
404
|
+
*
|
|
405
|
+
* @param resolvedPath - project path
|
|
406
|
+
* @param logger - logger instance
|
|
407
|
+
* @returns npm install result with captured messages
|
|
408
|
+
*/
|
|
409
|
+
async function executeNpmInstall(resolvedPath, logger) {
|
|
410
|
+
const npmErrors = [];
|
|
411
|
+
const npmSecurityWarnings = [];
|
|
412
|
+
try {
|
|
413
|
+
await execNpmCommand(['install'], {
|
|
414
|
+
cwd: resolvedPath,
|
|
415
|
+
throwOnError: true,
|
|
416
|
+
logger: {
|
|
417
|
+
info: (msg) => {
|
|
418
|
+
const message = typeof msg === 'string' ? msg : JSON.stringify(msg);
|
|
419
|
+
// Capture security vulnerabilities
|
|
420
|
+
if (message.toLowerCase().includes('vulnerabilities') ||
|
|
421
|
+
message.toLowerCase().includes('security')) {
|
|
422
|
+
npmSecurityWarnings.push(message);
|
|
423
|
+
}
|
|
424
|
+
logger.info(msg);
|
|
425
|
+
},
|
|
426
|
+
error: (msg) => {
|
|
427
|
+
const message = typeof msg === 'string' ? msg : JSON.stringify(msg);
|
|
428
|
+
npmErrors.push(message);
|
|
429
|
+
logger.error(msg);
|
|
430
|
+
},
|
|
431
|
+
debug: (msg) => {
|
|
432
|
+
logger.debug(msg);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
logger.info(t('npm.installSuccess'));
|
|
437
|
+
return {
|
|
438
|
+
success: true,
|
|
439
|
+
...(npmSecurityWarnings.length > 0 && { securityWarnings: npmSecurityWarnings })
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
const installError = error;
|
|
444
|
+
npmErrors.push(installError.message);
|
|
445
|
+
logger.error(t('migrate.status.installFailed'));
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
errors: npmErrors,
|
|
449
|
+
...(npmSecurityWarnings.length > 0 && { securityWarnings: npmSecurityWarnings })
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Resolve migration options with fallbacks from matched system.
|
|
455
|
+
*
|
|
456
|
+
* @param options
|
|
457
|
+
* @param matchedSystem
|
|
458
|
+
*/
|
|
459
|
+
function resolveMigrationOptions(options, matchedSystem) {
|
|
460
|
+
return {
|
|
461
|
+
...options,
|
|
462
|
+
destination: options.destination ?? options.sapSystemName ?? matchedSystem?.name,
|
|
463
|
+
hostname: options.hostname ?? (matchedSystem?.url ? getHostnameFromUrl(matchedSystem.url) : undefined),
|
|
464
|
+
client: options.client ??
|
|
465
|
+
matchedSystem?.client ??
|
|
466
|
+
(ProjectAccess.getClientFromDestinationName(options.destination ?? options.sapSystemName ?? matchedSystem?.name ?? '') ||
|
|
467
|
+
undefined)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Calculate base URI for migration.
|
|
472
|
+
* Priority: hostname > matchedSystem.url (if no hostname override) > destination (BAS only).
|
|
473
|
+
*
|
|
474
|
+
* @param destination
|
|
475
|
+
* @param hostname
|
|
476
|
+
* @param matchedSystem
|
|
477
|
+
* @param hostnameOverride
|
|
478
|
+
*/
|
|
479
|
+
function calculateBaseUri(destination, hostname, matchedSystem, hostnameOverride) {
|
|
480
|
+
// Start with destination-based URI: BAS routes by destination name via the proxy;
|
|
481
|
+
// outside BAS only a full hostname URL is valid.
|
|
482
|
+
let baseUri = isAppStudio() && destination ? `/${destination}` : '';
|
|
483
|
+
// Override with matched system URL if present and no hostname override
|
|
484
|
+
if (matchedSystem?.url && !hostnameOverride) {
|
|
485
|
+
baseUri = matchedSystem.url;
|
|
486
|
+
}
|
|
487
|
+
// Override with hostname if provided (takes precedence)
|
|
488
|
+
else if (hostname) {
|
|
489
|
+
baseUri = `https://${hostname}`;
|
|
490
|
+
}
|
|
491
|
+
return baseUri;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Log migration messages by type.
|
|
495
|
+
*
|
|
496
|
+
* @param messages
|
|
497
|
+
* @param logger
|
|
498
|
+
*/
|
|
499
|
+
function logMigrationMessages(messages, logger) {
|
|
500
|
+
logger.info(t('migrate.status.messages'));
|
|
501
|
+
messages.forEach((msg) => {
|
|
502
|
+
const logMessage = ` ${msg.type}: ${msg.description}`;
|
|
503
|
+
if (msg.type === 'ERROR') {
|
|
504
|
+
logger.error(logMessage);
|
|
505
|
+
}
|
|
506
|
+
else if (msg.type === 'WARNING') {
|
|
507
|
+
logger.warn(logMessage);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
logger.info(logMessage);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Handle successful migration - install dependencies and write log.
|
|
516
|
+
*
|
|
517
|
+
* @param result
|
|
518
|
+
* @param options
|
|
519
|
+
* @param resolvedPath
|
|
520
|
+
* @param migrationLogEntry
|
|
521
|
+
* @param logger
|
|
522
|
+
*/
|
|
523
|
+
async function handleMigrationSuccess(result, options, resolvedPath, migrationLogEntry, logger) {
|
|
524
|
+
logger.info(t('migrate.status.success'));
|
|
525
|
+
if (result.messages?.length) {
|
|
526
|
+
logMigrationMessages(result.messages, logger);
|
|
527
|
+
}
|
|
528
|
+
if (options.skipInstall) {
|
|
529
|
+
logger.warn(t('migrate.status.skipInstallWarning'));
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
logger.info(t('migrate.status.installing'));
|
|
533
|
+
migrationLogEntry.npmInstall = await executeNpmInstall(resolvedPath, logger);
|
|
534
|
+
}
|
|
535
|
+
writeMigrationLog(migrationLogEntry, logger);
|
|
536
|
+
logger.info(`Migration log location: ${getMigrationLogsDirectory()}`);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Handle failed migration - write log and throw error.
|
|
540
|
+
*
|
|
541
|
+
* @param result
|
|
542
|
+
* @param migrationLogEntry
|
|
543
|
+
* @param logger
|
|
544
|
+
*/
|
|
545
|
+
function handleMigrationFailure(result, migrationLogEntry, logger) {
|
|
546
|
+
logger.error(t('migrate.status.failed'));
|
|
547
|
+
if (result.messages?.length) {
|
|
548
|
+
result.messages.forEach((msg) => logger.error(` ${msg.description}`));
|
|
549
|
+
}
|
|
550
|
+
writeMigrationLog(migrationLogEntry, logger);
|
|
551
|
+
logger.info(`Migration log location: ${getMigrationLogsDirectory()}`);
|
|
552
|
+
throw new Error(t('migrate.status.failedError'));
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Execute the migration command.
|
|
556
|
+
*
|
|
557
|
+
* @param projectPath - path to the project to migrate
|
|
558
|
+
* @param options - command options
|
|
559
|
+
*/
|
|
560
|
+
async function migrate(projectPath, options) {
|
|
561
|
+
const logger = getLogger();
|
|
562
|
+
// 1. Get or prompt for project path
|
|
563
|
+
const resolvedPath = await getProjectPath(projectPath);
|
|
564
|
+
logger.info(t('migrate.status.migratingAt', { path: resolvedPath }));
|
|
565
|
+
// 2. Check if force flag is required
|
|
566
|
+
const shouldProceed = await checkForceRequired(resolvedPath, options.force ?? false);
|
|
567
|
+
if (!shouldProceed) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const matchedSystem = await findMigrationSystem(resolvedPath);
|
|
571
|
+
// 3. Resolve options with fallbacks
|
|
572
|
+
const resolvedOptions = resolveMigrationOptions(options, matchedSystem);
|
|
573
|
+
const { destination, hostname } = await getDestinationOrHostname(resolvedOptions);
|
|
574
|
+
// 4. Get optional client
|
|
575
|
+
const client = await getClient(resolvedOptions.client);
|
|
576
|
+
// 5. Get UI5 version
|
|
577
|
+
const ui5Version = await getUI5Version(options.ui5Version);
|
|
578
|
+
const sapClient = client ?? (destination ? ProjectAccess.getClientFromDestinationName(destination) || undefined : undefined);
|
|
579
|
+
// 6. Execute migration
|
|
580
|
+
logger.info(t('migrate.status.starting'));
|
|
581
|
+
const baseUri = calculateBaseUri(destination, hostname, matchedSystem, !!options.hostname);
|
|
582
|
+
const ui5SnapshotUrl = ui5Version ? `https://ui5.sap.com/${ui5Version}` : '';
|
|
583
|
+
const migrationHostname = hostname ?? (matchedSystem?.url ? getHostnameFromUrl(matchedSystem.url) : undefined);
|
|
584
|
+
// Initialize i18n for proper error messages
|
|
585
|
+
await initMigrationI18n();
|
|
586
|
+
const internalToggle = isInternalFeaturesSettingEnabled();
|
|
587
|
+
// Load project info first, then merge CLI overrides
|
|
588
|
+
const migrationProjectInfo = sapClient || destination || hostname
|
|
589
|
+
? {
|
|
590
|
+
...(sapClient && { sapClient }),
|
|
591
|
+
...(destination && { destination }),
|
|
592
|
+
...(migrationHostname && { hostname: migrationHostname }),
|
|
593
|
+
...(!options.destination && !options.sapSystemName && matchedSystem && { scp: matchedSystem.scp })
|
|
594
|
+
}
|
|
595
|
+
: undefined;
|
|
596
|
+
const result = await ProjectMigrator.migrate({
|
|
597
|
+
projectRoot: resolvedPath,
|
|
598
|
+
baseUri,
|
|
599
|
+
ui5SnapshotUrl,
|
|
600
|
+
importProjectInfo: migrationProjectInfo,
|
|
601
|
+
internalToggle: internalToggle || undefined
|
|
602
|
+
});
|
|
603
|
+
// Prepare migration log entry
|
|
604
|
+
const migrationLogEntry = {
|
|
605
|
+
timestamp: new Date().toISOString(),
|
|
606
|
+
projectPath: resolvedPath,
|
|
607
|
+
projectName: basename(resolvedPath),
|
|
608
|
+
migrationMessages: result.messages || [],
|
|
609
|
+
migrationSuccess: result.result
|
|
610
|
+
};
|
|
611
|
+
if (result.result) {
|
|
612
|
+
await handleMigrationSuccess(result, options, resolvedPath, migrationLogEntry, logger);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
handleMigrationFailure(result, migrationLogEntry, logger);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Logger } from '@sap-ux/logger';
|
|
2
|
+
export interface MigrationLogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
projectName: string;
|
|
6
|
+
migrationMessages: Array<{
|
|
7
|
+
type: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}>;
|
|
10
|
+
npmInstall?: {
|
|
11
|
+
success: boolean;
|
|
12
|
+
errors?: string[];
|
|
13
|
+
warnings?: string[];
|
|
14
|
+
securityWarnings?: string[];
|
|
15
|
+
};
|
|
16
|
+
migrationSuccess: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the migration logs directory.
|
|
20
|
+
*
|
|
21
|
+
* @returns path to migration logs directory
|
|
22
|
+
*/
|
|
23
|
+
export declare function getMigrationLogsDirectory(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the migration logs directory exists.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ensureMigrationLogsDirectory(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a unique log file name for a migration.
|
|
30
|
+
*
|
|
31
|
+
* @param projectName - name of the migrated project
|
|
32
|
+
* @param timestamp - ISO timestamp string
|
|
33
|
+
* @returns log file name
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateLogFileName(projectName: string, timestamp: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Write migration log to file.
|
|
38
|
+
*
|
|
39
|
+
* @param logEntry - migration log entry
|
|
40
|
+
* @param logger - optional logger for feedback
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeMigrationLog(logEntry: MigrationLogEntry, logger?: Logger): void;
|
|
43
|
+
//# sourceMappingURL=traceability.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { getFioriToolsDirectory } from '@sap-ux/store';
|
|
4
|
+
/**
|
|
5
|
+
* Get the migration logs directory.
|
|
6
|
+
*
|
|
7
|
+
* @returns path to migration logs directory
|
|
8
|
+
*/
|
|
9
|
+
export function getMigrationLogsDirectory() {
|
|
10
|
+
const fioriToolsDir = getFioriToolsDirectory();
|
|
11
|
+
return join(fioriToolsDir, 'migration-logs');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Ensure the migration logs directory exists.
|
|
15
|
+
*/
|
|
16
|
+
export function ensureMigrationLogsDirectory() {
|
|
17
|
+
const logsDir = getMigrationLogsDirectory();
|
|
18
|
+
if (!existsSync(logsDir)) {
|
|
19
|
+
mkdirSync(logsDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generate a unique log file name for a migration.
|
|
24
|
+
*
|
|
25
|
+
* @param projectName - name of the migrated project
|
|
26
|
+
* @param timestamp - ISO timestamp string
|
|
27
|
+
* @returns log file name
|
|
28
|
+
*/
|
|
29
|
+
export function generateLogFileName(projectName, timestamp) {
|
|
30
|
+
const cleanTimestamp = timestamp.replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
31
|
+
return `${projectName}_${cleanTimestamp}.json`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write migration log to file.
|
|
35
|
+
*
|
|
36
|
+
* @param logEntry - migration log entry
|
|
37
|
+
* @param logger - optional logger for feedback
|
|
38
|
+
*/
|
|
39
|
+
export function writeMigrationLog(logEntry, logger) {
|
|
40
|
+
try {
|
|
41
|
+
ensureMigrationLogsDirectory();
|
|
42
|
+
const logsDir = getMigrationLogsDirectory();
|
|
43
|
+
const logFileName = generateLogFileName(logEntry.projectName, logEntry.timestamp);
|
|
44
|
+
const logFilePath = join(logsDir, logFileName);
|
|
45
|
+
writeFileSync(logFilePath, JSON.stringify(logEntry, null, 2), 'utf-8');
|
|
46
|
+
logger?.info(`Migration log written to: ${logFilePath}`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger?.warn(`Failed to write migration log: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=traceability.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BackendSystem, BackendSystemKey } from '@sap-ux/store';
|
|
2
|
+
/**
|
|
3
|
+
* Create a store service for BackendSystem entities.
|
|
4
|
+
* When `silentForJson` is true, uses a NullTransport logger so that store
|
|
5
|
+
* diagnostic messages do not appear on stdout before a JSON payload.
|
|
6
|
+
*
|
|
7
|
+
* @param silentForJson - suppress store log output when true
|
|
8
|
+
* @returns configured store service
|
|
9
|
+
*/
|
|
10
|
+
export declare function getSystemService(silentForJson: boolean): Promise<import("@sap-ux/store").Service<BackendSystem, BackendSystemKey>>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a view of a BackendSystem with only non-sensitive fields.
|
|
13
|
+
*
|
|
14
|
+
* @param system - the backend system
|
|
15
|
+
* @returns non-sensitive system data
|
|
16
|
+
*/
|
|
17
|
+
export declare function toPublicView(system: BackendSystem): Record<string, unknown>;
|
|
18
|
+
//# sourceMappingURL=system-store.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getService } from '@sap-ux/store';
|
|
2
|
+
import { NullTransport, ToolsLogger } from '@sap-ux/logger';
|
|
3
|
+
import { getLogger } from '../../tracing/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a store service for BackendSystem entities.
|
|
6
|
+
* When `silentForJson` is true, uses a NullTransport logger so that store
|
|
7
|
+
* diagnostic messages do not appear on stdout before a JSON payload.
|
|
8
|
+
*
|
|
9
|
+
* @param silentForJson - suppress store log output when true
|
|
10
|
+
* @returns configured store service
|
|
11
|
+
*/
|
|
12
|
+
export function getSystemService(silentForJson) {
|
|
13
|
+
const logger = getLogger();
|
|
14
|
+
const storeLogger = silentForJson ? new ToolsLogger({ transports: [new NullTransport()] }) : logger;
|
|
15
|
+
return getService({
|
|
16
|
+
entityName: 'system',
|
|
17
|
+
logger: storeLogger
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns a view of a BackendSystem with only non-sensitive fields.
|
|
22
|
+
*
|
|
23
|
+
* @param system - the backend system
|
|
24
|
+
* @returns non-sensitive system data
|
|
25
|
+
*/
|
|
26
|
+
export function toPublicView(system) {
|
|
27
|
+
return {
|
|
28
|
+
name: system.name,
|
|
29
|
+
url: system.url,
|
|
30
|
+
client: system.client,
|
|
31
|
+
systemType: system.systemType,
|
|
32
|
+
authenticationType: system.authenticationType,
|
|
33
|
+
connectionType: system.connectionType,
|
|
34
|
+
userDisplayName: system.userDisplayName,
|
|
35
|
+
hasSensitiveData: system.hasSensitiveData
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=system-store.js.map
|
package/dist/common/index.d.ts
CHANGED
package/dist/common/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execNpmCommand } from '@sap-ux/project-access';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
2
3
|
export { promptYUIQuestions } from './prompts.js';
|
|
3
4
|
/**
|
|
4
5
|
* Run npm install command.
|
|
@@ -10,12 +11,13 @@ export { promptYUIQuestions } from './prompts.js';
|
|
|
10
11
|
*/
|
|
11
12
|
export function runNpmInstallCommand(basePath, installArgs = [], options) {
|
|
12
13
|
const logger = options?.logger;
|
|
13
|
-
execNpmCommand(['install', ...installArgs], { cwd: basePath, logger: logger })
|
|
14
|
+
return execNpmCommand(['install', ...installArgs], { cwd: basePath, logger: logger })
|
|
14
15
|
.then(() => {
|
|
15
|
-
logger?.info('npm
|
|
16
|
+
logger?.info(t('npm.installSuccess'));
|
|
16
17
|
})
|
|
17
18
|
.catch((error) => {
|
|
18
|
-
logger?.error(
|
|
19
|
+
logger?.error(t('npm.installFailed', { error: error.message }));
|
|
20
|
+
throw error;
|
|
19
21
|
});
|
|
20
22
|
}
|
|
21
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -64,5 +64,65 @@
|
|
|
64
64
|
"systemAdded": "System '{{name}}' added",
|
|
65
65
|
"systemUpdated": "System '{{name}}' updated",
|
|
66
66
|
"systemRemoved": "System '{{name}}' removed"
|
|
67
|
+
},
|
|
68
|
+
"migrate": {
|
|
69
|
+
"command": {
|
|
70
|
+
"description": "Migrate legacy WebIDE Fiori project to modern Fiori tools format. In BAS, a destination is required. Outside BAS, hostname and client are required; destination is optional.",
|
|
71
|
+
"options": {
|
|
72
|
+
"destination": "SAP System destination name (required in BAS)",
|
|
73
|
+
"sapSystemName": "SAP System name (alias for destination)",
|
|
74
|
+
"hostname": "Backend hostname (required outside BAS unless resolved from a saved system)",
|
|
75
|
+
"client": "SAP Client (required outside BAS unless resolved from destination or saved system)",
|
|
76
|
+
"ui5Version": "UI5 version (defaults to source project version)",
|
|
77
|
+
"force": "Force migration even if project is already a Fiori tools project",
|
|
78
|
+
"skipInstall": "Skip the `npm install` step after migration"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"validation": {
|
|
82
|
+
"pathUnsafe": "Path contains unsafe characters",
|
|
83
|
+
"pathNotExists": "Path does not exist: {{path}}",
|
|
84
|
+
"destinationInvalid": "Destination name must contain only letters, numbers, hyphens, and underscores",
|
|
85
|
+
"hostnameInvalid": "Invalid hostname format",
|
|
86
|
+
"clientInvalid": "SAP client must be a 3-digit number (e.g., 100)",
|
|
87
|
+
"ui5VersionInvalid": "UI5 version must follow semantic versioning format (e.g., 1.120.0)",
|
|
88
|
+
"fieldRequired": "{{fieldName}} is required",
|
|
89
|
+
"operationCancelled": "Operation cancelled by user"
|
|
90
|
+
},
|
|
91
|
+
"prompts": {
|
|
92
|
+
"confirmCurrentDir": "Migrate project at current directory: {{path}}?",
|
|
93
|
+
"enterPath": "Enter project path:",
|
|
94
|
+
"enterPathField": "Project path",
|
|
95
|
+
"alreadyMigrated": "Project appears to be already migrated to Fiori tools.",
|
|
96
|
+
"forceConfirm": "Force migration anyway?",
|
|
97
|
+
"cancelled": "Migration cancelled.",
|
|
98
|
+
"enterBASDestination": "Enter SAP Business Application Studio destination:",
|
|
99
|
+
"destinationField": "Destination",
|
|
100
|
+
"useDestination": "Use SAP System destination?",
|
|
101
|
+
"enterBTPDestination": "Enter BTP destination or SAP system name (letters, numbers, hyphens, underscores only):",
|
|
102
|
+
"enterHostname": "Enter hostname:",
|
|
103
|
+
"hostnameField": "Hostname",
|
|
104
|
+
"enterClient": "SAP Client:",
|
|
105
|
+
"clientField": "SAP Client",
|
|
106
|
+
"enterClientOptional": "SAP Client (optional, press Enter to skip):",
|
|
107
|
+
"enterUI5Version": "UI5 Version (optional, press Enter to use project default):",
|
|
108
|
+
"selectBASDestination": "Select the SAP Business Application Studio destination:",
|
|
109
|
+
"destinationChoice": "{{name}} ({{client}})",
|
|
110
|
+
"defaultClient": "default client"
|
|
111
|
+
},
|
|
112
|
+
"status": {
|
|
113
|
+
"migratingAt": "Migrating project at: {{path}}",
|
|
114
|
+
"starting": "Starting migration...",
|
|
115
|
+
"success": "✓ Migration completed successfully!",
|
|
116
|
+
"messages": "\nMessages:",
|
|
117
|
+
"skipInstallWarning": "`npm install` was skipped. Install project dependencies before running the application.",
|
|
118
|
+
"installing": "Installing project dependencies...",
|
|
119
|
+
"installFailed": "Migration completed, but dependency installation failed. Resolve the npm error above before running the project.",
|
|
120
|
+
"failed": "✗ Migration failed",
|
|
121
|
+
"failedError": "Migration failed"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"npm": {
|
|
125
|
+
"installSuccess": "npm install completed successfully.",
|
|
126
|
+
"installFailed": "npm install failed. '{{error}}'"
|
|
67
127
|
}
|
|
68
128
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap-ux/create",
|
|
3
3
|
"description": "SAP Fiori tools module to add or remove features",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/SAP/open-ux-tools.git",
|
|
@@ -35,26 +35,28 @@
|
|
|
35
35
|
"mem-fs": "2.1.0",
|
|
36
36
|
"mem-fs-editor": "9.4.0",
|
|
37
37
|
"prompts": "2.4.2",
|
|
38
|
-
"@sap-ux/abap-deploy-config-inquirer": "2.0.
|
|
39
|
-
"@sap-ux/abap-deploy-config-writer": "1.0.
|
|
40
|
-
"@sap-ux/adp-tooling": "1.0.
|
|
41
|
-
"@sap-ux/btp-utils": "2.0.
|
|
42
|
-
"@sap-ux/app-config-writer": "1.1.
|
|
43
|
-
"@sap-ux/cap-config-writer": "1.0.
|
|
44
|
-
"@sap-ux/inquirer-common": "1.0.
|
|
38
|
+
"@sap-ux/abap-deploy-config-inquirer": "2.0.53",
|
|
39
|
+
"@sap-ux/abap-deploy-config-writer": "1.0.32",
|
|
40
|
+
"@sap-ux/adp-tooling": "1.0.65",
|
|
41
|
+
"@sap-ux/btp-utils": "2.0.7",
|
|
42
|
+
"@sap-ux/app-config-writer": "1.1.28",
|
|
43
|
+
"@sap-ux/cap-config-writer": "1.0.44",
|
|
44
|
+
"@sap-ux/inquirer-common": "1.0.48",
|
|
45
45
|
"@sap-ux/logger": "1.0.3",
|
|
46
|
-
"@sap-ux/mockserver-config-writer": "1.0.
|
|
47
|
-
"@sap-ux/odata-service-writer": "1.0.
|
|
48
|
-
"@sap-ux/preview-middleware": "1.2.
|
|
49
|
-
"@sap-ux/project-access": "2.3.
|
|
50
|
-
"@sap-ux/project-input-validator": "1.0.
|
|
46
|
+
"@sap-ux/mockserver-config-writer": "1.0.23",
|
|
47
|
+
"@sap-ux/odata-service-writer": "1.0.26",
|
|
48
|
+
"@sap-ux/preview-middleware": "1.2.27",
|
|
49
|
+
"@sap-ux/project-access": "2.3.1",
|
|
50
|
+
"@sap-ux/project-input-validator": "1.0.24",
|
|
51
|
+
"@sap-ux/system-access": "1.0.16",
|
|
51
52
|
"@sap-ux/store": "2.0.7",
|
|
52
|
-
"@sap-ux/ui5-config": "1.0.
|
|
53
|
-
"@sap-ux/ui5-test-writer": "1.18.
|
|
54
|
-
"@sap-ux/
|
|
55
|
-
"@sap-ux/
|
|
56
|
-
"@sap-ux/
|
|
57
|
-
"@sap-ux/
|
|
53
|
+
"@sap-ux/ui5-config": "1.0.10",
|
|
54
|
+
"@sap-ux/ui5-test-writer": "1.18.2",
|
|
55
|
+
"@sap-ux/fiori-migration-writer": "0.0.4",
|
|
56
|
+
"@sap-ux/feature-toggle": "1.0.5",
|
|
57
|
+
"@sap-ux/flp-config-inquirer": "1.0.65",
|
|
58
|
+
"@sap-ux/nodejs-utils": "1.0.11",
|
|
59
|
+
"@sap-ux/axios-extension": "2.1.2"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
62
|
"@jest/globals": "30.4.1",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"@types/mem-fs": "1.1.2",
|
|
64
66
|
"@types/mem-fs-editor": "7.0.1",
|
|
65
67
|
"@types/prompts": "2.4.9",
|
|
66
|
-
"@sap-ux/inquirer-common": "1.0.
|
|
68
|
+
"@sap-ux/inquirer-common": "1.0.48",
|
|
67
69
|
"@sap-ux/store": "2.0.7"
|
|
68
70
|
},
|
|
69
71
|
"scripts": {
|