@sap-ux/fiori-migration-writer 0.0.2 → 0.0.3

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.
@@ -1,6 +1,6 @@
1
1
  import { FioriElementsVersion, FileName } from './project-spec-types.js';
2
2
  import { determineMessage, readFile, updateFile } from './utils/index.js';
3
- import { createMemFsEditor, getCurrentEditor, runWithEditor, exists } from './utils/fs-adapter.js';
3
+ import { createMemFsEditor, getCurrentEditor, runWithEditor, exists, loadProjectIntoEditor } from './utils/fs-adapter.js';
4
4
  import { commitFileSystemChanges } from './files/file-system.js';
5
5
  import { ui5VersionRequestInfo } from '@sap-ux/ui5-info';
6
6
  import { getAppProgrammingLanguage } from '@sap-ux/project-access';
@@ -50,6 +50,9 @@ export class ProjectMigrator {
50
50
  let messages = [];
51
51
  let result = false;
52
52
  try {
53
+ // Load the project tree into mem-fs so existence checks see untracked files
54
+ // on disk and migration writes never silently overwrite them.
55
+ await loadProjectIntoEditor(editor, projectRoot);
53
56
  // Load or fetch project information
54
57
  const { projectInfo, messages: projectInfoMessages } = await loadOrFetchProjectInfo(projectRoot, importProjectInfoOpt);
55
58
  messages = messages.concat(projectInfoMessages);
@@ -47,6 +47,40 @@ function addMockserverMiddleware(orderedYaml, templateData) {
47
47
  orderedYaml.server.customMiddleware.unshift(mockserverMiddleware);
48
48
  }
49
49
  }
50
+ /**
51
+ * Create a new UI5Config instance with metadata and type pre-configured.
52
+ *
53
+ * @param templateData - Template data providing the config name
54
+ * @returns Configured UI5Config instance
55
+ */
56
+ async function createBaseConfig(templateData) {
57
+ const config = await UI5Config.newInstance('', { validateSchema: false });
58
+ config.setMetadata({
59
+ name: templateData.ui5Yaml?.name?.toLowerCase() || templateData.project.name?.toLowerCase() || 'app'
60
+ });
61
+ config.setType('application');
62
+ return config;
63
+ }
64
+ /**
65
+ * Post-process a UI5Config YAML string: add specVersion 4.0 at the top and
66
+ * optionally set webapp and appreload paths.
67
+ *
68
+ * @param yamlContent - Raw YAML string from UI5Config.toString()
69
+ * @param webappPath - Optional webapp path to inject
70
+ * @returns Final YAML string with specVersion and optional paths applied
71
+ */
72
+ function postProcessYaml(yamlContent, webappPath) {
73
+ const yamlJson = parse(yamlContent);
74
+ const orderedYaml = { specVersion: '4.0' };
75
+ Object.keys(yamlJson).forEach((key) => {
76
+ orderedYaml[key] = yamlJson[key];
77
+ });
78
+ if (webappPath) {
79
+ setWebappPath(orderedYaml, webappPath);
80
+ setAppreloadPath(orderedYaml, webappPath);
81
+ }
82
+ return stringify(orderedYaml);
83
+ }
50
84
  /**
51
85
  * Build backend configuration from template data and neo-app destinations
52
86
  *
@@ -86,12 +120,7 @@ function buildBackendConfigs(templateData, neoappDestinations, messages, destina
86
120
  */
87
121
  export async function generateUI5YamlContent(options) {
88
122
  const { templateData, neoappDestinations, messages, destination, firstNeoAppDestination, webappPath, setUI5Version, strict = false } = options;
89
- const config = await UI5Config.newInstance('', { validateSchema: false });
90
- // Set metadata and type
91
- config.setMetadata({
92
- name: templateData.ui5Yaml?.name?.toLowerCase() || templateData.project.name?.toLowerCase() || 'app'
93
- });
94
- config.setType('application');
123
+ const config = await createBaseConfig(templateData);
95
124
  // Build backend configuration using helpers
96
125
  const backends = buildBackendConfigs(templateData, neoappDestinations, messages, destination, firstNeoAppDestination, strict);
97
126
  // Add fiori-tools-proxy middleware with all backends
@@ -104,20 +133,7 @@ export async function generateUI5YamlContent(options) {
104
133
  if (previewMiddleware) {
105
134
  config.addCustomMiddleware(previewMiddleware);
106
135
  }
107
- const yamlContent = config.toString();
108
- // Post-process: Add specVersion 4.0 (UI5Config doesn't set this)
109
- const yamlJson = parse(yamlContent);
110
- // Add specVersion at the top
111
- const orderedYaml = { specVersion: '4.0' };
112
- Object.keys(yamlJson).forEach((key) => {
113
- orderedYaml[key] = yamlJson[key];
114
- });
115
- // Apply webappPath if provided
116
- if (webappPath) {
117
- setWebappPath(orderedYaml, webappPath);
118
- setAppreloadPath(orderedYaml, webappPath);
119
- }
120
- return stringify(orderedYaml);
136
+ return postProcessYaml(config.toString(), webappPath);
121
137
  }
122
138
  /**
123
139
  * Generate ui5-local.yaml content using @sap-ux/ui5-config builder
@@ -135,12 +151,7 @@ export async function generateUI5YamlContent(options) {
135
151
  */
136
152
  export async function generateUI5LocalYamlContent(options) {
137
153
  const { templateData, neoappDestinations, messages, destination, firstNeoAppDestination, webappPath, strict = false } = options;
138
- const config = await UI5Config.newInstance('', { validateSchema: false });
139
- // Set metadata and type
140
- config.setMetadata({
141
- name: templateData.ui5Yaml?.name?.toLowerCase() || templateData.project.name?.toLowerCase() || 'app'
142
- });
143
- config.setType('application');
154
+ const config = await createBaseConfig(templateData);
144
155
  // Add framework section with SAPUI5 libraries (ui5-local.yaml specific)
145
156
  if (templateData.project.localUI5Version && templateData.ui5Yaml?.sapUiLibs) {
146
157
  const libraries = [...templateData.ui5Yaml.sapUiLibs];
@@ -165,9 +176,7 @@ export async function generateUI5LocalYamlContent(options) {
165
176
  config.addCustomMiddleware(previewMiddleware);
166
177
  }
167
178
  const yamlContent = config.toString();
168
- // Parse and post-process
169
179
  const yamlJson = parse(yamlContent);
170
- // Add specVersion at the top
171
180
  const orderedYaml = { specVersion: '4.0' };
172
181
  Object.keys(yamlJson).forEach((key) => {
173
182
  orderedYaml[key] = yamlJson[key];
@@ -192,12 +201,7 @@ export async function generateUI5LocalYamlContent(options) {
192
201
  * @returns Promise resolving to ui5-mock.yaml content as string
193
202
  */
194
203
  export async function generateUI5MockYamlContent(templateData, webappPath, setUI5Version) {
195
- const config = await UI5Config.newInstance('', { validateSchema: false });
196
- // Set metadata and type
197
- config.setMetadata({
198
- name: templateData.ui5Yaml?.name?.toLowerCase() || templateData.project.name?.toLowerCase() || 'app'
199
- });
200
- config.setType('application');
204
+ const config = await createBaseConfig(templateData);
201
205
  // Add fiori-tools-proxy middleware with UI5 version if provided
202
206
  const proxyConfig = {
203
207
  ignoreCertErrors: false,
@@ -214,19 +218,6 @@ export async function generateUI5MockYamlContent(templateData, webappPath, setUI
214
218
  config.addFioriToolsProxyMiddleware(proxyConfig);
215
219
  // Add fiori-tools-appreload middleware
216
220
  config.addFioriToolsAppReloadMiddleware();
217
- const yamlContent = config.toString();
218
- // Post-process: Add specVersion and webappPath
219
- const yamlJson = parse(yamlContent);
220
- // Add specVersion at the top
221
- const orderedYaml = { specVersion: '4.0' };
222
- Object.keys(yamlJson).forEach((key) => {
223
- orderedYaml[key] = yamlJson[key];
224
- });
225
- // Apply webappPath if provided
226
- if (webappPath) {
227
- setWebappPath(orderedYaml, webappPath);
228
- setAppreloadPath(orderedYaml, webappPath);
229
- }
230
- return stringify(orderedYaml);
221
+ return postProcessYaml(config.toString(), webappPath);
231
222
  }
232
223
  //# sourceMappingURL=ui5-config-adapter.js.map
package/dist/index.d.ts CHANGED
@@ -45,12 +45,12 @@ export { BulkProjectMigrator } from './BulkProjectMigrator.js';
45
45
  export { createMemFsEditor } from './utils/fs-adapter.js';
46
46
  export * from './types.js';
47
47
  export * from './i18n.js';
48
- export { findProjectRoot, getProjectType, getMainService, hasDependency, getMinUI5VersionAsArray, type CdsUi5PluginInfo, readFile, readJSON, fileExists, writeFile, updateFile, updateJSON, deleteFile, findAllManifest, isFioriToolsProject, determineMessage, createDirectory } from './utils/index.js';
48
+ export { findProjectRoot, getProjectType, getMainService, hasDependency, getMinUI5VersionAsArray, type CdsUi5PluginInfo, readFile, readJSON, fileExists, writeFile, updateFile, updateJSON, deleteFile, findAllManifest, isFioriToolsProject, determineMessage } from './utils/index.js';
49
49
  export { buildMainBackend, buildProxyConfig, buildPreviewMiddleware } from './adapters/ui5-config-helpers.js';
50
50
  export { generateUI5YamlContent, generateUI5LocalYamlContent, generateUI5MockYamlContent } from './adapters/ui5-config-adapter.js';
51
51
  export type { UI5YamlOptions } from './adapters/ui5-config-adapter.js';
52
52
  export { readManifest, getUI5Version } from './utils/manifest-and-version-utils.js';
53
- export { doesPropertyExist, stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './utils/file-system-utils.js';
53
+ export { stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './utils/file-system-utils.js';
54
54
  export { generateSapLibsStr, getSourceTemplate } from './utils/migration-utils.js';
55
55
  export { getWebIDEProjectPathsAsLabels } from './utils/project-discovery.js';
56
56
  export { handleGitIgnoreFile, handlePackageJsonFile, handleLocateReuseLibsFile } from './utils/template-generators/index.js';
package/dist/index.js CHANGED
@@ -56,13 +56,13 @@ export * from './i18n.js';
56
56
  // Public API - Utilities (re-export selective functions for backward compatibility)
57
57
  export { findProjectRoot, getProjectType, getMainService, hasDependency, getMinUI5VersionAsArray, readFile, readJSON, fileExists, writeFile, updateFile, updateJSON, deleteFile, findAllManifest, isFioriToolsProject,
58
58
  // Additional exports for test compatibility
59
- determineMessage, createDirectory } from './utils/index.js';
59
+ determineMessage } from './utils/index.js';
60
60
  // Additional exports from adapters for test compatibility
61
61
  export { buildMainBackend, buildProxyConfig, buildPreviewMiddleware } from './adapters/ui5-config-helpers.js';
62
62
  export { generateUI5YamlContent, generateUI5LocalYamlContent, generateUI5MockYamlContent } from './adapters/ui5-config-adapter.js';
63
63
  // Additional exports from utils for test compatibility
64
64
  export { readManifest, getUI5Version } from './utils/manifest-and-version-utils.js';
65
- export { doesPropertyExist, stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './utils/file-system-utils.js';
65
+ export { stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './utils/file-system-utils.js';
66
66
  export { generateSapLibsStr, getSourceTemplate } from './utils/migration-utils.js';
67
67
  export { getWebIDEProjectPathsAsLabels } from './utils/project-discovery.js';
68
68
  export { handleGitIgnoreFile, handlePackageJsonFile, handleLocateReuseLibsFile } from './utils/template-generators/index.js';
@@ -31,81 +31,48 @@ export function buildLegacyPaths(rootPath, legacyPath) {
31
31
  * @param rootPath - Project root path
32
32
  * @param paths - Legacy paths object
33
33
  */
34
+ /**
35
+ * Move all mem-fs files rooted at `fromPath` to be rooted at `toPath`.
36
+ * Iterates the store after collecting to avoid modifying during iteration.
37
+ *
38
+ * mem-fs keys use the OS-native separator, so comparisons use `sep`.
39
+ *
40
+ * @param fs
41
+ * @param fromPath
42
+ * @param toPath
43
+ */
44
+ function moveMemFsDirectory(fs, fromPath, toPath) {
45
+ const filesToMove = [];
46
+ fs.store.each((file) => {
47
+ const filePath = file.path;
48
+ if (filePath === fromPath || filePath.startsWith(fromPath + sep)) {
49
+ const relativePath = filePath === fromPath ? '' : filePath.substring(fromPath.length + 1);
50
+ const newPath = relativePath ? join(toPath, relativePath) : toPath;
51
+ filesToMove.push({ oldPath: filePath, newPath });
52
+ }
53
+ });
54
+ for (const { oldPath, newPath } of filesToMove) {
55
+ const fileContent = fs.read(oldPath);
56
+ fs.write(newPath, fileContent);
57
+ fs.delete(oldPath);
58
+ }
59
+ }
34
60
  async function memFsMove(rootPath, paths) {
35
- const fs = getCurrentEditor();
36
- if (!fs || !hasStore(fs)) {
61
+ const editor = getCurrentEditor();
62
+ if (!editor || !hasStore(editor)) {
37
63
  return;
38
64
  }
39
65
  // Move webapp files from src/main/webapp to webapp
40
66
  if (await exists(paths.ffLegacyWebappPath)) {
41
- // Iterate through all files in mem-fs store
42
- // mem-fs stores files with absolute paths as keys
43
- const filesToMove = [];
44
- fs.store.each((file) => {
45
- const filePath = file.path;
46
- // Check if this file is under the legacy webapp path.
47
- // mem-fs keys use the OS-native separator, so compare with `sep` (not a hardcoded '/')
48
- // or the match fails on Windows and no files are moved.
49
- if (filePath === paths.ffLegacyWebappPath || filePath.startsWith(paths.ffLegacyWebappPath + sep)) {
50
- // Calculate relative path from legacy webapp
51
- const relativePath = filePath === paths.ffLegacyWebappPath
52
- ? ''
53
- : filePath.substring(paths.ffLegacyWebappPath.length + 1);
54
- const newPath = relativePath
55
- ? join(rootPath, DirName.Webapp, relativePath)
56
- : join(rootPath, DirName.Webapp);
57
- filesToMove.push({ oldPath: filePath, newPath });
58
- }
59
- });
60
- // Move files (do this after iteration to avoid modifying during iteration)
61
- for (const { oldPath, newPath } of filesToMove) {
62
- const fileContent = fs.read(oldPath);
63
- fs.write(newPath, fileContent);
64
- fs.delete(oldPath);
65
- }
67
+ moveMemFsDirectory(editor, paths.ffLegacyWebappPath, join(rootPath, DirName.Webapp));
66
68
  }
67
- // Move test/qunit to webapp/test
69
+ // Move test/qunit to webapp/test/qunit
68
70
  if (await exists(paths.ffLegacyTestQunitPath)) {
69
- const filesToMove = [];
70
- fs.store.each((file) => {
71
- const filePath = file.path;
72
- if (filePath === paths.ffLegacyTestQunitPath || filePath.startsWith(paths.ffLegacyTestQunitPath + sep)) {
73
- const relativePath = filePath === paths.ffLegacyTestQunitPath
74
- ? ''
75
- : filePath.substring(paths.ffLegacyTestQunitPath.length + 1);
76
- const newPath = relativePath
77
- ? join(paths.ffNewTestPath, 'qunit', relativePath)
78
- : join(paths.ffNewTestPath, 'qunit');
79
- filesToMove.push({ oldPath: filePath, newPath });
80
- }
81
- });
82
- for (const { oldPath, newPath } of filesToMove) {
83
- const fileContent = fs.read(oldPath);
84
- fs.write(newPath, fileContent);
85
- fs.delete(oldPath);
86
- }
71
+ moveMemFsDirectory(editor, paths.ffLegacyTestQunitPath, join(paths.ffNewTestPath, 'qunit'));
87
72
  }
88
- // Move test/uiveri5 to webapp/test
73
+ // Move test/uiveri5 to webapp/test/uiveri5
89
74
  if (await exists(paths.ffLegacyTestuiveri5Path)) {
90
- const filesToMove = [];
91
- fs.store.each((file) => {
92
- const filePath = file.path;
93
- if (filePath === paths.ffLegacyTestuiveri5Path ||
94
- filePath.startsWith(paths.ffLegacyTestuiveri5Path + sep)) {
95
- const relativePath = filePath === paths.ffLegacyTestuiveri5Path
96
- ? ''
97
- : filePath.substring(paths.ffLegacyTestuiveri5Path.length + 1);
98
- const newPath = relativePath
99
- ? join(paths.ffNewTestPath, 'uiveri5', relativePath)
100
- : join(paths.ffNewTestPath, 'uiveri5');
101
- filesToMove.push({ oldPath: filePath, newPath });
102
- }
103
- });
104
- for (const { oldPath, newPath } of filesToMove) {
105
- const fileContent = fs.read(oldPath);
106
- fs.write(newPath, fileContent);
107
- fs.delete(oldPath);
108
- }
75
+ moveMemFsDirectory(editor, paths.ffLegacyTestuiveri5Path, join(paths.ffNewTestPath, 'uiveri5'));
109
76
  }
110
77
  }
111
78
  /**
@@ -1,13 +1,13 @@
1
1
  import type { ImportProjectInfo } from '../types.js';
2
2
  /**
3
3
  * Sets up necessary directories for migration
4
- * Creates localService directory if data source exists, and test directory if webapp exists
4
+ * Note: mem-fs handles directory creation implicitly when files are written
5
5
  *
6
- * @param projectInfo - Project information
7
- * @param rootPath - Root path of the project
8
- * @param hasDataSource - Whether project has a data source
6
+ * @param _projectInfo - Project information (unused - kept for API compatibility)
7
+ * @param _rootPath - Root path of the project (unused - kept for API compatibility)
8
+ * @param _hasDataSource - Whether project has a data source (unused - kept for API compatibility)
9
9
  */
10
- export declare function setupMigrationDirectories(projectInfo: ImportProjectInfo, rootPath: string, hasDataSource: boolean): Promise<void>;
10
+ export declare function setupMigrationDirectories(_projectInfo: ImportProjectInfo, _rootPath: string, _hasDataSource: boolean): Promise<void>;
11
11
  /**
12
12
  * Removes package-lock.json if it exists
13
13
  * Package-lock is regenerated during installation
@@ -1,25 +1,16 @@
1
1
  import { join } from 'node:path';
2
- import { DirName } from '../project-spec-types.js';
3
- import { TemplateFileName } from '../index.js';
4
- import { createDirectory, fileExists, deleteFile } from '../utils/index.js';
2
+ import { fileExists, deleteFile } from '../utils/index.js';
5
3
  /**
6
4
  * Sets up necessary directories for migration
7
- * Creates localService directory if data source exists, and test directory if webapp exists
5
+ * Note: mem-fs handles directory creation implicitly when files are written
8
6
  *
9
- * @param projectInfo - Project information
10
- * @param rootPath - Root path of the project
11
- * @param hasDataSource - Whether project has a data source
7
+ * @param _projectInfo - Project information (unused - kept for API compatibility)
8
+ * @param _rootPath - Root path of the project (unused - kept for API compatibility)
9
+ * @param _hasDataSource - Whether project has a data source (unused - kept for API compatibility)
12
10
  */
13
- export async function setupMigrationDirectories(projectInfo, rootPath, hasDataSource) {
14
- // Create localService directory if project has a data source
15
- if (hasDataSource) {
16
- createDirectory(join(rootPath, projectInfo.webappPath, DirName.LocalService));
17
- }
18
- // Create test directory if webapp exists
19
- const webAppPath = join(rootPath, projectInfo.webappPath);
20
- if (await fileExists(webAppPath)) {
21
- createDirectory(join(rootPath, projectInfo.webappPath, TemplateFileName.Test));
22
- }
11
+ export async function setupMigrationDirectories(_projectInfo, _rootPath, _hasDataSource) {
12
+ // mem-fs handles directory creation implicitly when files are written to paths
13
+ // No explicit directory creation needed
23
14
  }
24
15
  /**
25
16
  * Removes package-lock.json if it exists
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { join } from 'node:path';
5
5
  import { TemplateFileName } from '../index.js';
6
- import { fileExists, readFile } from '../utils/index.js';
6
+ import { readFile, fileExists } from '../utils/index.js';
7
7
  import { xmlToJson } from '../utils/service.js';
8
8
  import { isAppFreestyle } from '../utils/common.js';
9
9
  /**
@@ -11,7 +11,7 @@ export declare const v4MockServerDep: {
11
11
  };
12
12
  export { packageJsonTmplName, propertyEditorTaskProjectTypes, getBaseTemplatesMap, getFLPSandboxTemplatesMap, getFLPSandboxMockServerTemplatesMap, getCommonV4TemplatesMap, getSapTemplatesMap, getFeTemplatesMap, getAdaptationTemplatesMap, getLibraryTemplatesMap, getFFTestSuiteMap } from './template-maps.js';
13
13
  export { findAllWebIDEProjectFolders, getWebIDEProjectPathsAsLabels, findProjectsByManifest } from './project-discovery.js';
14
- export { stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists, doesPropertyExist, createDirectory } from './file-system-utils.js';
14
+ export { normalizePath, stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './file-system-utils.js';
15
15
  export { getUI5Version, checkManifestUI5Version, readManifest, isAppFreestyle, isGenerateIndex } from './manifest-and-version-utils.js';
16
16
  export { generateSapLibsStr, MigrationError, determineMessage, generateTemplate, getSourceTemplate, generateToolsId, determineSapUxLayer, buildUrlParam } from './migration-utils.js';
17
17
  //# sourceMappingURL=common.d.ts.map
@@ -19,7 +19,7 @@ export { packageJsonTmplName, propertyEditorTaskProjectTypes, getBaseTemplatesMa
19
19
  // Re-export from project-discovery
20
20
  export { findAllWebIDEProjectFolders, getWebIDEProjectPathsAsLabels, findProjectsByManifest } from './project-discovery.js';
21
21
  // Re-export from file-system-utils
22
- export { stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists, doesPropertyExist, createDirectory } from './file-system-utils.js';
22
+ export { normalizePath, stripSpaces, escapeSingleQuotes, escapeDoubleQuotes, doesDirectoryExists } from './file-system-utils.js';
23
23
  // Re-export from manifest-and-version-utils
24
24
  export { getUI5Version, checkManifestUI5Version, readManifest, isAppFreestyle, isGenerateIndex } from './manifest-and-version-utils.js';
25
25
  // Re-export from migration-utils
@@ -12,7 +12,7 @@ export declare function readFile(pathOrFs: string | Editor, path?: string): stri
12
12
  *
13
13
  * @param pathOrFs - Path to file, or Editor instance
14
14
  * @param path - Path to file (if first param is Editor)
15
- * @returns Parsed JSON object with indentation metadata for round-trip preservation
15
+ * @returns Parsed JSON object
16
16
  */
17
17
  export declare function readJSON<T = any>(pathOrFs: string | Editor, path?: string): Promise<T>;
18
18
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
  // @ts-expect-error - no type definitions available
9
9
  import parseJson from 'json-parse-even-better-errors';
10
- import { readFile as fsReadFile } from 'node:fs/promises';
10
+ import { readFile as fsReadFile, access } from 'node:fs/promises';
11
11
  import { editorHasPath, getCurrentEditor, exists as fsAdapterExists, getEditor } from './fs-adapter.js';
12
12
  /**
13
13
  * Read a text file
@@ -26,7 +26,7 @@ export function readFile(pathOrFs, path) {
26
26
  *
27
27
  * @param pathOrFs - Path to file, or Editor instance
28
28
  * @param path - Path to file (if first param is Editor)
29
- * @returns Parsed JSON object with indentation metadata for round-trip preservation
29
+ * @returns Parsed JSON object
30
30
  */
31
31
  export async function readJSON(pathOrFs, path) {
32
32
  const filePath = typeof pathOrFs === 'string' ? pathOrFs : path;
@@ -40,23 +40,7 @@ export async function readJSON(pathOrFs, path) {
40
40
  const fs = getEditor(pathOrFs);
41
41
  content = fs.read(filePath);
42
42
  }
43
- // Parse with JSON.parse for consistent SyntaxError behavior
44
- const result = JSON.parse(content);
45
- // Also parse with parseJson to extract indentation metadata
46
- // This is needed for updateJSON to preserve formatting
47
- try {
48
- const resultWithIndent = parseJson(content);
49
- // Copy indent metadata to the result object
50
- const indent = Symbol.for('indent');
51
- if (resultWithIndent[indent]) {
52
- result[indent] = resultWithIndent[indent];
53
- }
54
- }
55
- catch (error) {
56
- // If parseJson fails, we still have the result from JSON.parse
57
- // updateJSON will use default 4-space indentation
58
- }
59
- return result;
43
+ return JSON.parse(content);
60
44
  }
61
45
  /**
62
46
  * Check if a file exists
@@ -66,15 +50,24 @@ export async function readJSON(pathOrFs, path) {
66
50
  * @returns true if file exists, false otherwise
67
51
  */
68
52
  export async function fileExists(pathOrFs, path) {
69
- const editor = getCurrentEditor();
70
- const filePath = typeof pathOrFs === 'string' ? pathOrFs : path;
71
- // When called with just a path string and no editor context, check filesystem directly
72
- if (typeof pathOrFs === 'string' && !editor) {
53
+ // String path: delegate to the single shared existence check (mem-fs staged OR real fs).
54
+ if (typeof pathOrFs === 'string') {
73
55
  return fsAdapterExists(pathOrFs);
74
56
  }
75
- // Otherwise use mem-fs editor
76
- const fs = getEditor(pathOrFs);
77
- return editorHasPath(fs, filePath);
57
+ // Explicit editor argument: check that editor's staged files, then fall back to real fs.
58
+ // The real-fs fallback prevents overwriting files not yet loaded into the given editor
59
+ // (e.g., the mockserver.js overwrite bug).
60
+ const filePath = path;
61
+ if (editorHasPath(pathOrFs, filePath)) {
62
+ return true;
63
+ }
64
+ try {
65
+ await access(filePath);
66
+ return true;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
78
71
  }
79
72
  /**
80
73
  * Write a text file
@@ -8,6 +8,7 @@ import fastGlob from 'fast-glob';
8
8
  import { dirname, basename, join, normalize } from 'node:path';
9
9
  import { access } from 'node:fs/promises';
10
10
  import { readJSON } from './file-access.js';
11
+ import { normalizePath } from './file-system-utils.js';
11
12
  /**
12
13
  * Reuse library type enum
13
14
  * Matches @sap/ux-project-access ReuseLibType
@@ -92,8 +93,8 @@ export async function findAllProjectRoots(paths, sapuxRequired = false) {
92
93
  async function findLibraryProjectRoot(manifestPath, workspaceBoundary) {
93
94
  let current = dirname(manifestPath);
94
95
  // Normalize paths for comparison
95
- const normalizedBoundary = workspaceBoundary.replaceAll('\\', '/');
96
- const normalizedCurrent = (path) => path.replaceAll('\\', '/');
96
+ const normalizedBoundary = normalizePath(workspaceBoundary);
97
+ const normalizedCurrent = (path) => normalizePath(path);
97
98
  // Helper to check if a file/directory exists using Node.js fs API
98
99
  // (not mem-fs, since project markers are real filesystem entries)
99
100
  const pathExists = async (path) => {
@@ -1,4 +1,12 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
+ /**
3
+ * Normalize path separators to forward slashes for cross-platform compatibility
4
+ * mem-fs and many tools expect forward slashes regardless of OS
5
+ *
6
+ * @param path - Path to normalize
7
+ * @returns Path with forward slashes
8
+ */
9
+ export declare const normalizePath: (path: string) => string;
2
10
  /**
3
11
  * stripSpaces
4
12
  *
@@ -25,19 +33,4 @@ export declare const escapeDoubleQuotes: (s: string) => string;
25
33
  * @param directory - Directory path (if first param is Editor)
26
34
  */
27
35
  export declare function doesDirectoryExists(directoryOrFs: string | Editor, directory?: string): boolean;
28
- /**
29
- * Check if property exists on object
30
- *
31
- * @param obj
32
- * @param fieldName
33
- */
34
- export declare function doesPropertyExist(obj: unknown, fieldName: string): boolean;
35
- /**
36
- * Create directory if it doesn't exist
37
- * Note: mem-fs handles directories implicitly, so this is a no-op
38
- *
39
- * @param directoryOrFs - Directory path or Editor instance
40
- * @param directory - Directory path (if first param is Editor)
41
- */
42
- export declare function createDirectory(directoryOrFs: string | Editor, directory?: string): boolean;
43
36
  //# sourceMappingURL=file-system-utils.d.ts.map
@@ -1,4 +1,12 @@
1
1
  import { editorHasPath, getEditor } from './fs-adapter.js';
2
+ /**
3
+ * Normalize path separators to forward slashes for cross-platform compatibility
4
+ * mem-fs and many tools expect forward slashes regardless of OS
5
+ *
6
+ * @param path - Path to normalize
7
+ * @returns Path with forward slashes
8
+ */
9
+ export const normalizePath = (path) => path.replaceAll('\\', '/');
2
10
  /**
3
11
  * stripSpaces
4
12
  *
@@ -30,26 +38,4 @@ export function doesDirectoryExists(directoryOrFs, directory) {
30
38
  // Use editorHasPath instead of fs.exists() to avoid side effect of staging paths
31
39
  return editorHasPath(fs, dir);
32
40
  }
33
- /**
34
- * Check if property exists on object
35
- *
36
- * @param obj
37
- * @param fieldName
38
- */
39
- export function doesPropertyExist(obj, fieldName) {
40
- return Object.hasOwn(obj, fieldName);
41
- }
42
- /**
43
- * Create directory if it doesn't exist
44
- * Note: mem-fs handles directories implicitly, so this is a no-op
45
- *
46
- * @param directoryOrFs - Directory path or Editor instance
47
- * @param directory - Directory path (if first param is Editor)
48
- */
49
- export function createDirectory(directoryOrFs, directory) {
50
- // mem-fs handles directories implicitly when writing files
51
- // Check if it exists for consistency
52
- const exists = doesDirectoryExists(directoryOrFs, directory);
53
- return !exists; // Return true if it didn't exist (was "created")
54
- }
55
41
  //# sourceMappingURL=file-system-utils.js.map
@@ -28,6 +28,17 @@ export declare function createMemFsEditor(): Editor;
28
28
  * @returns Result of the callback
29
29
  */
30
30
  export declare function runWithEditor<T>(editor: Editor, callback: () => T | Promise<T>): T | Promise<T>;
31
+ /**
32
+ * Load every file under a project folder into the mem-fs store.
33
+ *
34
+ * Pulling files into the store makes existence checks that scan the store (editorHasPath)
35
+ * see the real project tree, so later writes do not overwrite untracked files. Loading via
36
+ * the store's get() does not mark files as modified, so commit() still skips them.
37
+ *
38
+ * @param editor - Mem-fs editor whose store should be populated
39
+ * @param projectRoot - Absolute path to the project folder to load
40
+ */
41
+ export declare function loadProjectIntoEditor(editor: Editor, projectRoot: string): Promise<void>;
31
42
  /**
32
43
  * Get the current editor from context
33
44
  *
@@ -51,11 +62,14 @@ export declare function isMemFsEnabled(): boolean;
51
62
  */
52
63
  export declare function editorHasPath(editor: Editor, path: string): boolean;
53
64
  /**
54
- * Check if file/directory exists (async)
55
- * Uses editor from context if available, otherwise checks real filesystem
65
+ * Check if a file/directory exists (async).
66
+ *
67
+ * When an editor is in context, the migration tree is already loaded into mem-fs, so a staged
68
+ * path is authoritative. A real-filesystem check is still used as a fallback so files that were
69
+ * never loaded into mem-fs are not reported missing (and therefore not silently overwritten).
56
70
  *
57
71
  * @param path - Path to check
58
- * @returns True if exists in mem-fs (when editor available) OR on real filesystem
72
+ * @returns True if staged in mem-fs OR present on the real filesystem
59
73
  */
60
74
  export declare function exists(path: string): Promise<boolean>;
61
75
  /**
@@ -2,6 +2,8 @@ import { create as createMemFs } from 'mem-fs';
2
2
  import { create as createEditor } from 'mem-fs-editor';
3
3
  import { access } from 'node:fs/promises';
4
4
  import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import fastGlob from 'fast-glob';
6
+ import { normalizePath } from './file-system-utils.js';
5
7
  /**
6
8
  * AsyncLocalStorage for tracking the current migration's editor
7
9
  */
@@ -42,6 +44,32 @@ export function createMemFsEditor() {
42
44
  export function runWithEditor(editor, callback) {
43
45
  return editorContext.run(editor, callback);
44
46
  }
47
+ /**
48
+ * Load every file under a project folder into the mem-fs store.
49
+ *
50
+ * Pulling files into the store makes existence checks that scan the store (editorHasPath)
51
+ * see the real project tree, so later writes do not overwrite untracked files. Loading via
52
+ * the store's get() does not mark files as modified, so commit() still skips them.
53
+ *
54
+ * @param editor - Mem-fs editor whose store should be populated
55
+ * @param projectRoot - Absolute path to the project folder to load
56
+ */
57
+ export async function loadProjectIntoEditor(editor, projectRoot) {
58
+ const files = await fastGlob('**/*', {
59
+ cwd: projectRoot,
60
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
61
+ absolute: true,
62
+ onlyFiles: true,
63
+ dot: true
64
+ });
65
+ const store = editor.store;
66
+ for (const file of files) {
67
+ // get() lazily reads the file into the store without marking it modified.
68
+ // The store resolves and keys the path itself; editorHasPath normalizes separators
69
+ // at comparison time, so both sides stay consistent cross-platform.
70
+ store.get(file);
71
+ }
72
+ }
45
73
  /**
46
74
  * Get the current editor from context
47
75
  *
@@ -70,7 +98,7 @@ export function isMemFsEnabled() {
70
98
  export function editorHasPath(editor, path) {
71
99
  // Normalize path separators to forward slashes for consistent comparison
72
100
  // mem-fs stores all paths with forward slashes regardless of OS
73
- const normalizedPath = path.replaceAll('\\', '/');
101
+ const normalizedPath = normalizePath(path);
74
102
  const directoryPrefix = normalizedPath.endsWith('/') ? normalizedPath : normalizedPath + '/';
75
103
  let found = false;
76
104
  editor.store.each((file) => {
@@ -78,7 +106,7 @@ export function editorHasPath(editor, path) {
78
106
  return;
79
107
  }
80
108
  // Normalize file path as well for comparison
81
- const normalizedFilePath = file.path.replaceAll('\\', '/');
109
+ const normalizedFilePath = normalizePath(file.path);
82
110
  // Check exact match OR files under this directory
83
111
  if (normalizedFilePath === normalizedPath || normalizedFilePath.startsWith(directoryPrefix)) {
84
112
  found = true;
@@ -87,19 +115,20 @@ export function editorHasPath(editor, path) {
87
115
  return found;
88
116
  }
89
117
  /**
90
- * Check if file/directory exists (async)
91
- * Uses editor from context if available, otherwise checks real filesystem
118
+ * Check if a file/directory exists (async).
119
+ *
120
+ * When an editor is in context, the migration tree is already loaded into mem-fs, so a staged
121
+ * path is authoritative. A real-filesystem check is still used as a fallback so files that were
122
+ * never loaded into mem-fs are not reported missing (and therefore not silently overwritten).
92
123
  *
93
124
  * @param path - Path to check
94
- * @returns True if exists in mem-fs (when editor available) OR on real filesystem
125
+ * @returns True if staged in mem-fs OR present on the real filesystem
95
126
  */
96
127
  export async function exists(path) {
97
128
  const editor = getCurrentEditor();
98
- if (editor) {
99
- // In mem-fs mode, check mem-fs only
100
- return editorHasPath(editor, path);
129
+ if (editor && editorHasPath(editor, path)) {
130
+ return true;
101
131
  }
102
- // No editor context - check real filesystem
103
132
  try {
104
133
  await access(path);
105
134
  return true;
@@ -2,7 +2,6 @@ export * from './common.js';
2
2
  export * from './UI5Theme.js';
3
3
  export { validateMetadata } from './service.js';
4
4
  export { determineMessage } from './migration-utils.js';
5
- export { createDirectory } from './file-system-utils.js';
6
5
  export { readFile, readJSON, fileExists, writeFile, updateFile, updateJSON, deleteFile } from './file-access.js';
7
6
  export { findAllProjectRoots, getReuseLibs, ReuseLibType } from './file-discovery.js';
8
7
  export { findProjectRoot, getProjectType, getMainService, hasDependency, getMinUI5VersionAsArray, type CdsUi5PluginInfo } from '@sap-ux/project-access';
@@ -8,8 +8,6 @@ export * from './UI5Theme.js';
8
8
  export { validateMetadata } from './service.js';
9
9
  // Migration utilities
10
10
  export { determineMessage } from './migration-utils.js';
11
- // File system utilities
12
- export { createDirectory } from './file-system-utils.js';
13
11
  // Re-export native file access to replace @sap/ux-project-access file I/O
14
12
  export { readFile, readJSON, fileExists, writeFile, updateFile, updateJSON, deleteFile } from './file-access.js';
15
13
  // Re-export native file discovery to replace @sap/ux-project-access
@@ -4,7 +4,7 @@ import { readPackageUpAsync } from './read-package-up-compat.js';
4
4
  import { v4 as uuidV4 } from 'uuid';
5
5
  import { i18nText } from '../i18n.js';
6
6
  import { writeFile } from './file-access.js';
7
- import { createDirectory } from './file-system-utils.js';
7
+ import uniq from 'lodash/uniq.js';
8
8
  // Get current directory using import.meta.url (ESM)
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  // Matches strings starting with "sap" or containing ".sap" (e.g., "sap.m", "com.sap.ui")
@@ -17,14 +17,12 @@ const sapLibsRegexFilter = /(?:^sap|\.sap)/;
17
17
  * @param libsStrInput
18
18
  */
19
19
  export function generateSapLibsStr(manifestLibs, libsStrInput) {
20
- let libStrOut = '';
21
20
  const mlibs = Object.keys(manifestLibs || {});
22
21
  // remove spaces and split at comma
23
22
  const strlibs = libsStrInput?.replaceAll(' ', '').split(',') || [];
24
23
  const libs = mlibs.concat(strlibs);
25
- const uniquelibs = libs.filter((v, i, a) => a.indexOf(v) === i);
26
- libStrOut = uniquelibs.filter((ele) => sapLibsRegexFilter.test(ele)).join(', ');
27
- return libStrOut;
24
+ const uniquelibs = uniq(libs);
25
+ return uniquelibs.filter((ele) => sapLibsRegexFilter.test(ele)).join(', ');
28
26
  }
29
27
  /**
30
28
  * Custom error class for migration errors
@@ -82,14 +80,12 @@ export async function generateTemplate(templateData, projectRoot, templateRoot,
82
80
  const { fullTemplateName, targetFile } = resolveTemplatePaths(projectRoot, templateRoot, templateName, templateProps);
83
81
  // 2. Resolve template data
84
82
  const resolvedData = resolveTemplateData(templateData, templateProps);
85
- // 3. Ensure target directory exists (uses fs-adapter)
86
- createDirectory(dirname(targetFile));
87
- // 4. Render template
83
+ // 3. Render template (mem-fs handles directory creation implicitly)
88
84
  const { isRendered = true, opts = {} } = templateProps;
89
85
  let content = renderTemplate(templateRoot, fullTemplateName, resolvedData, opts, isRendered);
90
- // 5. Apply file-specific handlers
86
+ // 4. Apply file-specific handlers
91
87
  content = await applyFileSpecificHandlers(templateName, content, projectRoot, targetFile, templateData);
92
- // 6. Write file (uses fs-adapter)
88
+ // 5. Write file (mem-fs handles directory creation implicitly)
93
89
  // Do not catch - let errors propagate to caller for proper handling
94
90
  writeFile(targetFile, content);
95
91
  }
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.2",
13
+ "version": "0.0.3",
14
14
  "license": "Apache-2.0",
15
15
  "main": "dist/index.js",
16
16
  "files": [
@@ -25,7 +25,7 @@
25
25
  "ejs": "3.1.10",
26
26
  "fast-glob": "3.3.3",
27
27
  "fast-xml-parser": "5.10.1",
28
- "fs-extra": "11.3.6",
28
+ "fs-extra": "11.4.1",
29
29
  "i18next": "26.3.6",
30
30
  "json-parse-even-better-errors": "5.0.0",
31
31
  "lodash": "4.18.1",
@@ -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.64",
41
- "@sap-ux/axios-extension": "2.1.1",
42
- "@sap-ux/i18n": "1.0.5",
43
- "@sap-ux/btp-utils": "2.0.6",
44
- "@sap-ux/nodejs-utils": "1.0.10",
45
- "@sap-ux/launch-config": "1.0.24",
46
- "@sap-ux/project-access": "2.3.0",
40
+ "@sap-ux/axios-extension": "2.1.2",
41
+ "@sap-ux/btp-utils": "2.0.7",
42
+ "@sap-ux/i18n": "1.0.6",
43
+ "@sap-ux/nodejs-utils": "1.0.11",
44
+ "@sap-ux/adp-tooling": "1.0.65",
45
+ "@sap-ux/project-access": "2.3.1",
47
46
  "@sap-ux/store": "2.0.7",
48
- "@sap-ux/ui5-application-writer": "2.1.5",
49
- "@sap-ux/ui5-config": "1.0.9",
50
- "@sap-ux/ui5-info": "1.0.6"
47
+ "@sap-ux/ui5-application-writer": "2.1.6",
48
+ "@sap-ux/ui5-config": "1.0.10",
49
+ "@sap-ux/ui5-info": "1.0.7",
50
+ "@sap-ux/launch-config": "1.0.25"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jest/globals": "30.4.1",
@@ -56,10 +56,10 @@
56
56
  "@types/lodash": "4.17.24",
57
57
  "@types/mem-fs": "1.1.2",
58
58
  "@types/mem-fs-editor": "7.0.1",
59
- "@types/semver": "7.7.1",
59
+ "@types/semver": "7.8.0",
60
60
  "jest-specific-snapshot": "^8.0.0",
61
61
  "rimraf": "6.1.3",
62
- "@sap-ux/eslint-plugin-fiori-tools": "10.17.1"
62
+ "@sap-ux/eslint-plugin-fiori-tools": "10.17.2"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22.x"