@microsoft/power-apps-native-host 0.2.22 → 0.3.0

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.
Files changed (156) hide show
  1. package/README.md +162 -26
  2. package/compatibility/current.json +87 -0
  3. package/lib/NOTICE.txt +140 -1226
  4. package/lib/commonjs/auth/AuthContext.js +74 -18
  5. package/lib/commonjs/auth/AuthContext.web.js +32 -0
  6. package/lib/commonjs/auth/completePowerAppsAuthSession.js +16 -0
  7. package/lib/commonjs/bridge/NativePowerAppsBridge.js +20 -17
  8. package/lib/commonjs/bridge/PowerPagesBridge.js +157 -0
  9. package/lib/commonjs/bridge/bridgeState.js +40 -0
  10. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  11. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  12. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  13. package/lib/commonjs/connections/connectionState.js +96 -0
  14. package/lib/commonjs/connections/disambiguate.js +19 -11
  15. package/lib/commonjs/connections/resolveConnections.js +347 -0
  16. package/lib/commonjs/connections/types.js +106 -0
  17. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +128 -65
  18. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  19. package/lib/commonjs/controls/FilePicker.js +6 -0
  20. package/lib/commonjs/controls/ImagePicker.js +8 -0
  21. package/lib/commonjs/index.js +188 -6
  22. package/lib/commonjs/logger.js +9 -6
  23. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  24. package/lib/commonjs/offline/useOfflineIntegration.js +12 -0
  25. package/lib/commonjs/offline/useOfflineIntegration.web.js +11 -0
  26. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +46 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  28. package/lib/commonjs/platform/DataverseDataSources.js +14 -0
  29. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +49 -0
  30. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  31. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  32. package/lib/commonjs/provider/PowerAppsHostProvider.js +156 -25
  33. package/lib/commonjs/provider/PowerAppsHostProvider.web.js +41 -0
  34. package/lib/commonjs/provider/PowerAppsProvider.js +229 -133
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +216 -0
  36. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  37. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  38. package/lib/commonjs/telemetry/index.js +22 -3
  39. package/lib/module/auth/AuthContext.js +74 -18
  40. package/lib/module/auth/AuthContext.web.js +26 -0
  41. package/lib/module/auth/completePowerAppsAuthSession.js +11 -0
  42. package/lib/module/bridge/NativePowerAppsBridge.js +21 -18
  43. package/lib/module/bridge/PowerPagesBridge.js +151 -0
  44. package/lib/module/bridge/bridgeState.js +34 -0
  45. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  46. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  47. package/lib/module/connections/connectionMetadata.js +112 -0
  48. package/lib/module/connections/connectionState.js +82 -0
  49. package/lib/module/connections/disambiguate.js +19 -12
  50. package/lib/module/connections/resolveConnections.js +342 -0
  51. package/lib/module/connections/types.js +91 -0
  52. package/lib/module/{platform → connections}/useConnectionRefs.js +129 -66
  53. package/lib/module/connections/useConnectionSetup.js +113 -54
  54. package/lib/module/controls/FilePicker.js +6 -0
  55. package/lib/module/controls/ImagePicker.js +8 -0
  56. package/lib/module/index.js +14 -6
  57. package/lib/module/logger.js +8 -6
  58. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  59. package/lib/module/offline/useOfflineIntegration.js +3 -0
  60. package/lib/module/offline/useOfflineIntegration.web.js +7 -0
  61. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +42 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  63. package/lib/module/platform/DataverseDataSources.js +10 -0
  64. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +45 -0
  65. package/lib/module/platform/GlobalDiscovery.js +45 -0
  66. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  67. package/lib/module/provider/PowerAppsHostProvider.js +159 -28
  68. package/lib/module/provider/PowerAppsHostProvider.web.js +36 -0
  69. package/lib/module/provider/PowerAppsProvider.js +233 -137
  70. package/lib/module/provider/SessionDetailsGesture.js +211 -0
  71. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  72. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  73. package/lib/module/telemetry/index.js +15 -4
  74. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  75. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +21 -0
  76. package/lib/typescript/commonjs/auth/completePowerAppsAuthSession.d.ts +8 -0
  77. package/lib/typescript/commonjs/bridge/PowerPagesBridge.d.ts +18 -0
  78. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  79. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  80. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  81. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  82. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  83. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  84. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +19 -19
  85. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  86. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +33 -24
  87. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  88. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  89. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  90. package/lib/typescript/commonjs/index.d.ts +20 -10
  91. package/lib/typescript/commonjs/logger.d.ts +4 -4
  92. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  93. package/lib/typescript/commonjs/offline/useOfflineIntegration.d.ts +2 -0
  94. package/lib/typescript/commonjs/offline/useOfflineIntegration.web.d.ts +4 -0
  95. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  97. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +27 -0
  98. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  99. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  100. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  101. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.web.d.ts +5 -0
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +32 -10
  104. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  105. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  106. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  107. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  108. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  109. package/lib/typescript/module/auth/AuthContext.web.d.ts +21 -0
  110. package/lib/typescript/module/auth/completePowerAppsAuthSession.d.ts +8 -0
  111. package/lib/typescript/module/bridge/PowerPagesBridge.d.ts +18 -0
  112. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  113. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  114. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  115. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  116. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  117. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  118. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +19 -19
  119. package/lib/typescript/module/connections/types.d.ts +129 -1
  120. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +33 -24
  121. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  122. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  123. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  124. package/lib/typescript/module/index.d.ts +24 -14
  125. package/lib/typescript/module/logger.d.ts +4 -4
  126. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  127. package/lib/typescript/module/offline/useOfflineIntegration.d.ts +2 -0
  128. package/lib/typescript/module/offline/useOfflineIntegration.web.d.ts +4 -0
  129. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  131. package/lib/typescript/module/platform/DataverseDataSources.d.ts +27 -0
  132. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  133. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  134. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  135. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.web.d.ts +5 -0
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +34 -12
  138. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  139. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  140. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  141. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  142. package/package.json +43 -18
  143. package/scripts/build-ios-base.js +66 -27
  144. package/scripts/bundle.js +212 -5
  145. package/scripts/generate-connector-schemas.js +8 -6
  146. package/scripts/metro-logger.js +213 -0
  147. package/scripts/start-web.mjs +137 -0
  148. package/scripts/upgrade/runtime.js +463 -0
  149. package/scripts/upgrade/system.js +244 -0
  150. package/scripts/upgrade-authoring.js +534 -0
  151. package/scripts/upgrade.js +188 -0
  152. package/scripts/wrap.js +536 -0
  153. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  154. package/lib/commonjs/platform/resolveConnections.js +0 -264
  155. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  156. package/lib/module/platform/resolveConnections.js +0 -259
