@sap-ux/fiori-migration-writer 0.0.5 → 0.0.6
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/dist/ProjectMigrator.d.ts +20 -8
- package/dist/ProjectMigrator.js +121 -123
- package/dist/adapters/ui5-config-adapter.js +64 -18
- package/dist/adapters/ui5-config-helpers.js +3 -10
- package/dist/components/extension.d.ts +3 -1
- package/dist/components/extension.js +5 -4
- package/dist/config/backend-i18n.d.ts +27 -6
- package/dist/config/backend-i18n.js +5 -11
- package/dist/config/flp/intent.d.ts +6 -3
- package/dist/config/flp/intent.js +6 -4
- package/dist/config/flp/sandbox.d.ts +3 -1
- package/dist/config/flp/sandbox.js +17 -12
- package/dist/config/launch-config.d.ts +4 -3
- package/dist/config/launch-config.js +5 -16
- package/dist/config/manifest-update.d.ts +21 -1
- package/dist/config/manifest-update.js +7 -12
- package/dist/config/package-json.d.ts +3 -1
- package/dist/config/package-json.js +15 -9
- package/dist/config/ui5-yaml.d.ts +9 -4
- package/dist/config/ui5-yaml.js +14 -10
- package/dist/data/sap-libraries.d.ts +3 -1
- package/dist/data/sap-libraries.js +4 -3
- package/dist/data/ui5-version.js +1 -1
- package/dist/files/project-files.d.ts +4 -3
- package/dist/files/project-files.js +10 -16
- package/dist/files/webapp.d.ts +5 -2
- package/dist/files/webapp.js +30 -35
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/migration-process/configuration.d.ts +3 -1
- package/dist/migration-process/configuration.js +3 -2
- package/dist/migration-process/legacy-helpers.d.ts +3 -1
- package/dist/migration-process/legacy-helpers.js +19 -32
- package/dist/migration-process/legacy.d.ts +3 -1
- package/dist/migration-process/legacy.js +66 -79
- package/dist/migration-process/migration-phases.d.ts +6 -1
- package/dist/migration-process/migration-phases.js +26 -14
- package/dist/migration-process/setup.d.ts +9 -6
- package/dist/migration-process/setup.js +19 -10
- package/dist/migration-process/validation.d.ts +5 -2
- package/dist/migration-process/validation.js +6 -4
- package/dist/migration-types.d.ts +5 -2
- package/dist/project/adaptation-project.js +1 -0
- package/dist/project/project-data.js +1 -1
- package/dist/project/project-detection.d.ts +3 -1
- package/dist/project/project-detection.js +10 -4
- package/dist/project/project-extension.d.ts +1 -1
- package/dist/project/project-extension.js +3 -1
- package/dist/project/project-info.d.ts +3 -1
- package/dist/project/project-info.js +6 -3
- package/dist/project/regular-project.js +17 -9
- package/dist/template/application.d.ts +2 -0
- package/dist/template/application.js +11 -9
- package/dist/template/assembly.js +4 -4
- package/dist/template/base.d.ts +2 -0
- package/dist/template/base.js +2 -30
- package/dist/template/details.d.ts +3 -1
- package/dist/template/details.js +7 -5
- package/dist/template/template-helpers.d.ts +3 -1
- package/dist/template/template-helpers.js +6 -5
- package/dist/template/theme.d.ts +3 -1
- package/dist/template/theme.js +3 -2
- package/dist/types/mem-fs-types.d.ts +1 -1
- package/dist/utils/Project.d.ts +17 -14
- package/dist/utils/Project.js +94 -72
- package/dist/utils/checkForMigration.d.ts +4 -3
- package/dist/utils/checkForMigration.js +11 -8
- package/dist/utils/file-access.d.ts +36 -34
- package/dist/utils/file-access.js +61 -93
- package/dist/utils/file-discovery.d.ts +5 -2
- package/dist/utils/file-discovery.js +6 -4
- package/dist/utils/file-system-utils.d.ts +15 -5
- package/dist/utils/file-system-utils.js +34 -11
- package/dist/utils/fs-adapter.d.ts +5 -63
- package/dist/utils/fs-adapter.js +6 -111
- package/dist/utils/manifest-and-version-utils.d.ts +3 -1
- package/dist/utils/manifest-and-version-utils.js +6 -3
- package/dist/utils/migration-utils.d.ts +3 -1
- package/dist/utils/migration-utils.js +5 -4
- package/dist/utils/project-access-adapters.d.ts +3 -1
- package/dist/utils/project-access-adapters.js +3 -2
- package/dist/utils/project-discovery.d.ts +6 -2
- package/dist/utils/project-discovery.js +44 -18
- package/dist/utils/project-readers/adaptation-project-utils.d.ts +5 -2
- package/dist/utils/project-readers/adaptation-project-utils.js +21 -16
- package/dist/utils/project-readers/backend-utils.d.ts +3 -1
- package/dist/utils/project-readers/backend-utils.js +4 -3
- package/dist/utils/project-readers/extension-project-utils.d.ts +5 -2
- package/dist/utils/project-readers/extension-project-utils.js +15 -11
- package/dist/utils/project-readers/manifest-reader.d.ts +3 -1
- package/dist/utils/project-readers/manifest-reader.js +3 -2
- package/dist/utils/project-readers/manifest-utils.d.ts +5 -2
- package/dist/utils/project-readers/manifest-utils.js +9 -6
- package/dist/utils/project-readers/package-reader.d.ts +3 -1
- package/dist/utils/project-readers/package-reader.js +3 -2
- package/dist/utils/project-readers/reuse-lib-utils.d.ts +7 -3
- package/dist/utils/project-readers/reuse-lib-utils.js +13 -7
- package/dist/utils/project-readers/webapp-path-resolver.d.ts +3 -1
- package/dist/utils/project-readers/webapp-path-resolver.js +24 -19
- package/dist/utils/service.d.ts +3 -2
- package/dist/utils/service.js +10 -6
- package/dist/utils/template/template-post-processors.d.ts +3 -1
- package/dist/utils/template/template-post-processors.js +4 -3
- package/dist/utils/template/template-renderer.d.ts +3 -1
- package/dist/utils/template/template-renderer.js +3 -2
- package/dist/utils/template-generators/file-handlers.d.ts +5 -2
- package/dist/utils/template-generators/file-handlers.js +59 -32
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilities for detecting and processing reuse library projects
|
|
3
3
|
*/
|
|
4
|
+
import type { Editor } from 'mem-fs-editor';
|
|
4
5
|
import type { Manifest } from '../../project-spec-types.js';
|
|
5
6
|
import { MigrationTypes } from '../constants.js';
|
|
6
7
|
import type { ProjectFolder } from '../../types.js';
|
|
@@ -27,10 +28,11 @@ export declare function isSamePath(pathA: string, pathB: string): boolean;
|
|
|
27
28
|
* same way the workspace discovery path does and returns the library whose root
|
|
28
29
|
* resolves back to projectRoot.
|
|
29
30
|
*
|
|
31
|
+
* @param fs - mem-fs editor
|
|
30
32
|
* @param projectRoot - Root path of the project
|
|
31
33
|
* @returns The nested manifest and its path, or undefined if none is found
|
|
32
34
|
*/
|
|
33
|
-
export declare function findNestedReuseLibManifest(projectRoot: string): Promise<{
|
|
35
|
+
export declare function findNestedReuseLibManifest(fs: Editor, projectRoot: string): Promise<{
|
|
34
36
|
manifest: Manifest;
|
|
35
37
|
manifestPath: string;
|
|
36
38
|
} | undefined>;
|
|
@@ -40,16 +42,18 @@ export declare function findNestedReuseLibManifest(projectRoot: string): Promise
|
|
|
40
42
|
* @param projectRoot - Root path of the project
|
|
41
43
|
* @param type - Optional migration type
|
|
42
44
|
* @param manifest - Optional manifest object
|
|
45
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
43
46
|
* @returns True if project is a reuse library
|
|
44
47
|
*/
|
|
45
|
-
export declare function checkIfReuseLib(projectRoot: string, type?: MigrationTypes, manifest?: Manifest): Promise<boolean>;
|
|
48
|
+
export declare function checkIfReuseLib(projectRoot: string, type?: MigrationTypes, manifest?: Manifest, fsEditor?: Editor): Promise<boolean>;
|
|
46
49
|
/**
|
|
47
50
|
* Get module name for reuse library
|
|
48
51
|
*
|
|
49
52
|
* @param projectRoot - Root path of the project
|
|
50
53
|
* @param workspaceFolders - Optional workspace folders
|
|
51
54
|
* @param manifest - Optional manifest object
|
|
55
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
52
56
|
* @returns Module name
|
|
53
57
|
*/
|
|
54
|
-
export declare function getReuseLibModuleName(projectRoot: string, workspaceFolders?: readonly ProjectFolder[], manifest?: Manifest): Promise<string>;
|
|
58
|
+
export declare function getReuseLibModuleName(projectRoot: string, workspaceFolders?: readonly ProjectFolder[], manifest?: Manifest, fsEditor?: Editor): Promise<string>;
|
|
55
59
|
//# sourceMappingURL=reuse-lib-utils.d.ts.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { basename, join, relative } from 'node:path';
|
|
5
5
|
import { getReuseLibs, ReuseLibType } from '../file-discovery.js';
|
|
6
|
+
import { createMemFsEditor } from '../fs-adapter.js';
|
|
6
7
|
import { readJSON } from '../../index.js';
|
|
7
8
|
import { FileName } from '../../project-spec-types.js';
|
|
8
9
|
import { MigrationTypes } from '../constants.js';
|
|
@@ -32,17 +33,18 @@ export function isSamePath(pathA, pathB) {
|
|
|
32
33
|
* same way the workspace discovery path does and returns the library whose root
|
|
33
34
|
* resolves back to projectRoot.
|
|
34
35
|
*
|
|
36
|
+
* @param fs - mem-fs editor
|
|
35
37
|
* @param projectRoot - Root path of the project
|
|
36
38
|
* @returns The nested manifest and its path, or undefined if none is found
|
|
37
39
|
*/
|
|
38
|
-
export async function findNestedReuseLibManifest(projectRoot) {
|
|
39
|
-
const libs = await getReuseLibs([{ uri: URI.file(projectRoot), name: projectRoot, index: 0 }]);
|
|
40
|
+
export async function findNestedReuseLibManifest(fs, projectRoot) {
|
|
41
|
+
const libs = await getReuseLibs(fs, [{ uri: URI.file(projectRoot), name: projectRoot, index: 0 }]);
|
|
40
42
|
const matchedLib = libs.find((lib) => lib.value.type === ReuseLibType.LIBRARY && isSamePath(lib.value.libRoot, projectRoot));
|
|
41
43
|
if (!matchedLib) {
|
|
42
44
|
return undefined;
|
|
43
45
|
}
|
|
44
46
|
try {
|
|
45
|
-
const manifest = await readJSON(matchedLib.value.path);
|
|
47
|
+
const manifest = await readJSON(fs, matchedLib.value.path);
|
|
46
48
|
return { manifest, manifestPath: matchedLib.value.path };
|
|
47
49
|
}
|
|
48
50
|
catch {
|
|
@@ -57,17 +59,19 @@ export async function findNestedReuseLibManifest(projectRoot) {
|
|
|
57
59
|
* @param projectRoot - Root path of the project
|
|
58
60
|
* @param type - Optional migration type
|
|
59
61
|
* @param manifest - Optional manifest object
|
|
62
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
60
63
|
* @returns True if project is a reuse library
|
|
61
64
|
*/
|
|
62
|
-
export async function checkIfReuseLib(projectRoot, type, manifest) {
|
|
65
|
+
export async function checkIfReuseLib(projectRoot, type, manifest, fsEditor) {
|
|
63
66
|
if (type) {
|
|
64
67
|
return type === MigrationTypes.library;
|
|
65
68
|
}
|
|
66
69
|
if (manifest) {
|
|
67
70
|
return manifest['sap.app']?.type === 'library';
|
|
68
71
|
}
|
|
72
|
+
const fs = fsEditor ?? createMemFsEditor();
|
|
69
73
|
try {
|
|
70
|
-
const reuseManifest = await readJSON(join(projectRoot, FileName.Manifest));
|
|
74
|
+
const reuseManifest = await readJSON(fs, join(projectRoot, FileName.Manifest));
|
|
71
75
|
return reuseManifest['sap.app']?.type === 'library';
|
|
72
76
|
}
|
|
73
77
|
catch {
|
|
@@ -82,9 +86,11 @@ export async function checkIfReuseLib(projectRoot, type, manifest) {
|
|
|
82
86
|
* @param projectRoot - Root path of the project
|
|
83
87
|
* @param workspaceFolders - Optional workspace folders
|
|
84
88
|
* @param manifest - Optional manifest object
|
|
89
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
85
90
|
* @returns Module name
|
|
86
91
|
*/
|
|
87
|
-
export async function getReuseLibModuleName(projectRoot, workspaceFolders, manifest) {
|
|
92
|
+
export async function getReuseLibModuleName(projectRoot, workspaceFolders, manifest, fsEditor) {
|
|
93
|
+
const fs = fsEditor ?? createMemFsEditor();
|
|
88
94
|
let moduleName;
|
|
89
95
|
if (manifest) {
|
|
90
96
|
moduleName = manifest['sap.app']?.id;
|
|
@@ -96,7 +102,7 @@ export async function getReuseLibModuleName(projectRoot, workspaceFolders, manif
|
|
|
96
102
|
name: folder.name,
|
|
97
103
|
index: folder.index
|
|
98
104
|
}));
|
|
99
|
-
const libs = await getReuseLibs(workspaceFoldersWithUri);
|
|
105
|
+
const libs = await getReuseLibs(fs, workspaceFoldersWithUri);
|
|
100
106
|
const matchedLib = libs.find((lib) => {
|
|
101
107
|
return isSamePath(lib.value.libRoot, projectRoot);
|
|
102
108
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { Editor } from 'mem-fs-editor';
|
|
1
2
|
import { MigrationTypes } from '../constants.js';
|
|
2
3
|
/**
|
|
3
4
|
* Get webapp path for project
|
|
4
5
|
* Tries multiple sources in order of priority
|
|
5
6
|
*
|
|
7
|
+
* @param fs - mem-fs editor
|
|
6
8
|
* @param projectRoot
|
|
7
9
|
* @param type
|
|
8
10
|
* @param libPath
|
|
9
11
|
*/
|
|
10
|
-
export declare function getWebappPath(projectRoot: string, type?: MigrationTypes, libPath?: string): Promise<string>;
|
|
12
|
+
export declare function getWebappPath(fs: Editor, projectRoot: string, type?: MigrationTypes, libPath?: string): Promise<string>;
|
|
11
13
|
//# sourceMappingURL=webapp-path-resolver.d.ts.map
|
|
@@ -4,24 +4,26 @@
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
6
|
import { DirName, FileName } from '../../project-spec-types.js';
|
|
7
|
-
import { fileExists, readJSON
|
|
7
|
+
import { fileExists, readJSON } from '../../index.js';
|
|
8
|
+
import { readFile } from '../../utils/file-access.js';
|
|
8
9
|
import { sapWattCommonSetting } from '../../types.js';
|
|
9
10
|
import { MigrationTypes } from '../constants.js';
|
|
10
11
|
/**
|
|
11
12
|
* Resolve webapp path from .project.json file
|
|
12
13
|
*
|
|
14
|
+
* @param fs - mem-fs editor
|
|
13
15
|
* @param projectRoot
|
|
14
16
|
*/
|
|
15
|
-
async function resolveFromProjectJson(projectRoot) {
|
|
17
|
+
async function resolveFromProjectJson(fs, projectRoot) {
|
|
16
18
|
const projectJsonPath = join(projectRoot, '.project.json');
|
|
17
|
-
if (!(
|
|
19
|
+
if (!fileExists(fs, projectJsonPath)) {
|
|
18
20
|
return undefined;
|
|
19
21
|
}
|
|
20
22
|
try {
|
|
21
|
-
const projectJson = await readJSON(projectJsonPath);
|
|
23
|
+
const projectJson = await readJSON(fs, projectJsonPath);
|
|
22
24
|
if (projectJson?.hcpdeploy?.entryPath) {
|
|
23
25
|
const projectWebappPath = join(projectRoot, projectJson.hcpdeploy.entryPath);
|
|
24
|
-
return (
|
|
26
|
+
return fileExists(fs, projectWebappPath) ? projectJson.hcpdeploy.entryPath : undefined;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
catch (error) {
|
|
@@ -32,20 +34,21 @@ async function resolveFromProjectJson(projectRoot) {
|
|
|
32
34
|
/**
|
|
33
35
|
* Resolve webapp path from .che/project.json file
|
|
34
36
|
*
|
|
37
|
+
* @param fs - mem-fs editor
|
|
35
38
|
* @param projectRoot
|
|
36
39
|
*/
|
|
37
|
-
async function resolveFromCheProjectJson(projectRoot) {
|
|
40
|
+
async function resolveFromCheProjectJson(fs, projectRoot) {
|
|
38
41
|
const cheProjectJsonPath = join(projectRoot, '.che', 'project.json');
|
|
39
|
-
if (!(
|
|
42
|
+
if (!fileExists(fs, cheProjectJsonPath)) {
|
|
40
43
|
return undefined;
|
|
41
44
|
}
|
|
42
45
|
try {
|
|
43
|
-
const projectJson = await readJSON(cheProjectJsonPath);
|
|
46
|
+
const projectJson = await readJSON(fs, cheProjectJsonPath);
|
|
44
47
|
if (projectJson?.attributes?.[sapWattCommonSetting]) {
|
|
45
48
|
const settings = JSON.parse(projectJson.attributes[sapWattCommonSetting]?.[0]);
|
|
46
49
|
if (settings?.hcpdeploy?.entryPath) {
|
|
47
50
|
const projectWebappPath = join(projectRoot, settings.hcpdeploy.entryPath);
|
|
48
|
-
return (
|
|
51
|
+
return fileExists(fs, projectWebappPath) ? settings.hcpdeploy.entryPath : undefined;
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -57,11 +60,12 @@ async function resolveFromCheProjectJson(projectRoot) {
|
|
|
57
60
|
/**
|
|
58
61
|
* Resolve webapp path from ui5.yaml file
|
|
59
62
|
*
|
|
63
|
+
* @param fs - mem-fs editor
|
|
60
64
|
* @param projectRoot
|
|
61
65
|
*/
|
|
62
|
-
function resolveFromUi5Yaml(projectRoot) {
|
|
66
|
+
function resolveFromUi5Yaml(fs, projectRoot) {
|
|
63
67
|
try {
|
|
64
|
-
const yamlContent = readFile(join(projectRoot, FileName.UI5Yaml));
|
|
68
|
+
const yamlContent = readFile(fs, join(projectRoot, FileName.UI5Yaml));
|
|
65
69
|
return parse(yamlContent)?.resources?.configuration?.paths?.webapp;
|
|
66
70
|
}
|
|
67
71
|
catch (error) {
|
|
@@ -86,27 +90,28 @@ function resolveForLibrary(projectRoot, libPath) {
|
|
|
86
90
|
* Get webapp path for project
|
|
87
91
|
* Tries multiple sources in order of priority
|
|
88
92
|
*
|
|
93
|
+
* @param fs - mem-fs editor
|
|
89
94
|
* @param projectRoot
|
|
90
95
|
* @param type
|
|
91
96
|
* @param libPath
|
|
92
97
|
*/
|
|
93
|
-
export async function getWebappPath(projectRoot, type, libPath) {
|
|
98
|
+
export async function getWebappPath(fs, projectRoot, type, libPath) {
|
|
94
99
|
const defaultPath = join(projectRoot, DirName.Webapp);
|
|
95
100
|
// Check if default webapp folder exists
|
|
96
|
-
if (
|
|
101
|
+
if (fileExists(fs, defaultPath)) {
|
|
97
102
|
return DirName.Webapp;
|
|
98
103
|
}
|
|
99
104
|
// Check if manifest.json is in root (no webapp folder)
|
|
100
|
-
if (
|
|
105
|
+
if (fileExists(fs, join(projectRoot, FileName.Manifest))) {
|
|
101
106
|
return '';
|
|
102
107
|
}
|
|
103
108
|
// Check for legacy Maven/Java structure: src/main/webapp
|
|
104
109
|
const legacyMavenPath = join('src', 'main', DirName.Webapp);
|
|
105
110
|
const legacyMavenFullPath = join(projectRoot, legacyMavenPath);
|
|
106
|
-
if (
|
|
111
|
+
if (fileExists(fs, legacyMavenFullPath)) {
|
|
107
112
|
// Verify it actually has webapp content (manifest.json)
|
|
108
113
|
const legacyManifestPath = join(legacyMavenFullPath, FileName.Manifest);
|
|
109
|
-
if (
|
|
114
|
+
if (fileExists(fs, legacyManifestPath)) {
|
|
110
115
|
return legacyMavenPath;
|
|
111
116
|
}
|
|
112
117
|
}
|
|
@@ -118,17 +123,17 @@ export async function getWebappPath(projectRoot, type, libPath) {
|
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
// Try .project.json
|
|
121
|
-
const projectJsonPath = await resolveFromProjectJson(projectRoot);
|
|
126
|
+
const projectJsonPath = await resolveFromProjectJson(fs, projectRoot);
|
|
122
127
|
if (projectJsonPath) {
|
|
123
128
|
return projectJsonPath;
|
|
124
129
|
}
|
|
125
130
|
// Try .che/project.json
|
|
126
|
-
const cheProjectJsonPath = await resolveFromCheProjectJson(projectRoot);
|
|
131
|
+
const cheProjectJsonPath = await resolveFromCheProjectJson(fs, projectRoot);
|
|
127
132
|
if (cheProjectJsonPath) {
|
|
128
133
|
return cheProjectJsonPath;
|
|
129
134
|
}
|
|
130
135
|
// Try ui5.yaml
|
|
131
|
-
const ui5YamlPath = resolveFromUi5Yaml(projectRoot);
|
|
136
|
+
const ui5YamlPath = resolveFromUi5Yaml(fs, projectRoot);
|
|
132
137
|
if (ui5YamlPath) {
|
|
133
138
|
return ui5YamlPath;
|
|
134
139
|
}
|
package/dist/utils/service.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Manifest } from '../project-spec-types.js';
|
|
2
|
+
import type { Editor } from 'mem-fs-editor';
|
|
2
3
|
export declare function xmlToJson(metadata: any): JSON | undefined;
|
|
3
|
-
export declare function validateMetadata(rootPath: string, webappPath: string, isSAPApp?: boolean, manifestJSON?: Manifest | undefined):
|
|
4
|
+
export declare function validateMetadata(rootPath: string, webappPath: string, isSAPApp?: boolean, manifestJSON?: Manifest | undefined, fsEditor?: Editor): {
|
|
4
5
|
config: boolean;
|
|
5
6
|
fileExists: boolean;
|
|
6
|
-
}
|
|
7
|
+
};
|
|
7
8
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/utils/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileExists } from './file-access.js';
|
|
2
|
+
import { createMemFsEditor } from './fs-adapter.js';
|
|
2
3
|
import { XMLValidator, XMLParser } from 'fast-xml-parser';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
const options = {
|
|
@@ -11,7 +12,8 @@ const options = {
|
|
|
11
12
|
export function xmlToJson(metadata) {
|
|
12
13
|
return XMLValidator.validate(metadata) ? new XMLParser(options).parse(metadata, true) : undefined;
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
+
export function validateMetadata(rootPath, webappPath, isSAPApp, manifestJSON, fsEditor) {
|
|
16
|
+
const fs = fsEditor ?? createMemFsEditor();
|
|
15
17
|
if (!manifestJSON) {
|
|
16
18
|
return { config: false, fileExists: false };
|
|
17
19
|
}
|
|
@@ -21,7 +23,7 @@ export async function validateMetadata(rootPath, webappPath, isSAPApp, manifestJ
|
|
|
21
23
|
}
|
|
22
24
|
// Validate datasources if they exist
|
|
23
25
|
if (manifestJSON['sap.app']?.dataSources) {
|
|
24
|
-
return validateDataSources(manifestJSON['sap.app'].dataSources, rootPath, webappPath);
|
|
26
|
+
return validateDataSources(fs, manifestJSON['sap.app'].dataSources, rootPath, webappPath);
|
|
25
27
|
}
|
|
26
28
|
return { config: false, fileExists: false };
|
|
27
29
|
}
|
|
@@ -30,11 +32,12 @@ export async function validateMetadata(rootPath, webappPath, isSAPApp, manifestJ
|
|
|
30
32
|
* Preserves last-wins behavior: if multiple datasources have localUri,
|
|
31
33
|
* the result reflects the last one processed
|
|
32
34
|
*
|
|
35
|
+
* @param fs
|
|
33
36
|
* @param dataSources
|
|
34
37
|
* @param rootPath
|
|
35
38
|
* @param webappPath
|
|
36
39
|
*/
|
|
37
|
-
|
|
40
|
+
function validateDataSources(fs, dataSources, rootPath, webappPath) {
|
|
38
41
|
const datasourceKeys = Object.keys(dataSources);
|
|
39
42
|
let metadataValidation = { config: false, fileExists: false };
|
|
40
43
|
// Check datasources sequentially - stops at first valid metadata file found
|
|
@@ -42,7 +45,7 @@ async function validateDataSources(dataSources, rootPath, webappPath) {
|
|
|
42
45
|
const datasource = dataSources[datasourceKey];
|
|
43
46
|
// Check OData datasources or datasources without explicit type
|
|
44
47
|
if (datasource.type === 'OData' || datasource.type === undefined) {
|
|
45
|
-
const result =
|
|
48
|
+
const result = checkLocalMetadata(fs, datasource, rootPath, webappPath);
|
|
46
49
|
if (result.config) {
|
|
47
50
|
metadataValidation = result;
|
|
48
51
|
}
|
|
@@ -53,16 +56,17 @@ async function validateDataSources(dataSources, rootPath, webappPath) {
|
|
|
53
56
|
/**
|
|
54
57
|
* Check if local metadata file exists for a datasource
|
|
55
58
|
*
|
|
59
|
+
* @param fs
|
|
56
60
|
* @param datasource
|
|
57
61
|
* @param rootPath
|
|
58
62
|
* @param webappPath
|
|
59
63
|
*/
|
|
60
|
-
|
|
64
|
+
function checkLocalMetadata(fs, datasource, rootPath, webappPath) {
|
|
61
65
|
if (!datasource.settings?.localUri) {
|
|
62
66
|
return { config: false, fileExists: false };
|
|
63
67
|
}
|
|
64
68
|
const metadataPath = join(rootPath, webappPath, datasource.settings.localUri);
|
|
65
|
-
const exists =
|
|
69
|
+
const exists = fileExists(fs, metadataPath);
|
|
66
70
|
return {
|
|
67
71
|
config: true,
|
|
68
72
|
fileExists: exists
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Data } from 'ejs';
|
|
2
|
+
import type { Editor } from 'mem-fs-editor';
|
|
2
3
|
/**
|
|
3
4
|
* Apply file-specific post-processing handlers
|
|
4
5
|
*
|
|
@@ -10,6 +11,7 @@ import type { Data } from 'ejs';
|
|
|
10
11
|
* - GitIgnore: Merges with existing .gitignore if present
|
|
11
12
|
* - package.json: Merges with existing package.json if present
|
|
12
13
|
*
|
|
14
|
+
* @param fs - mem-fs editor
|
|
13
15
|
* @param templateName - Name of the template being processed
|
|
14
16
|
* @param templateContent - Rendered template content
|
|
15
17
|
* @param projectRoot - Root directory of the project
|
|
@@ -17,5 +19,5 @@ import type { Data } from 'ejs';
|
|
|
17
19
|
* @param templateData - Original template data (for package.json handler)
|
|
18
20
|
* @returns Post-processed template content
|
|
19
21
|
*/
|
|
20
|
-
export declare function applyFileSpecificHandlers(templateName: string, templateContent: string, projectRoot: string, targetFile: string, templateData: Data): Promise<string>;
|
|
22
|
+
export declare function applyFileSpecificHandlers(fs: Editor, templateName: string, templateContent: string, projectRoot: string, targetFile: string, templateData: Data): Promise<string>;
|
|
21
23
|
//# sourceMappingURL=template-post-processors.d.ts.map
|
|
@@ -14,6 +14,7 @@ const packageJsonTmplName = `${FileName.Package}.tmpl`;
|
|
|
14
14
|
* - GitIgnore: Merges with existing .gitignore if present
|
|
15
15
|
* - package.json: Merges with existing package.json if present
|
|
16
16
|
*
|
|
17
|
+
* @param fs - mem-fs editor
|
|
17
18
|
* @param templateName - Name of the template being processed
|
|
18
19
|
* @param templateContent - Rendered template content
|
|
19
20
|
* @param projectRoot - Root directory of the project
|
|
@@ -21,18 +22,18 @@ const packageJsonTmplName = `${FileName.Package}.tmpl`;
|
|
|
21
22
|
* @param templateData - Original template data (for package.json handler)
|
|
22
23
|
* @returns Post-processed template content
|
|
23
24
|
*/
|
|
24
|
-
export async function applyFileSpecificHandlers(templateName, templateContent, projectRoot, targetFile, templateData) {
|
|
25
|
+
export async function applyFileSpecificHandlers(fs, templateName, templateContent, projectRoot, targetFile, templateData) {
|
|
25
26
|
// Handle LocateReuseLibs template
|
|
26
27
|
if (templateName === TemplateFileName.LocateReuseLibs) {
|
|
27
28
|
return handleLocateReuseLibsFile(templateContent, templateData);
|
|
28
29
|
}
|
|
29
30
|
// Handle .gitignore template
|
|
30
31
|
if (templateName === TemplateFileName.GitIgnoreTmpl) {
|
|
31
|
-
return handleGitIgnoreFile(targetFile, templateContent);
|
|
32
|
+
return handleGitIgnoreFile(targetFile, templateContent, fs);
|
|
32
33
|
}
|
|
33
34
|
// Handle package.json template
|
|
34
35
|
if (templateName === packageJsonTmplName) {
|
|
35
|
-
return handlePackageJsonFile(projectRoot, targetFile, templateContent, templateData);
|
|
36
|
+
return handlePackageJsonFile(projectRoot, targetFile, templateContent, templateData, fs);
|
|
36
37
|
}
|
|
37
38
|
// No special handling needed
|
|
38
39
|
return templateContent;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Data } from 'ejs';
|
|
2
|
+
import type { Editor } from 'mem-fs-editor';
|
|
2
3
|
/**
|
|
3
4
|
* Render a template file
|
|
4
5
|
*
|
|
5
6
|
* Reads the template file and optionally renders it using EJS.
|
|
6
7
|
* If rendering is disabled, returns the raw file content.
|
|
7
8
|
*
|
|
9
|
+
* @param fs - mem-fs editor
|
|
8
10
|
* @param templateRoot - Root directory containing templates
|
|
9
11
|
* @param fullTemplateName - Full path to template file relative to templateRoot
|
|
10
12
|
* @param templateData - Data to use for template rendering
|
|
@@ -13,5 +15,5 @@ import type { Data } from 'ejs';
|
|
|
13
15
|
* @returns Rendered template content or raw file content
|
|
14
16
|
* @throws Error if template path contains path traversal
|
|
15
17
|
*/
|
|
16
|
-
export declare function renderTemplate(templateRoot: string, fullTemplateName: string, templateData: Data, opts: object, isRendered: boolean): string;
|
|
18
|
+
export declare function renderTemplate(fs: Editor, templateRoot: string, fullTemplateName: string, templateData: Data, opts: object, isRendered: boolean): string;
|
|
17
19
|
//# sourceMappingURL=template-renderer.d.ts.map
|
|
@@ -30,6 +30,7 @@ function validateTemplatePath(templateRoot, templateName) {
|
|
|
30
30
|
* Reads the template file and optionally renders it using EJS.
|
|
31
31
|
* If rendering is disabled, returns the raw file content.
|
|
32
32
|
*
|
|
33
|
+
* @param fs - mem-fs editor
|
|
33
34
|
* @param templateRoot - Root directory containing templates
|
|
34
35
|
* @param fullTemplateName - Full path to template file relative to templateRoot
|
|
35
36
|
* @param templateData - Data to use for template rendering
|
|
@@ -38,10 +39,10 @@ function validateTemplatePath(templateRoot, templateName) {
|
|
|
38
39
|
* @returns Rendered template content or raw file content
|
|
39
40
|
* @throws Error if template path contains path traversal
|
|
40
41
|
*/
|
|
41
|
-
export function renderTemplate(templateRoot, fullTemplateName, templateData, opts, isRendered) {
|
|
42
|
+
export function renderTemplate(fs, templateRoot, fullTemplateName, templateData, opts, isRendered) {
|
|
42
43
|
// Validate path to prevent directory traversal attacks
|
|
43
44
|
const safePath = validateTemplatePath(templateRoot, fullTemplateName);
|
|
44
|
-
const templateFile = readFile(safePath);
|
|
45
|
+
const templateFile = readFile(fs, safePath);
|
|
45
46
|
return isRendered ? render(templateFile, templateData, { ...DEFAULT_EJS_OPTS, ...opts }) : templateFile;
|
|
46
47
|
}
|
|
47
48
|
//# sourceMappingURL=template-renderer.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { Editor } from 'mem-fs-editor';
|
|
1
2
|
/**
|
|
2
3
|
* Handle .gitignore file generation - append to existing file
|
|
3
4
|
*
|
|
4
5
|
* @param targetFile
|
|
5
6
|
* @param templateContent
|
|
7
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
6
8
|
*/
|
|
7
|
-
export declare function handleGitIgnoreFile(targetFile: string, templateContent: string):
|
|
9
|
+
export declare function handleGitIgnoreFile(targetFile: string, templateContent: string, fsEditor?: Editor): string;
|
|
8
10
|
/**
|
|
9
11
|
* Handle package.json file generation - merge with existing file
|
|
10
12
|
*
|
|
@@ -12,8 +14,9 @@ export declare function handleGitIgnoreFile(targetFile: string, templateContent:
|
|
|
12
14
|
* @param targetFile
|
|
13
15
|
* @param templateContent
|
|
14
16
|
* @param templateData
|
|
17
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
15
18
|
*/
|
|
16
|
-
export declare function handlePackageJsonFile(projectRoot: string, targetFile: string, templateContent: string, templateData: any): Promise<string>;
|
|
19
|
+
export declare function handlePackageJsonFile(projectRoot: string, targetFile: string, templateContent: string, templateData: any, fsEditor?: Editor): Promise<string>;
|
|
17
20
|
/**
|
|
18
21
|
* Handle locateReuseLibs file - replace mockserver path
|
|
19
22
|
*
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { readFile, fileExists, readJSON, deleteFile } from '../file-access.js';
|
|
7
|
+
import { createMemFsEditor } from '../fs-adapter.js';
|
|
7
8
|
import mergeWith from 'lodash/mergeWith.js';
|
|
8
9
|
import { MigrationTypes } from '../constants.js';
|
|
9
10
|
/**
|
|
@@ -11,17 +12,54 @@ import { MigrationTypes } from '../constants.js';
|
|
|
11
12
|
*
|
|
12
13
|
* @param targetFile
|
|
13
14
|
* @param templateContent
|
|
15
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
14
16
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
+
export function handleGitIgnoreFile(targetFile, templateContent, fsEditor) {
|
|
18
|
+
const fs = fsEditor ?? createMemFsEditor();
|
|
19
|
+
if (!fileExists(fs, targetFile)) {
|
|
17
20
|
return templateContent;
|
|
18
21
|
}
|
|
19
|
-
const currentFileContent = readFile(targetFile);
|
|
22
|
+
const currentFileContent = readFile(fs, targetFile);
|
|
20
23
|
let mergedContent = `${currentFileContent}\n${templateContent}`;
|
|
21
24
|
// Replace `changes_preview.js` so it can be checked in after migration
|
|
22
25
|
mergedContent = mergedContent.replaceAll(`/changes_preview.js`, '').replaceAll(`changes_preview.js`, '');
|
|
23
26
|
return mergedContent;
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Rename scripts in the existing package.json that collide with template script names,
|
|
30
|
+
* preserving the originals under a `<name>_old` key.
|
|
31
|
+
*
|
|
32
|
+
* @param currentFileContent - Parsed existing package.json (mutated in place)
|
|
33
|
+
* @param templateContentJSON - Parsed template package.json
|
|
34
|
+
*/
|
|
35
|
+
function renameConflictingScripts(currentFileContent, templateContentJSON) {
|
|
36
|
+
const oldScripts = {};
|
|
37
|
+
for (const script in templateContentJSON?.scripts) {
|
|
38
|
+
if (currentFileContent?.scripts?.[script] && !currentFileContent.scripts[`${script}_old`]) {
|
|
39
|
+
oldScripts[`${script}_old`] = currentFileContent.scripts[script];
|
|
40
|
+
delete currentFileContent.scripts[script];
|
|
41
|
+
}
|
|
42
|
+
else if (currentFileContent?.scripts?.[script]) {
|
|
43
|
+
delete currentFileContent.scripts[script];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const oldScript in oldScripts) {
|
|
47
|
+
currentFileContent.scripts[oldScript] = oldScripts[oldScript];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Add the extension-project `start-ui5-flp` script to the template package.json.
|
|
52
|
+
*
|
|
53
|
+
* @param templateContentJSON - Parsed template package.json (mutated in place)
|
|
54
|
+
* @param templateData - Template data with project/service configuration
|
|
55
|
+
*/
|
|
56
|
+
function addExtensionStartScript(templateContentJSON, templateData) {
|
|
57
|
+
let queryStr = templateData.project.ui5Theme ? `sap-theme=${templateData.project.ui5Theme}&` : '';
|
|
58
|
+
queryStr = templateData.service.client ? `${queryStr}sap-client=${templateData.service.client}&` : queryStr;
|
|
59
|
+
templateContentJSON.scripts ??= {};
|
|
60
|
+
templateContentJSON.scripts['start-ui5-flp'] =
|
|
61
|
+
`fiori run --open "/test-resources/sap/ushell/shells/sandbox/fioriSandbox.html?${queryStr}sap-ushell-test-url-url=..%2F..%2F..%2F..%2F..%2F..&sap-ushell-test-url-additionalInformation=SAPUI5.Component%3D${templateData.fullyQualifiedProjectName}#Test-url"`;
|
|
62
|
+
}
|
|
25
63
|
/**
|
|
26
64
|
* Handle package.json file generation - merge with existing file
|
|
27
65
|
*
|
|
@@ -29,14 +67,17 @@ export async function handleGitIgnoreFile(targetFile, templateContent) {
|
|
|
29
67
|
* @param targetFile
|
|
30
68
|
* @param templateContent
|
|
31
69
|
* @param templateData
|
|
70
|
+
* @param fsEditor - optional mem-fs editor; a fresh one is created when omitted
|
|
32
71
|
*/
|
|
33
|
-
export async function handlePackageJsonFile(projectRoot, targetFile, templateContent, templateData) {
|
|
34
|
-
|
|
72
|
+
export async function handlePackageJsonFile(projectRoot, targetFile, templateContent, templateData, fsEditor) {
|
|
73
|
+
const fs = fsEditor ?? createMemFsEditor();
|
|
74
|
+
const ownsEditor = !fsEditor;
|
|
75
|
+
if (!fileExists(fs, targetFile)) {
|
|
35
76
|
return templateContent;
|
|
36
77
|
}
|
|
37
78
|
let currentFileContent;
|
|
38
79
|
try {
|
|
39
|
-
currentFileContent = await readJSON(targetFile);
|
|
80
|
+
currentFileContent = await readJSON(fs, targetFile);
|
|
40
81
|
}
|
|
41
82
|
catch (error) {
|
|
42
83
|
// If file can't be read (shouldn't happen after fileExists check, but handle it anyway)
|
|
@@ -44,35 +85,15 @@ export async function handlePackageJsonFile(projectRoot, targetFile, templateCon
|
|
|
44
85
|
return templateContent;
|
|
45
86
|
}
|
|
46
87
|
const templateContentJSON = JSON.parse(templateContent);
|
|
47
|
-
|
|
48
|
-
const oldScripts = {};
|
|
49
|
-
for (const script in templateContentJSON?.scripts) {
|
|
50
|
-
if (currentFileContent?.scripts?.[script] && !currentFileContent.scripts[`${script}_old`]) {
|
|
51
|
-
oldScripts[`${script}_old`] = currentFileContent.scripts[script];
|
|
52
|
-
delete currentFileContent.scripts[script];
|
|
53
|
-
}
|
|
54
|
-
else if (currentFileContent?.scripts?.[script]) {
|
|
55
|
-
delete currentFileContent.scripts[script];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Add renamed old scripts back
|
|
59
|
-
for (const oldScript in oldScripts) {
|
|
60
|
-
currentFileContent.scripts[oldScript] = oldScripts[oldScript];
|
|
61
|
-
}
|
|
88
|
+
renameConflictingScripts(currentFileContent, templateContentJSON);
|
|
62
89
|
// Handle extension project special script
|
|
63
90
|
if (templateData.project.type === MigrationTypes.projectExtension) {
|
|
64
|
-
|
|
65
|
-
queryStr = templateData.service.client ? `${queryStr}sap-client=${templateData.service.client}&` : queryStr;
|
|
66
|
-
if (!templateContentJSON.scripts) {
|
|
67
|
-
templateContentJSON.scripts = {};
|
|
68
|
-
}
|
|
69
|
-
templateContentJSON.scripts['start-ui5-flp'] =
|
|
70
|
-
`fiori run --open "/test-resources/sap/ushell/shells/sandbox/fioriSandbox.html?${queryStr}sap-ushell-test-url-url=..%2F..%2F..%2F..%2F..%2F..&sap-ushell-test-url-additionalInformation=SAPUI5.Component%3D${templateData.fullyQualifiedProjectName}#Test-url"`;
|
|
91
|
+
addExtensionStartScript(templateContentJSON, templateData);
|
|
71
92
|
}
|
|
72
93
|
// Remove conflicting dependencies
|
|
73
94
|
removeLegacyDependencies(currentFileContent, templateContentJSON);
|
|
74
95
|
// Remove legacy Grunt module and file
|
|
75
|
-
|
|
96
|
+
removeLegacyGruntModule(fs, projectRoot, currentFileContent);
|
|
76
97
|
// Remove old beta ui5-tooling module
|
|
77
98
|
removeBetaUi5ToolingModule(currentFileContent);
|
|
78
99
|
// Filter duplicate keywords
|
|
@@ -83,6 +104,11 @@ export async function handlePackageJsonFile(projectRoot, targetFile, templateCon
|
|
|
83
104
|
return objValue.concat(srcValue);
|
|
84
105
|
}
|
|
85
106
|
});
|
|
107
|
+
// When we created the editor ourselves, persist staged side effects (e.g. Gruntfile.js deletion).
|
|
108
|
+
// When the caller passed an editor, they own the commit (migration flow commits once at the end).
|
|
109
|
+
if (ownsEditor) {
|
|
110
|
+
await new Promise((resolve, reject) => fs.commit((err) => (err ? reject(err) : resolve())));
|
|
111
|
+
}
|
|
86
112
|
return JSON.stringify(mergedContent, null, 2);
|
|
87
113
|
}
|
|
88
114
|
/**
|
|
@@ -117,10 +143,11 @@ function removeLegacyDependencies(currentFileContent, templateContentJSON) {
|
|
|
117
143
|
/**
|
|
118
144
|
* Remove legacy Grunt module and associated file
|
|
119
145
|
*
|
|
146
|
+
* @param fs - mem-fs editor
|
|
120
147
|
* @param projectRoot
|
|
121
148
|
* @param currentFileContent
|
|
122
149
|
*/
|
|
123
|
-
|
|
150
|
+
function removeLegacyGruntModule(fs, projectRoot, currentFileContent) {
|
|
124
151
|
const gruntModule = '@sap/grunt-sapui5-bestpractice-build';
|
|
125
152
|
let deleteGruntFile = false;
|
|
126
153
|
if (currentFileContent?.devDependencies?.[gruntModule]) {
|
|
@@ -134,8 +161,8 @@ async function removeLegacyGruntModule(projectRoot, currentFileContent) {
|
|
|
134
161
|
// If dependency exists, also remove the Gruntfile.js
|
|
135
162
|
if (deleteGruntFile) {
|
|
136
163
|
const gruntFilePath = join(projectRoot, 'Gruntfile.js');
|
|
137
|
-
if (
|
|
138
|
-
deleteFile(gruntFilePath);
|
|
164
|
+
if (fileExists(fs, gruntFilePath)) {
|
|
165
|
+
deleteFile(fs, gruntFilePath);
|
|
139
166
|
}
|
|
140
167
|
}
|
|
141
168
|
}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Afiori-migration-writer"
|
|
12
12
|
},
|
|
13
|
-
"version": "0.0.
|
|
13
|
+
"version": "0.0.6",
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"files": [
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
"uuid": "11.1.1",
|
|
38
38
|
"vscode-uri": "3.1.0",
|
|
39
39
|
"yaml": "2.9.0",
|
|
40
|
-
"@sap-ux/adp-tooling": "1.0.66",
|
|
41
40
|
"@sap-ux/axios-extension": "2.1.3",
|
|
42
41
|
"@sap-ux/btp-utils": "2.0.8",
|
|
43
42
|
"@sap-ux/i18n": "1.0.6",
|
|
44
43
|
"@sap-ux/launch-config": "1.0.25",
|
|
44
|
+
"@sap-ux/adp-tooling": "1.0.66",
|
|
45
45
|
"@sap-ux/nodejs-utils": "1.0.12",
|
|
46
46
|
"@sap-ux/project-access": "2.3.1",
|
|
47
|
-
"@sap-ux/store": "2.0.7",
|
|
48
47
|
"@sap-ux/ui5-application-writer": "2.1.6",
|
|
49
48
|
"@sap-ux/ui5-config": "1.0.10",
|
|
50
|
-
"@sap-ux/ui5-info": "1.0.7"
|
|
49
|
+
"@sap-ux/ui5-info": "1.0.7",
|
|
50
|
+
"@sap-ux/store": "2.0.7"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jest/globals": "30.4.1",
|