@@ -0,0 +1,463 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { applyPatch, formatPatch, parsePatch, reversePatch } = require('diff');
7
+
8
+ const HOST_PACKAGE = '@microsoft/power-apps-native-host';
9
+ const TEMPLATE_VERSION_PATH = 'app.json';
10
+ const TSCONFIG_PATH = 'tsconfig.json';
11
+ const TEMPLATE_MANAGED_PATHS = [
12
+ TEMPLATE_VERSION_PATH,
13
+ 'app.config.js',
14
+ 'package-lock.json',
15
+ 'package.json',
16
+ TSCONFIG_PATH,
17
+ ];
18
+ const UNIFIED_PATCH_MANAGED_PATHS = ['app.config.js'];
19
+
20
+ function isTemplateManagedPath(relativePath) {
21
+ return TEMPLATE_MANAGED_PATHS.includes(relativePath);
22
+ }
23
+
24
+ function isUnifiedPatchManagedPath(relativePath) {
25
+ return UNIFIED_PATCH_MANAGED_PATHS.includes(relativePath);
26
+ }
27
+
28
+ function isCustomerOwnedPath(relativePath) {
29
+ return !isTemplateManagedPath(relativePath);
30
+ }
31
+
32
+ function readJson(filePath) {
33
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
+ }
35
+
36
+ function hashContent(content) {
37
+ return content === null ? null : crypto.createHash('sha256').update(content).digest('hex');
38
+ }
39
+
40
+ function assertSafeFilePath(projectRoot, filePath) {
41
+ const root = path.resolve(projectRoot);
42
+ if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) {
43
+ throw new Error(`Patch path escapes project root: ${filePath}`);
44
+ }
45
+ let current = root;
46
+ for (const segment of path.relative(root, filePath).split(path.sep)) {
47
+ current = path.join(current, segment);
48
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
49
+ throw new Error(`Migration cannot modify symbolic link path: ${path.relative(root, current)}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ function parseFileMode(mode) {
55
+ if (mode === null || mode === undefined) return null;
56
+ const serialized = String(mode);
57
+ if (!/^(?:(?:100|000)?(?:644|755))$/.test(serialized)) throw new Error(`Invalid migration file mode: ${mode}`);
58
+ return Number.parseInt(serialized.slice(-3), 8);
59
+ }
60
+
61
+ function isHashOrNull(value) {
62
+ return value === null || typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
63
+ }
64
+
65
+ function jsonFormatting(source) {
66
+ const indentMatch = source.match(/\n([\t ]+)\S/);
67
+ return {
68
+ indent: indentMatch ? indentMatch[1] : ' ',
69
+ newline: source.includes('\r\n') ? '\r\n' : '\n',
70
+ };
71
+ }
72
+
73
+ function stringifyJson(value, source) {
74
+ const { indent, newline } = jsonFormatting(source);
75
+ return `${JSON.stringify(value, null, indent).replace(/\n/g, newline)}${newline}`;
76
+ }
77
+
78
+ function setManagedValues(target, values, section, changes) {
79
+ target[section] ||= {};
80
+ for (const [name, version] of Object.entries(values)) {
81
+ const otherSections = section === 'dependencies'
82
+ ? ['devDependencies', 'optionalDependencies']
83
+ : section === 'devDependencies' ? ['dependencies', 'optionalDependencies'] : [];
84
+ for (const otherSection of otherSections) {
85
+ if (target[otherSection]?.[name] !== undefined) {
86
+ changes.push(`move ${name} from ${otherSection} to ${section}`);
87
+ delete target[otherSection][name];
88
+ }
89
+ }
90
+ if (target[section][name] !== version) {
91
+ changes.push(`${section}.${name}: ${target[section][name] || '(missing)'} -> ${version}`);
92
+ target[section][name] = version;
93
+ }
94
+ }
95
+ }
96
+
97
+ function sameValue(left, right) {
98
+ return JSON.stringify(left) === JSON.stringify(right);
99
+ }
100
+
101
+ function formatValue(value) {
102
+ return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
103
+ }
104
+
105
+ function applyPackageTransitions(packageJson, transitions, changes, warnings) {
106
+ const blockedMoves = new Set();
107
+ for (const section of ['dependencies', 'devDependencies', 'overrides']) {
108
+ for (const [name, transition] of Object.entries(transitions?.[section] || {})) {
109
+ const current = packageJson[section]?.[name];
110
+ if (transition.to === null && current !== undefined && !sameValue(current, transition.from)) blockedMoves.add(name);
111
+ }
112
+ }
113
+ for (const section of ['dependencies', 'devDependencies', 'overrides']) {
114
+ packageJson[section] ||= {};
115
+ for (const [name, transition] of Object.entries(transitions?.[section] || {})) {
116
+ const current = packageJson[section][name];
117
+ if (transition.to === null) {
118
+ if (sameValue(current, transition.from)) {
119
+ changes.push(`remove ${section}.${name}`);
120
+ delete packageJson[section][name];
121
+ } else if (current !== undefined) warnings.push(`retain customized ${section}.${name}`);
122
+ } else if (transition.from === null && blockedMoves.has(name)) {
123
+ warnings.push(`retain customized placement for ${name}`);
124
+ } else if (current === undefined || sameValue(current, transition.from)) {
125
+ changes.push(`${section}.${name}: ${formatValue(current ?? '(missing)')} -> ${formatValue(transition.to)}`);
126
+ packageJson[section][name] = transition.to;
127
+ } else if (!sameValue(current, transition.to)) warnings.push(`retain customized ${section}.${name}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ function ensureManagedValues(packageJson, values, section, changes, warnings) {
133
+ packageJson[section] ||= {};
134
+ for (const [name, targetValue] of Object.entries(values || {})) {
135
+ const otherSections = section === 'dependencies'
136
+ ? ['devDependencies', 'optionalDependencies']
137
+ : section === 'devDependencies' ? ['dependencies', 'optionalDependencies'] : [];
138
+ const otherSection = otherSections.find(candidate => packageJson[candidate]?.[name] !== undefined);
139
+ if (packageJson[section][name] === undefined && !otherSection) {
140
+ changes.push(`add missing ${section}.${name}: ${formatValue(targetValue)}`);
141
+ packageJson[section][name] = targetValue;
142
+ } else if (otherSection) warnings.push(`retain customized placement ${otherSection}.${name}`);
143
+ else if (!sameValue(packageJson[section][name], targetValue)) warnings.push(`retain customized ${section}.${name}`);
144
+ }
145
+ }
146
+
147
+ function patchPackageJson(source, profile, hostVersion, transitions = null) {
148
+ const packageJson = JSON.parse(source);
149
+ const changes = [];
150
+ const warnings = [];
151
+ packageJson.dependencies ||= {};
152
+ setManagedValues(packageJson, { expo: profile.expoVersion }, 'dependencies', changes);
153
+ setManagedValues(packageJson, { [HOST_PACKAGE]: hostVersion }, 'dependencies', changes);
154
+ if (transitions) applyPackageTransitions(packageJson, transitions, changes, warnings);
155
+ ensureManagedValues(packageJson, profile.managedDependencies, 'dependencies', changes, warnings);
156
+ ensureManagedValues(packageJson, profile.managedDevDependencies, 'devDependencies', changes, warnings);
157
+ ensureManagedValues(packageJson, profile.managedOverrides, 'overrides', changes, warnings);
158
+ return {
159
+ changed: changes.length > 0,
160
+ changes,
161
+ warnings,
162
+ content: changes.length ? stringifyJson(packageJson, source) : source,
163
+ };
164
+ }
165
+
166
+ function applyJsonTransitions(target, transitions, sectionPath, changes, warnings) {
167
+ let section = target;
168
+ for (const segment of sectionPath) {
169
+ section[segment] ||= {};
170
+ section = section[segment];
171
+ }
172
+ const prefix = sectionPath.join('.');
173
+ for (const [name, transition] of Object.entries(transitions || {})) {
174
+ const current = section[name];
175
+ if (transition.to === null) {
176
+ if (sameValue(current, transition.from)) {
177
+ changes.push(`remove ${prefix}.${name}`);
178
+ delete section[name];
179
+ } else if (current !== undefined) warnings.push(`retain customized ${prefix}.${name}`);
180
+ } else if (current === undefined || sameValue(current, transition.from)) {
181
+ changes.push(`${prefix}.${name}: ${formatValue(current ?? '(missing)')} -> ${formatValue(transition.to)}`);
182
+ section[name] = transition.to;
183
+ } else if (!sameValue(current, transition.to)) warnings.push(`retain customized ${prefix}.${name}`);
184
+ }
185
+ }
186
+
187
+ function patchTsconfigJson(source, transitions = null) {
188
+ const tsconfig = JSON.parse(source);
189
+ const changes = [];
190
+ const warnings = [];
191
+ applyJsonTransitions(tsconfig, transitions?.paths, ['compilerOptions', 'paths'], changes, warnings);
192
+ return {
193
+ changed: changes.length > 0,
194
+ changes,
195
+ warnings,
196
+ content: changes.length ? stringifyJson(tsconfig, source) : source,
197
+ };
198
+ }
199
+
200
+ function buildMigrationOperations(projectRoot, migration) {
201
+ if (!migration) return { operations: [], conflicts: [] };
202
+ const operations = [];
203
+ const rejectOperations = [];
204
+ const conflicts = [];
205
+ const patchedPaths = new Set();
206
+ for (const patch of parseMigrationPatch(migration.patch)) {
207
+ const oldPath = patch.oldFileName;
208
+ const newPath = patch.newFileName;
209
+ const relativePath = newPath === '/dev/null' ? oldPath : newPath;
210
+ if (!relativePath || path.isAbsolute(relativePath) || relativePath.split(/[\\/]/).includes('..')) {
211
+ throw new Error(`Migration ${migration.id} contains an unsafe patch path: ${relativePath}`);
212
+ }
213
+ if (!isUnifiedPatchManagedPath(relativePath)) throw new Error(`Migration ${migration.id} cannot modify path ${relativePath} with a unified patch`);
214
+ if (patchedPaths.has(relativePath)) throw new Error(`Migration ${migration.id} patches ${relativePath} more than once`);
215
+ patchedPaths.add(relativePath);
216
+ const filePath = path.resolve(projectRoot, relativePath);
217
+ assertSafeFilePath(projectRoot, filePath);
218
+ const currentValue = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
219
+ const currentMode = fs.existsSync(filePath) ? fs.statSync(filePath).mode & 0o777 : null;
220
+ const currentContent = currentValue || '';
221
+ const isCreate = oldPath === '/dev/null';
222
+ const isDelete = newPath === '/dev/null';
223
+ const metadata = migration.patchFiles?.[relativePath];
224
+ if (!metadata) throw new Error(`Migration ${migration.id} is missing metadata for ${relativePath}`);
225
+ const targetMode = parseFileMode(metadata.toMode);
226
+ if (hashContent(currentValue) === metadata.toHash) {
227
+ addModeOperation(operations, filePath, currentValue, currentMode, targetMode, metadata.toMode, migration.id);
228
+ continue;
229
+ }
230
+ if (isCreate && currentValue !== null) {
231
+ addConflict(rejectOperations, conflicts, filePath, relativePath, migration.id, patch, true);
232
+ continue;
233
+ }
234
+ if (isDelete && currentValue === null) continue;
235
+ const patchedContent = applyPatch(currentContent, patch);
236
+ if (patchedContent === false) {
237
+ const reversedContent = applyPatch(currentContent, reversePatch(patch));
238
+ if (reversedContent !== false && applyPatch(reversedContent, patch) === currentContent) {
239
+ addModeOperation(operations, filePath, currentValue, currentMode, targetMode, metadata.toMode, migration.id);
240
+ continue;
241
+ }
242
+ addConflict(rejectOperations, conflicts, filePath, relativePath, migration.id, patch, false);
243
+ continue;
244
+ }
245
+ if (isDelete && patchedContent !== '') {
246
+ addConflict(rejectOperations, conflicts, filePath, relativePath, migration.id, patch, false);
247
+ continue;
248
+ }
249
+ if (hashContent(currentValue) === metadata.fromHash && hashContent(isDelete ? null : patchedContent) !== metadata.toHash) {
250
+ throw new Error(`Migration ${migration.id} produced an unexpected result for ${relativePath}`);
251
+ }
252
+ operations.push({
253
+ filePath,
254
+ changed: true,
255
+ changes: [`apply unified patch from migration ${migration.id}`],
256
+ content: isDelete ? null : patchedContent,
257
+ delete: isDelete,
258
+ mode: metadata.toMode,
259
+ });
260
+ }
261
+ return conflicts.length ? { operations: rejectOperations, conflicts } : { operations, conflicts: [] };
262
+ }
263
+
264
+ function addModeOperation(operations, filePath, content, currentMode, targetMode, serializedMode, migrationId) {
265
+ if (targetMode !== null && currentMode !== targetMode) {
266
+ operations.push({ filePath, changed: true, changes: [`apply file mode from migration ${migrationId}`], content, mode: serializedMode });
267
+ }
268
+ }
269
+
270
+ function addConflict(operations, conflicts, filePath, relativePath, migrationId, patch, existingFile) {
271
+ conflicts.push(relativePath);
272
+ operations.push({
273
+ filePath: `${filePath}.power-apps-${migrationId}.rej`,
274
+ changed: true,
275
+ changes: [`write rejected migration hunk${existingFile ? ' for existing file' : ' for'} ${relativePath}`],
276
+ content: formatPatch(patch),
277
+ reject: true,
278
+ });
279
+ }
280
+
281
+ function readProjectState(projectRoot, targetVersion, fromVersion) {
282
+ const statePath = path.join(projectRoot, TEMPLATE_VERSION_PATH);
283
+ if (fs.existsSync(statePath)) {
284
+ const state = readJson(statePath).expo?.extra?.powerappsNative;
285
+ if (state) {
286
+ if (state.schemaVersion !== 1) throw new Error(`${TEMPLATE_VERSION_PATH} uses unsupported schemaVersion ${state.schemaVersion}`);
287
+ if (!Number.isInteger(state.templateVersion)) throw new Error(`${TEMPLATE_VERSION_PATH} has an invalid templateVersion`);
288
+ return state;
289
+ }
290
+ }
291
+ if (fromVersion) return { schemaVersion: 1, templateVersion: fromVersion };
292
+ if (targetVersion === 1) return { schemaVersion: 1, templateVersion: 1 };
293
+ throw new Error(`Template state is missing. Rerun with --from-version <version> after identifying the app's source template version.`);
294
+ }
295
+
296
+ function selectNextMigration(migrations, fromVersion, targetVersion) {
297
+ if (fromVersion > targetVersion) throw new Error(`Template downgrade ${fromVersion} -> ${targetVersion} is not supported`);
298
+ if (fromVersion === targetVersion) return null;
299
+ const candidates = migrations.filter(migration => migration.fromTemplateVersion === fromVersion);
300
+ if (candidates.length !== 1) throw new Error(`Expected exactly one migration from template version ${fromVersion}; found ${candidates.length}`);
301
+ const migration = candidates[0];
302
+ if (migration.toTemplateVersion !== fromVersion + 1 || migration.toTemplateVersion > targetVersion) {
303
+ throw new Error(`Migration ${migration.id} has an invalid target template version`);
304
+ }
305
+ return migration;
306
+ }
307
+
308
+ function loadMigrations(profile, migrationsDirectory) {
309
+ if (profile.schemaVersion !== 1) throw new Error(`Compatibility profile uses unsupported schemaVersion ${profile.schemaVersion}`);
310
+ if (!Number.isInteger(profile.targetTemplateVersion) || profile.targetTemplateVersion < 1) throw new Error('Compatibility profile has an invalid targetTemplateVersion');
311
+ const ids = profile.migrations || [];
312
+ if (new Set(ids).size !== ids.length) throw new Error('Compatibility profile contains duplicate migration IDs');
313
+ const migrations = ids.map(migrationId => loadMigration(migrationId, migrationsDirectory));
314
+ validateGraph(migrations, profile.targetTemplateVersion);
315
+ return migrations;
316
+ }
317
+
318
+ function loadMigration(migrationId, migrationsDirectory) {
319
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(migrationId)) throw new Error(`Compatibility profile contains unsafe migration ID: ${migrationId}`);
320
+ const migrationPath = path.join(migrationsDirectory, `${migrationId}.json`);
321
+ if (!fs.existsSync(migrationPath)) throw new Error(`Migration ${migrationId} is missing from the host package`);
322
+ const migration = readJson(migrationPath);
323
+ if (migration.schemaVersion !== 1) throw new Error(`Migration ${migrationId} uses unsupported schemaVersion ${migration.schemaVersion}`);
324
+ if (migration.patchFile !== `${migrationId}.patch`) throw new Error(`Migration ${migrationId} has an unsafe patch filename`);
325
+ const patchPath = path.join(migrationsDirectory, migration.patchFile);
326
+ if (!fs.existsSync(patchPath)) throw new Error(`Patch file ${migration.patchFile} is missing from the host package`);
327
+ const patch = fs.readFileSync(patchPath, 'utf8');
328
+ if (migration.id !== migrationId) throw new Error(`Migration file ${migrationId} has mismatched id ${migration.id}`);
329
+ if (!Number.isInteger(migration.fromTemplateVersion) || !Number.isInteger(migration.toTemplateVersion)) {
330
+ throw new Error(`Migration ${migration.id} is missing template version boundaries`);
331
+ }
332
+ if (!migration.targetProfile?.hostVersion || !migration.targetProfile?.expoVersion || !migration.targetProfile?.expoDoctorVersion) {
333
+ throw new Error(`Migration ${migration.id} has an incomplete target profile`);
334
+ }
335
+ validatePatchMetadata(migration, patch);
336
+ return { ...migration, patch };
337
+ }
338
+
339
+ function validatePatchMetadata(migration, patch) {
340
+ try {
341
+ const parsedPatches = parseMigrationPatch(patch);
342
+ const patchPaths = parsedPatches.map(item => item.newFileName === '/dev/null' ? item.oldFileName : item.newFileName);
343
+ if (new Set(patchPaths).size !== patchPaths.length) throw new Error('duplicate patch paths');
344
+ if (patchPaths.some(filePath => !migration.patchFiles?.[filePath])) throw new Error('patch metadata is incomplete');
345
+ if (Object.keys(migration.patchFiles || {}).some(filePath => !patchPaths.includes(filePath))) throw new Error('patch metadata contains extra files');
346
+ for (const parsedPatch of parsedPatches) {
347
+ const oldPath = parsedPatch.oldFileName;
348
+ const newPath = parsedPatch.newFileName;
349
+ const isCreate = oldPath === '/dev/null';
350
+ const isDelete = newPath === '/dev/null';
351
+ if (isCreate && isDelete || !isCreate && !isDelete && oldPath !== newPath) throw new Error(`unsupported rename patch ${oldPath} -> ${newPath}`);
352
+ const filePath = isDelete ? oldPath : newPath;
353
+ if (!filePath || path.isAbsolute(filePath) || filePath.split(/[\\/]/).includes('..')) throw new Error(`unsafe patch path ${filePath}`);
354
+ if (!isUnifiedPatchManagedPath(filePath)) throw new Error(`unified patch path ${filePath} is not managed by raw patches`);
355
+ const metadata = migration.patchFiles[filePath];
356
+ if (!isHashOrNull(metadata.fromHash) || !isHashOrNull(metadata.toHash)) throw new Error(`invalid hashes for ${filePath}`);
357
+ parseFileMode(metadata.fromMode);
358
+ parseFileMode(metadata.toMode);
359
+ if (isCreate && (metadata.fromHash !== null || metadata.fromMode !== null || metadata.toHash === null || metadata.toMode === null)) {
360
+ throw new Error(`invalid creation metadata for ${filePath}`);
361
+ }
362
+ if (isDelete && (metadata.toHash !== null || metadata.toMode !== null || metadata.fromHash === null || metadata.fromMode === null)) {
363
+ throw new Error(`invalid deletion metadata for ${filePath}`);
364
+ }
365
+ if (!isCreate && !isDelete && (metadata.fromHash === null || metadata.toHash === null || metadata.fromMode === null || metadata.toMode === null)) {
366
+ throw new Error(`invalid update metadata for ${filePath}`);
367
+ }
368
+ }
369
+ } catch (error) {
370
+ throw new Error(`Migration ${migration.id} has an invalid patch: ${error.message}`);
371
+ }
372
+ }
373
+
374
+ function parseMigrationPatch(patch) {
375
+ return patch.trim() ? parsePatch(patch) : [];
376
+ }
377
+
378
+ function validateGraph(migrations, targetVersion) {
379
+ const fromVersions = new Set();
380
+ for (const migration of migrations) {
381
+ if (!Number.isInteger(migration.fromTemplateVersion) || migration.fromTemplateVersion < 1) throw new Error(`Migration ${migration.id} has an invalid source version`);
382
+ if (migration.toTemplateVersion !== migration.fromTemplateVersion + 1) throw new Error(`Migration ${migration.id} is not adjacent`);
383
+ if (fromVersions.has(migration.fromTemplateVersion)) throw new Error(`Multiple migrations start at template version ${migration.fromTemplateVersion}`);
384
+ fromVersions.add(migration.fromTemplateVersion);
385
+ }
386
+ if (migrations.length) {
387
+ const firstVersion = Math.min(...fromVersions);
388
+ for (let version = firstVersion; version < targetVersion; version += 1) {
389
+ if (!fromVersions.has(version)) throw new Error(`Migration graph is missing template version ${version} -> ${version + 1}`);
390
+ }
391
+ if (Math.max(...migrations.map(migration => migration.toTemplateVersion)) !== targetVersion) {
392
+ throw new Error(`Migration graph does not reach target template version ${targetVersion}`);
393
+ }
394
+ }
395
+ }
396
+
397
+ function buildUpgradePlan(projectRoot, profile, hostVersion, fromVersion, migrationsDirectory, includeState = true) {
398
+ const migrations = loadMigrations(profile, migrationsDirectory);
399
+ const state = readProjectState(projectRoot, profile.targetTemplateVersion, fromVersion);
400
+ const migration = selectNextMigration(migrations, state.templateVersion, profile.targetTemplateVersion);
401
+ const targetProfile = migration ? migration.targetProfile : profile;
402
+ const targetHostVersion = migration ? migration.targetProfile.hostVersion : hostVersion;
403
+ const migrationPlan = buildMigrationOperations(projectRoot, migration);
404
+ if (migrationPlan.conflicts.length) return migrationPlan;
405
+ const operations = migrationPlan.operations;
406
+ const packagePath = path.join(projectRoot, 'package.json');
407
+ const packagePatch = patchPackageJson(fs.readFileSync(packagePath, 'utf8'), targetProfile, targetHostVersion, migration?.packageTransitions || null);
408
+ if (packagePatch.changed) operations.push({ filePath: packagePath, ...packagePatch });
409
+ if (migration?.tsconfigTransitions) {
410
+ const tsconfigPath = path.join(projectRoot, TSCONFIG_PATH);
411
+ const source = fs.existsSync(tsconfigPath) ? fs.readFileSync(tsconfigPath, 'utf8') : '{}\n';
412
+ const tsconfigPatch = patchTsconfigJson(source, migration.tsconfigTransitions);
413
+ if (tsconfigPatch.changed) operations.push({ filePath: tsconfigPath, ...tsconfigPatch });
414
+ }
415
+ if (includeState) addStateOperation(operations, projectRoot, migration?.toTemplateVersion || profile.targetTemplateVersion);
416
+ return {
417
+ operations,
418
+ conflicts: [],
419
+ migration,
420
+ state,
421
+ targetProfile,
422
+ targetHostVersion,
423
+ hasMore: Boolean(migration && migration.toTemplateVersion < profile.targetTemplateVersion),
424
+ };
425
+ }
426
+
427
+ function addStateOperation(operations, projectRoot, targetVersion) {
428
+ const statePath = path.join(projectRoot, TEMPLATE_VERSION_PATH);
429
+ const nextState = { schemaVersion: 1, templateVersion: targetVersion };
430
+ const appConfig = fs.existsSync(statePath) ? readJson(statePath) : {};
431
+ const currentState = appConfig.expo?.extra?.powerappsNative || null;
432
+ if (JSON.stringify(currentState) !== JSON.stringify(nextState)) {
433
+ appConfig.expo ||= {};
434
+ appConfig.expo.extra ||= {};
435
+ appConfig.expo.extra.powerappsNative = nextState;
436
+ operations.push({
437
+ filePath: statePath,
438
+ changed: true,
439
+ changes: [`record template version ${targetVersion}`],
440
+ content: `${JSON.stringify(appConfig, null, 2)}\n`,
441
+ state: true,
442
+ });
443
+ }
444
+ }
445
+
446
+ module.exports = {
447
+ HOST_PACKAGE,
448
+ TEMPLATE_VERSION_PATH,
449
+ assertSafeFilePath,
450
+ buildMigrationOperations,
451
+ buildUpgradePlan,
452
+ hashContent,
453
+ isHashOrNull,
454
+ isCustomerOwnedPath,
455
+ isTemplateManagedPath,
456
+ isUnifiedPatchManagedPath,
457
+ parseFileMode,
458
+ patchTsconfigJson,
459
+ patchPackageJson,
460
+ readJson,
461
+ readProjectState,
462
+ selectNextMigration,
463
+ };