@microsoft/power-apps-native-host 0.2.25 → 0.3.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.
Files changed (154) hide show
  1. package/README.md +21 -10
  2. package/compatibility/current.json +31 -4
  3. package/lib/NOTICE.txt +1325 -24
  4. package/lib/commonjs/auth/AuthContext.js +110 -33
  5. package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
  6. package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
  7. package/lib/commonjs/bridge/bridgeState.js +40 -0
  8. package/lib/commonjs/config/babelConfig.js +13 -0
  9. package/lib/commonjs/config/expoConfig.js +71 -0
  10. package/lib/commonjs/config/metroConfig.js +70 -0
  11. package/lib/commonjs/config/metroLogger.js +182 -0
  12. package/lib/commonjs/config/tamaguiConfig.js +98 -0
  13. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  14. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  15. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  16. package/lib/commonjs/connections/connectionState.js +96 -0
  17. package/lib/commonjs/connections/disambiguate.js +19 -11
  18. package/lib/commonjs/connections/resolveConnections.js +348 -0
  19. package/lib/commonjs/connections/types.js +106 -0
  20. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
  21. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  22. package/lib/commonjs/controls/FilePicker.js +6 -0
  23. package/lib/commonjs/controls/ImagePicker.js +8 -0
  24. package/lib/commonjs/index.js +167 -4
  25. package/lib/commonjs/logger.js +9 -6
  26. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
  28. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  29. package/lib/commonjs/platform/DataverseDataSources.js +18 -0
  30. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
  31. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  32. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  33. package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
  34. package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +208 -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 +110 -33
  40. package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
  41. package/lib/module/bridge/PowerPagesBridge.js +27 -8
  42. package/lib/module/bridge/bridgeState.js +34 -0
  43. package/lib/module/config/babelConfig.js +9 -0
  44. package/lib/module/config/expoConfig.js +67 -0
  45. package/lib/module/config/metroConfig.js +65 -0
  46. package/lib/module/config/metroLogger.js +175 -0
  47. package/lib/module/config/tamaguiConfig.js +93 -0
  48. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  49. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  50. package/lib/module/connections/connectionMetadata.js +112 -0
  51. package/lib/module/connections/connectionState.js +82 -0
  52. package/lib/module/connections/disambiguate.js +19 -12
  53. package/lib/module/connections/resolveConnections.js +343 -0
  54. package/lib/module/connections/types.js +91 -0
  55. package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
  56. package/lib/module/connections/useConnectionSetup.js +113 -54
  57. package/lib/module/controls/FilePicker.js +6 -0
  58. package/lib/module/controls/ImagePicker.js +8 -0
  59. package/lib/module/index.js +14 -4
  60. package/lib/module/logger.js +8 -6
  61. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
  63. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  64. package/lib/module/platform/DataverseDataSources.js +13 -0
  65. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
  66. package/lib/module/platform/GlobalDiscovery.js +45 -0
  67. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  68. package/lib/module/provider/PowerAppsHostProvider.js +160 -28
  69. package/lib/module/provider/PowerAppsProvider.js +182 -138
  70. package/lib/module/provider/SessionDetailsGesture.js +203 -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 +1 -0
  76. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  77. package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
  78. package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
  79. package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
  80. package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
  81. package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
  82. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  83. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  84. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  85. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  86. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  87. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
  88. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  89. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
  90. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  91. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  92. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  93. package/lib/typescript/commonjs/index.d.ts +18 -10
  94. package/lib/typescript/commonjs/logger.d.ts +4 -4
  95. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  97. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  98. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
  99. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  100. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  101. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
  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 +1 -0
  110. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  111. package/lib/typescript/module/config/babelConfig.d.ts +12 -0
  112. package/lib/typescript/module/config/expoConfig.d.ts +21 -0
  113. package/lib/typescript/module/config/metroConfig.d.ts +6 -0
  114. package/lib/typescript/module/config/metroLogger.d.ts +21 -0
  115. package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
  116. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  117. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  118. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  119. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  120. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  121. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
  122. package/lib/typescript/module/connections/types.d.ts +129 -1
  123. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
  124. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  125. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  126. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  127. package/lib/typescript/module/index.d.ts +18 -10
  128. package/lib/typescript/module/logger.d.ts +4 -4
  129. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  131. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  132. package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
  133. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  134. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  135. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
  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 +52 -13
  143. package/scripts/build-codegen-package.js +283 -0
  144. package/scripts/start-web.mjs +9 -5
  145. package/scripts/upgrade/runtime.js +139 -24
  146. package/scripts/upgrade-authoring.js +274 -180
  147. package/scripts/upgrade.js +10 -16
  148. package/src/config/generated-config.d.ts +4 -0
  149. package/src/config/index.d.ts +3 -0
  150. package/src/config/tsconfig.json +26 -0
  151. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  152. package/lib/commonjs/platform/resolveConnections.js +0 -264
  153. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  154. package/lib/module/platform/resolveConnections.js +0 -259
@@ -1,19 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  const childProcess = require('child_process');
4
- const crypto = require('crypto');
5
4
  const fs = require('fs');
6
5
  const path = require('path');
7
6
  const { setLockProvider, withLock } = require('@ster5/global-mutex');
8
7
  const { createTwoFilesPatch, parsePatch } = require('diff');
9
8
  const writeFileAtomic = require('write-file-atomic');
10
- const { HOST_PACKAGE, TEMPLATE_VERSION_PATH, assertSafeFilePath, hashContent, isCustomerOwnedPath, isHashOrNull, readJson } = require('./upgrade/runtime');
9
+ const { HOST_PACKAGE, TEMPLATE_VERSION_PATH, assertSafeFilePath, hashContent, isCustomerOwnedPath, readJson } = require('./upgrade/runtime');
11
10
 
12
11
  const AUTHORING_LOCK_PATH = 'packages/power-apps-native-host/compatibility/.migration-build.lock';
13
12
  const AUTHORING_JOURNAL_PATH = 'packages/power-apps-native-host/compatibility/.migration-build-journal.json';
14
13
  const TEMPLATE_PATH = 'templates/expo-app-standalone';
15
- const MIGRATION_CHANGE_PATH = 'migration-change';
16
14
  const TSCONFIG_PATH = 'tsconfig.json';
15
+ const GITIGNORE_PATH = '.gitignore';
16
+ const GITIGNORE_START = '# POWER APPS MANAGED START - DO NOT REMOVE OR RENAME THE COMMENT';
17
+ const GITIGNORE_END = '# POWER APPS MANAGED END - DO NOT REMOVE OR RENAME THE COMMENT';
18
+ const MANAGED_PACKAGE_FIELDS = ['main'];
19
+ const PROFILE_PATH = 'packages/power-apps-native-host/compatibility/current.json';
20
+ const TEMPLATE_BASELINE_TAG_PREFIX = 'standalone-template-migration-baseline-v';
21
+ const LEGACY_TEMPLATE_BASELINE_TAG_PATTERN = /^template-migration-baseline-v\d+$/;
22
+ const RELEASE_HOST_VERSION = '__RELEASE_HOST_VERSION__';
23
+ const NATIVE_RUNTIME_FINGERPRINT_SCRIPT = 'scripts/repository/native-runtime-fingerprint.js';
24
+ const NATIVE_RUNTIME_PLATFORMS = ['android', 'ios'];
17
25
  const CUSTOMER_SECTIONS = [
18
26
  {
19
27
  name: 'customer app settings',
@@ -43,7 +51,7 @@ function git(repositoryRoot, args, allowFailure = false) {
43
51
  }
44
52
 
45
53
  function resolveVersionTag(repositoryRoot, tag) {
46
- if (!tag) throw new Error('change/check requires --from-tag <version-tag> or MIGRATION_BASE_TAG');
54
+ if (!tag) throw new Error('build requires --from-tag <version-tag>');
47
55
  const tagRef = `refs/tags/${tag}`;
48
56
  if (git(repositoryRoot, ['show-ref', '--verify', '--quiet', tagRef], true) === null) {
49
57
  throw new Error(`${tag} is not a local Git tag; fetch tags or provide a valid --from-tag`);
@@ -55,6 +63,26 @@ function resolveVersionTag(repositoryRoot, tag) {
55
63
  return { tag, commit };
56
64
  }
57
65
 
66
+ function templateBaselineTag(templateVersion) {
67
+ return `${TEMPLATE_BASELINE_TAG_PREFIX}${templateVersion}`;
68
+ }
69
+
70
+ function inferBuildBaseTag(repositoryRoot) {
71
+ const profile = readJson(path.join(repositoryRoot, PROFILE_PATH));
72
+ if (!Number.isInteger(profile.targetTemplateVersion)) throw new Error('Current compatibility profile has an invalid targetTemplateVersion');
73
+ const currentTag = templateBaselineTag(profile.targetTemplateVersion);
74
+ if (git(repositoryRoot, ['show-ref', '--verify', '--quiet', `refs/tags/${currentTag}`], true) !== null) return currentTag;
75
+ const legacyTags = git(repositoryRoot, ['tag', '--merged', 'HEAD', '--list', 'template-migration-baseline-v*', '--sort=-version:refname'])
76
+ .trim().split('\n').filter(Boolean);
77
+ const legacyTag = legacyTags.find(tag => {
78
+ if (!LEGACY_TEMPLATE_BASELINE_TAG_PATTERN.test(tag)) return false;
79
+ const base = resolveVersionTag(repositoryRoot, tag);
80
+ return readTemplateState(repositoryRoot, base.commit)?.templateVersion === profile.targetTemplateVersion;
81
+ });
82
+ if (legacyTag) return legacyTag;
83
+ throw new Error(`Could not infer template migration baseline. Expected ${currentTag}.`);
84
+ }
85
+
58
86
  function getTemplateChanges(repositoryRoot, fromTag) {
59
87
  const base = resolveVersionTag(repositoryRoot, fromTag);
60
88
  const tracked = git(repositoryRoot, ['diff', '--no-renames', '--name-only', '--diff-filter=ACDMT', base.commit, '--', TEMPLATE_PATH])
@@ -65,93 +93,12 @@ function getTemplateChanges(repositoryRoot, fromTag) {
65
93
  return {
66
94
  baseTag: base.tag,
67
95
  baseCommit: base.commit,
68
- headCommit: git(repositoryRoot, ['rev-parse', 'HEAD']).trim(),
69
96
  files: paths.map(filePath => {
70
- const absolutePath = path.join(repositoryRoot, filePath);
71
- const content = fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath) : null;
72
- return { path: filePath.slice(`${TEMPLATE_PATH}/`.length), targetHash: hashContent(content) };
97
+ return { path: filePath.slice(`${TEMPLATE_PATH}/`.length) };
73
98
  }),
74
99
  };
75
100
  }
76
101
 
77
- function readMigrationChanges(repositoryRoot) {
78
- const directory = path.join(repositoryRoot, MIGRATION_CHANGE_PATH);
79
- if (!fs.existsSync(directory)) return [];
80
- return fs.readdirSync(directory)
81
- .filter(name => name.endsWith('.json'))
82
- .sort()
83
- .map(name => validateMigrationChange(name, readJson(path.join(directory, name))));
84
- }
85
-
86
- function validateMigrationChange(name, record) {
87
- if (record.schemaVersion !== 1 || record.type !== 'template-migration' || typeof record.baseTag !== 'string' || typeof record.baseCommit !== 'string') {
88
- throw new Error(`Migration changelog ${name} has an invalid schema`);
89
- }
90
- if (!Array.isArray(record.files)) throw new Error(`Migration changelog ${name} has an invalid files list`);
91
- const seen = new Set();
92
- for (const file of record.files) {
93
- if (!file || !isSafeTemplatePath(file.path) || !isHashOrNull(file.targetHash)) throw new Error(`Migration changelog ${name} contains invalid file metadata`);
94
- if (seen.has(file.path)) throw new Error(`Migration changelog ${name} contains duplicate path ${file.path}`);
95
- seen.add(file.path);
96
- }
97
- return { name, ...record };
98
- }
99
-
100
- function isSafeTemplatePath(relativePath) {
101
- return typeof relativePath === 'string'
102
- && relativePath.length > 0
103
- && !relativePath.includes('\\')
104
- && !path.posix.isAbsolute(relativePath)
105
- && path.posix.normalize(relativePath) === relativePath
106
- && !relativePath.split('/').includes('..');
107
- }
108
-
109
- function createMigrationChange(repositoryRoot, options) {
110
- if (!options.message) throw new Error('change requires --message');
111
- if (!options.fromTag) throw new Error('change requires --from-tag <version-tag> or MIGRATION_BASE_TAG');
112
- const templateChanges = getTemplateChanges(repositoryRoot, options.fromTag);
113
- assertNoCustomerOwnedChanges(templateChanges.files);
114
- if (!templateChanges.files.length) throw new Error('No template changes were found');
115
- const record = {
116
- schemaVersion: 1,
117
- type: 'template-migration',
118
- comment: options.message,
119
- createdAt: new Date().toISOString(),
120
- ...templateChanges,
121
- };
122
- const slug = options.message.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'template-change';
123
- const relativePath = path.join(MIGRATION_CHANGE_PATH, `${slug}-${crypto.randomUUID().slice(0, 8)}.json`);
124
- console.log(`${options.dryRun ? '[dry-run] ' : ''}Create ${relativePath}`);
125
- if (!options.dryRun) {
126
- fs.mkdirSync(path.dirname(path.join(repositoryRoot, relativePath)), { recursive: true });
127
- fs.writeFileSync(path.join(repositoryRoot, relativePath), `${JSON.stringify(record, null, 2)}\n`);
128
- }
129
- return record;
130
- }
131
-
132
- function checkMigrationChanges(repositoryRoot, options) {
133
- const records = readMigrationChanges(repositoryRoot);
134
- const recordedTags = [...new Set(records.map(record => record.baseTag).filter(Boolean))];
135
- const changes = getTemplateChanges(repositoryRoot, options.fromTag || (recordedTags.length === 1 ? recordedTags[0] : null));
136
- assertNoCustomerOwnedChanges(changes.files);
137
- const covered = new Set(records.flatMap(record => record.files || []).map(file => `${file.path}:${file.targetHash}`));
138
- const current = new Set(changes.files.map(file => `${file.path}:${file.targetHash}`));
139
- if (!changes.files.length) {
140
- if (covered.size) throw new Error('Migration changelogs contain stale template changes but the template matches the version tag');
141
- console.log('No template changes require a migration changelog.');
142
- return;
143
- }
144
- const wrongBase = records.filter(record => record.baseTag !== changes.baseTag || record.baseCommit !== changes.baseCommit);
145
- if (wrongBase.length) throw new Error(`Migration changelogs must all use version tag ${changes.baseTag}; regenerate: ${wrongBase.map(record => record.name).join(', ')}`);
146
- const missing = changes.files.filter(file => !covered.has(`${file.path}:${file.targetHash}`));
147
- const stale = [...covered].filter(file => !current.has(file));
148
- if (missing.length) {
149
- throw new Error(`Template changes require an up-to-date migration changelog:\n${missing.map(file => ` - ${file.path}`).join('\n')}\nRun upgrade-template change --from-tag ${changes.baseTag} --message "description"`);
150
- }
151
- if (stale.length) throw new Error(`Migration changelogs contain stale template changes:\n${stale.map(file => ` - ${file.split(':')[0]}`).join('\n')}`);
152
- console.log(`${changes.files.length} template change(s) are covered by migration changelogs.`);
153
- }
154
-
155
102
  function assertNoCustomerOwnedChanges(files) {
156
103
  const protectedFiles = files.filter(file => isCustomerOwnedPath(file.path));
157
104
  if (protectedFiles.length) {
@@ -176,10 +123,9 @@ function fileMode(repositoryRoot, commit, relativePath) {
176
123
 
177
124
  async function runAuthoringCommand(command, startPath, options) {
178
125
  const repositoryRoot = findRepositoryRoot(startPath);
179
- if (command === 'change') return createMigrationChange(repositoryRoot, options);
180
- if (command === 'check') return checkMigrationChanges(repositoryRoot, options);
126
+ if (command !== 'build') throw new Error(`Unsupported template authoring command: ${command}`);
181
127
  return withAuthoringLock(repositoryRoot, () => {
182
- recoverAuthoringJournal(repositoryRoot);
128
+ if (recoverAuthoringJournal(repositoryRoot) === 'committed') return null;
183
129
  return buildMigrationStep(repositoryRoot, options);
184
130
  });
185
131
  }
@@ -200,28 +146,40 @@ async function withAuthoringLock(repositoryRoot, action) {
200
146
 
201
147
  function recoverAuthoringJournal(repositoryRoot) {
202
148
  const journalPath = path.join(repositoryRoot, AUTHORING_JOURNAL_PATH);
203
- if (!fs.existsSync(journalPath)) return;
149
+ if (!fs.existsSync(journalPath)) return null;
204
150
  const journal = readJson(journalPath);
205
151
  validateAuthoringJournal(repositoryRoot, journal);
206
152
  if (journal.status !== 'committed') {
207
153
  writeFileAtomic.sync(journal.profilePath, Buffer.from(journal.originalProfile, 'base64'));
154
+ if (journal.appJsonPath && journal.originalAppJson) writeFileAtomic.sync(journal.appJsonPath, Buffer.from(journal.originalAppJson, 'base64'));
208
155
  for (const outputPath of journal.outputPaths) fs.rmSync(outputPath, { force: true });
209
- for (const record of journal.records) {
210
- fs.mkdirSync(path.dirname(record.path), { recursive: true });
211
- writeFileAtomic.sync(record.path, Buffer.from(record.content, 'base64'));
212
- }
213
156
  console.warn('[upgrade-template] Recovered an interrupted migration build.');
214
157
  }
158
+ for (const filePath of [journal.profilePath, journal.appJsonPath, ...journal.outputPaths].filter(Boolean)) {
159
+ removeTemporarySiblings(filePath);
160
+ }
215
161
  fs.rmSync(journalPath, { force: true });
162
+ return journal.status;
163
+ }
164
+
165
+ function removeTemporarySiblings(filePath) {
166
+ const directory = path.dirname(filePath);
167
+ const temporaryPrefix = `${path.basename(filePath)}.tmp-`;
168
+ for (const name of fs.readdirSync(directory)) {
169
+ if (name.startsWith(temporaryPrefix)) fs.rmSync(path.join(directory, name), { force: true });
170
+ }
216
171
  }
217
172
 
218
173
  function validateAuthoringJournal(repositoryRoot, journal) {
219
174
  if (journal.schemaVersion !== 1 || !['preparing', 'committed'].includes(journal.status)) throw new Error('Migration build journal has an unsupported schema');
220
175
  const profilePath = path.join(repositoryRoot, 'packages/power-apps-native-host/compatibility/current.json');
176
+ const appJsonPath = path.join(repositoryRoot, TEMPLATE_PATH, TEMPLATE_VERSION_PATH);
221
177
  const migrationDirectory = path.join(repositoryRoot, 'packages/power-apps-native-host/compatibility/migrations');
222
- const changeDirectory = path.join(repositoryRoot, MIGRATION_CHANGE_PATH);
223
178
  if (path.resolve(journal.profilePath || '') !== profilePath || !isCanonicalBase64(journal.originalProfile)) throw new Error('Migration build journal has invalid profile backup data');
224
- if (!Array.isArray(journal.outputPaths) || !Array.isArray(journal.records)) throw new Error('Migration build journal has invalid file entries');
179
+ if (journal.appJsonPath !== undefined || journal.originalAppJson !== undefined) {
180
+ if (path.resolve(journal.appJsonPath || '') !== appJsonPath || !isCanonicalBase64(journal.originalAppJson)) throw new Error('Migration build journal has invalid app.json backup data');
181
+ }
182
+ if (!Array.isArray(journal.outputPaths)) throw new Error('Migration build journal has invalid file entries');
225
183
  const outputPaths = new Set();
226
184
  for (const outputPath of journal.outputPaths) {
227
185
  if (!isPathWithin(migrationDirectory, outputPath) || !/\.(?:json|patch)$/.test(outputPath) || outputPaths.has(path.resolve(outputPath))) {
@@ -230,14 +188,6 @@ function validateAuthoringJournal(repositoryRoot, journal) {
230
188
  assertSafeFilePath(repositoryRoot, path.resolve(outputPath));
231
189
  outputPaths.add(path.resolve(outputPath));
232
190
  }
233
- const recordPaths = new Set();
234
- for (const record of journal.records) {
235
- if (!record || !isPathWithin(changeDirectory, record.path) || !record.path.endsWith('.json') || !isCanonicalBase64(record.content) || recordPaths.has(path.resolve(record.path))) {
236
- throw new Error('Migration build journal has invalid changelog backup data');
237
- }
238
- assertSafeFilePath(repositoryRoot, path.resolve(record.path));
239
- recordPaths.add(path.resolve(record.path));
240
- }
241
191
  }
242
192
 
243
193
  function isPathWithin(directory, filePath) {
@@ -253,13 +203,76 @@ function isCanonicalBase64(value) {
253
203
 
254
204
  function updateProfileFromTemplate(profile, repositoryRoot) {
255
205
  const templatePackage = readJson(path.join(repositoryRoot, TEMPLATE_PATH, 'package.json'));
206
+ validateDevPlayerDependencies(repositoryRoot, templatePackage);
207
+ updateProfileFromPackage(profile, templatePackage);
208
+ }
209
+
210
+ function nextNativeRuntime(current, fingerprint) {
211
+ return {
212
+ version: current.version + (fingerprint === current.fingerprint ? 0 : 1),
213
+ fingerprint,
214
+ };
215
+ }
216
+
217
+ function updateNativeRuntime(profile, repositoryRoot) {
218
+ const previous = JSON.parse(JSON.stringify(profile.nativeRuntime));
219
+ for (const platform of NATIVE_RUNTIME_PLATFORMS) {
220
+ const current = profile.nativeRuntime[platform];
221
+ if (!Number.isInteger(current?.version) || current.version < 1 || !/^[a-f0-9]{64}$/.test(current.fingerprint || '')) {
222
+ throw new Error(`Current compatibility profile has invalid nativeRuntime.${platform}`);
223
+ }
224
+ const result = childProcess.spawnSync(
225
+ process.execPath,
226
+ [path.join(repositoryRoot, NATIVE_RUNTIME_FINGERPRINT_SCRIPT), platform, path.join(repositoryRoot, TEMPLATE_PATH)],
227
+ { cwd: repositoryRoot, encoding: 'utf8' },
228
+ );
229
+ if (result.error) throw result.error;
230
+ if (result.status !== 0) {
231
+ throw new Error(`Could not fingerprint the ${platform} native runtime: ${String(result.stderr || '').trim()}`);
232
+ }
233
+ const fingerprint = String(result.stdout || '').trim();
234
+ if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw new Error(`Invalid ${platform} native runtime fingerprint`);
235
+ profile.nativeRuntime[platform] = nextNativeRuntime(current, fingerprint);
236
+ }
237
+ return previous;
238
+ }
239
+
240
+ function updateProfileFromPackage(profile, templatePackage) {
256
241
  const dependencies = { ...templatePackage.dependencies };
257
- profile.expoVersion = dependencies.expo;
242
+ const expoVersion = dependencies.expo;
243
+ const expoMajorMatch = typeof expoVersion === 'string' && expoVersion.match(/^(?:[~^])?(\d+)\./);
244
+ if (!expoMajorMatch) throw new Error('Standalone template package.json must define a supported Expo version');
245
+ profile.expoSdk = Number(expoMajorMatch[1]);
246
+ profile.expoVersion = expoVersion;
258
247
  delete dependencies.expo;
259
248
  delete dependencies[HOST_PACKAGE];
260
249
  profile.managedDependencies = dependencies;
261
250
  profile.managedDevDependencies = { ...templatePackage.devDependencies };
262
251
  profile.managedOverrides = { ...templatePackage.overrides };
252
+ profile.managedScripts = { ...templatePackage.scripts };
253
+ profile.managedFields = Object.fromEntries(MANAGED_PACKAGE_FIELDS
254
+ .filter(name => templatePackage[name] !== undefined)
255
+ .map(name => [name, templatePackage[name]]));
256
+ }
257
+
258
+ function validateDevPlayerDependencies(repositoryRoot, templatePackage) {
259
+ const devPlayerPackagePath = path.join(repositoryRoot, 'apps/dev-player/package.json');
260
+ if (!fs.existsSync(devPlayerPackagePath)) {
261
+ throw new Error('Dev Player package.json is missing; cannot validate standalone template dependencies');
262
+ }
263
+
264
+ const devPlayerPackage = readJson(devPlayerPackagePath);
265
+ const missingDependencies = Object.keys(templatePackage.dependencies || {})
266
+ .filter(name => !Object.hasOwn(devPlayerPackage.dependencies || {}, name));
267
+ const missingDevDependencies = Object.keys(templatePackage.devDependencies || {})
268
+ .filter(name => !Object.hasOwn(devPlayerPackage.devDependencies || {}, name));
269
+ const missing = [
270
+ ...missingDependencies.map(name => `dependencies.${name}`),
271
+ ...missingDevDependencies.map(name => `devDependencies.${name}`),
272
+ ].sort();
273
+ if (missing.length > 0) {
274
+ throw new Error(`Dev Player package.json is missing standalone template dependencies: ${missing.join(', ')}`);
275
+ }
263
276
  }
264
277
 
265
278
  function packageTransitions(baseValues = {}, targetValues = {}) {
@@ -272,23 +285,48 @@ function packageTransitions(baseValues = {}, targetValues = {}) {
272
285
  }]));
273
286
  }
274
287
 
275
- function tsconfigWithoutManagedPaths(tsconfig) {
276
- const clone = JSON.parse(JSON.stringify(tsconfig || {}));
277
- if (clone.compilerOptions?.paths) {
278
- delete clone.compilerOptions.paths;
279
- if (Object.keys(clone.compilerOptions).length === 0) delete clone.compilerOptions;
280
- }
281
- return clone;
288
+ function buildPackageTransitions(basePackage, targetPackage) {
289
+ return {
290
+ fields: packageTransitions(
291
+ Object.fromEntries(MANAGED_PACKAGE_FIELDS.filter(name => basePackage[name] !== undefined).map(name => [name, basePackage[name]])),
292
+ Object.fromEntries(MANAGED_PACKAGE_FIELDS.filter(name => targetPackage[name] !== undefined).map(name => [name, targetPackage[name]])),
293
+ ),
294
+ scripts: packageTransitions(basePackage.scripts, targetPackage.scripts),
295
+ dependencies: Object.fromEntries(Object.entries(packageTransitions(basePackage.dependencies, targetPackage.dependencies))
296
+ .filter(([name]) => name !== HOST_PACKAGE && name !== 'expo')),
297
+ devDependencies: packageTransitions(basePackage.devDependencies, targetPackage.devDependencies),
298
+ overrides: packageTransitions(basePackage.overrides, targetPackage.overrides),
299
+ };
282
300
  }
283
301
 
284
302
  function buildTsconfigTransitions(relativePath, baseContent, targetContent) {
285
303
  const base = baseContent ? JSON.parse(baseContent.toString('utf8')) : {};
286
304
  const target = targetContent ? JSON.parse(targetContent.toString('utf8')) : {};
287
- if (JSON.stringify(tsconfigWithoutManagedPaths(base)) !== JSON.stringify(tsconfigWithoutManagedPaths(target))) {
288
- throw new Error(`Template change ${relativePath} modifies unsupported tsconfig fields; add a structured upgrade handler before building this migration`);
289
- }
305
+ const fields = packageTransitions(
306
+ Object.fromEntries(['extends', 'include', 'exclude'].filter(name => base[name] !== undefined).map(name => [name, base[name]])),
307
+ Object.fromEntries(['extends', 'include', 'exclude'].filter(name => target[name] !== undefined).map(name => [name, target[name]])),
308
+ );
309
+ const baseCompilerOptions = { ...base.compilerOptions };
310
+ const targetCompilerOptions = { ...target.compilerOptions };
311
+ delete baseCompilerOptions.paths;
312
+ delete targetCompilerOptions.paths;
313
+ const compilerOptions = packageTransitions(baseCompilerOptions, targetCompilerOptions);
290
314
  const paths = packageTransitions(base.compilerOptions?.paths, target.compilerOptions?.paths);
291
- return Object.keys(paths).length ? { paths } : null;
315
+ const transitions = {};
316
+ if (Object.keys(fields).length) transitions.fields = fields;
317
+ if (Object.keys(compilerOptions).length) transitions.compilerOptions = compilerOptions;
318
+ if (Object.keys(paths).length) transitions.paths = paths;
319
+ return Object.keys(transitions).length ? transitions : null;
320
+ }
321
+
322
+ function readManagedGitignoreBlock(relativePath, content, required) {
323
+ if (content === null) return null;
324
+ const source = content.toString('utf8');
325
+ const start = source.indexOf(GITIGNORE_START);
326
+ const end = source.indexOf(GITIGNORE_END);
327
+ if (start === -1 && end === -1 && !required) return null;
328
+ if (start === -1 || end < start) throw new Error(`Template file ${relativePath} has invalid Power Apps managed markers`);
329
+ return source.slice(start + GITIGNORE_START.length, end).replace(/^\r?\n|\r?\n$/g, '');
292
330
  }
293
331
 
294
332
  function assertPatchableMode(relativePath, mode) {
@@ -297,22 +335,78 @@ function assertPatchableMode(relativePath, mode) {
297
335
  }
298
336
  }
299
337
 
338
+ function readTemplateState(repositoryRoot, commit) {
339
+ const content = commit
340
+ ? readGitFile(repositoryRoot, commit, TEMPLATE_VERSION_PATH)
341
+ : fs.readFileSync(path.join(repositoryRoot, TEMPLATE_PATH, TEMPLATE_VERSION_PATH));
342
+ return content ? JSON.parse(content.toString('utf8')).expo?.extra?.powerappsNative : null;
343
+ }
344
+
345
+ function buildTemplateVersionContent(repositoryRoot, templateVersion, nativeRuntime) {
346
+ const appJsonPath = path.join(repositoryRoot, TEMPLATE_PATH, TEMPLATE_VERSION_PATH);
347
+ const appConfig = readJson(appJsonPath);
348
+ appConfig.expo ||= {};
349
+ appConfig.expo.extra ||= {};
350
+ appConfig.expo.extra.powerappsNative ||= { schemaVersion: 1 };
351
+ appConfig.expo.extra.powerappsNative.schemaVersion ||= 1;
352
+ appConfig.expo.extra.powerappsNative.templateVersion = templateVersion;
353
+ appConfig.expo.extra.powerappsNative.nativeRuntimeVersions = Object.fromEntries(
354
+ NATIVE_RUNTIME_PLATFORMS.map(platform => [platform, nativeRuntime[platform].version]),
355
+ );
356
+ return `${JSON.stringify(appConfig, null, 2)}\n`;
357
+ }
358
+
300
359
  function buildMigrationStep(repositoryRoot, options) {
301
- if (!options.id) throw new Error('build requires --id');
302
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(options.id)) throw new Error('--id may contain only letters, numbers, dots, underscores, and hyphens');
303
- const records = readMigrationChanges(repositoryRoot);
304
- if (!records.length) throw new Error('No pending migration changelogs were found');
305
- const baseTags = [...new Set(records.map(record => record.baseTag))];
306
- const baseCommits = [...new Set(records.map(record => record.baseCommit))];
307
- if (baseTags.length !== 1 || baseCommits.length !== 1 || !baseTags[0]) throw new Error('All pending migration changelogs must use the same version tag');
308
- checkMigrationChanges(repositoryRoot, { fromTag: baseTags[0] });
309
-
310
- const paths = [...new Set(records.flatMap(record => (record.files || []).map(file => file.path)))].sort();
360
+ const fromTag = options.fromTag || inferBuildBaseTag(repositoryRoot);
361
+ const templateChanges = getTemplateChanges(repositoryRoot, fromTag);
362
+ const base = { tag: templateChanges.baseTag, commit: templateChanges.baseCommit };
363
+ const isLegacyBootstrap = !options.fromTag && LEGACY_TEMPLATE_BASELINE_TAG_PATTERN.test(fromTag);
364
+ const migrationFiles = isLegacyBootstrap
365
+ ? templateChanges.files.filter(file => !isCustomerOwnedPath(file.path))
366
+ : templateChanges.files;
367
+ assertNoCustomerOwnedChanges(migrationFiles);
368
+ if (!migrationFiles.length) {
369
+ if (options.skipIfNoChanges) {
370
+ console.log(`No standalone template changes found since ${base.tag}.`);
371
+ return null;
372
+ }
373
+ throw new Error('No template changes were found');
374
+ }
375
+
376
+ const appJsonPath = path.join(repositoryRoot, TEMPLATE_PATH, TEMPLATE_VERSION_PATH);
377
+ const originalAppJson = fs.readFileSync(appJsonPath);
378
+ const baseState = readTemplateState(repositoryRoot, base.commit);
379
+ if (!Number.isInteger(baseState?.templateVersion)) {
380
+ throw new Error(`Version tag ${base.tag} does not contain ${TEMPLATE_VERSION_PATH} expo.extra.powerappsNative.templateVersion`);
381
+ }
382
+ const currentState = readTemplateState(repositoryRoot, null);
383
+ if (currentState?.templateVersion !== baseState.templateVersion) {
384
+ throw new Error(`Template version must not be advanced before release build (${TEMPLATE_VERSION_PATH} is ${currentState?.templateVersion ?? 'missing'}, expected ${baseState.templateVersion})`);
385
+ }
386
+ if (migrationFiles.some(file => file.path === TEMPLATE_VERSION_PATH)) {
387
+ throw new Error(`${TEMPLATE_VERSION_PATH} changes are limited to the release-owned templateVersion bump; add a structured migration handler before changing other fields`);
388
+ }
389
+ const expectedTargetVersion = baseState.templateVersion + 1;
390
+ const migrationId = options.id || `template-v${expectedTargetVersion}`;
391
+ const migrationMessage = options.message || `Release standalone template v${expectedTargetVersion}`;
392
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(migrationId)) throw new Error('--id may contain only letters, numbers, dots, underscores, and hyphens');
393
+ const profilePath = path.join(repositoryRoot, 'packages/power-apps-native-host/compatibility/current.json');
394
+ const profile = readJson(profilePath);
395
+ if (profile.targetTemplateVersion !== undefined && profile.targetTemplateVersion !== baseState.templateVersion) {
396
+ throw new Error(`Current profile targets template ${profile.targetTemplateVersion}, but migration starts at ${baseState.templateVersion}`);
397
+ }
398
+ const previousNativeRuntime = updateNativeRuntime(profile, repositoryRoot);
399
+ updateProfileFromTemplate(profile, repositoryRoot);
400
+ profile.targetTemplateVersion = expectedTargetVersion;
401
+ const targetAppJson = buildTemplateVersionContent(repositoryRoot, expectedTargetVersion, profile.nativeRuntime);
402
+
403
+ const paths = migrationFiles.map(file => file.path).sort();
311
404
  const patches = [];
312
405
  const patchFiles = {};
313
406
  let tsconfigTransitions = null;
407
+ let gitignoreTransition = null;
314
408
  for (const relativePath of paths) {
315
- const baseContent = readGitFile(repositoryRoot, baseCommits[0], relativePath);
409
+ const baseContent = readGitFile(repositoryRoot, base.commit, relativePath);
316
410
  const targetPath = path.join(repositoryRoot, TEMPLATE_PATH, relativePath);
317
411
  if (fs.existsSync(targetPath) && fs.lstatSync(targetPath).isSymbolicLink()) {
318
412
  throw new Error(`Symbolic link template change ${relativePath} is not supported`);
@@ -323,11 +417,18 @@ function buildMigrationStep(repositoryRoot, options) {
323
417
  tsconfigTransitions = buildTsconfigTransitions(relativePath, baseContent, targetContent);
324
418
  continue;
325
419
  }
420
+ if (relativePath === GITIGNORE_PATH) {
421
+ gitignoreTransition = {
422
+ from: readManagedGitignoreBlock(relativePath, baseContent, false),
423
+ to: readManagedGitignoreBlock(relativePath, targetContent, true),
424
+ };
425
+ continue;
426
+ }
326
427
  if ([TEMPLATE_VERSION_PATH, 'package.json', 'package-lock.json'].includes(relativePath)) continue;
327
428
  if (!isPatchableText(baseContent) || !isPatchableText(targetContent)) {
328
429
  throw new Error(`Binary template change ${relativePath} cannot be represented by a unified migration patch`);
329
430
  }
330
- const fromMode = fileMode(repositoryRoot, baseCommits[0], relativePath);
431
+ const fromMode = fileMode(repositoryRoot, base.commit, relativePath);
331
432
  const toMode = fileMode(repositoryRoot, null, relativePath);
332
433
  assertPatchableMode(relativePath, fromMode);
333
434
  assertPatchableMode(relativePath, toMode);
@@ -342,61 +443,48 @@ function buildMigrationStep(repositoryRoot, options) {
342
443
  targetContent === null ? '/dev/null' : relativePath,
343
444
  baseContent?.toString('utf8') || '',
344
445
  targetContent?.toString('utf8') || '',
345
- baseTags[0],
346
- options.id,
446
+ base.tag,
447
+ migrationId,
347
448
  { context: 3 },
348
449
  ));
349
450
  }
350
451
 
351
- const baseStateContent = readGitFile(repositoryRoot, baseCommits[0], TEMPLATE_VERSION_PATH);
352
- if (!baseStateContent && !options.fromVersion) {
353
- throw new Error(`Version tag ${baseTags[0]} does not contain ${TEMPLATE_VERSION_PATH}; rerun build with --from-version <version>`);
354
- }
355
- const baseState = baseStateContent
356
- ? JSON.parse(baseStateContent.toString('utf8'))
357
- : { schemaVersion: 1, templateVersion: options.fromVersion };
358
- const targetState = readJson(path.join(repositoryRoot, TEMPLATE_PATH, TEMPLATE_VERSION_PATH));
359
- if (targetState.templateVersion !== baseState.templateVersion + 1) {
360
- throw new Error(`Migration must advance exactly one template version (${baseState.templateVersion} -> ${baseState.templateVersion + 1})`);
361
- }
452
+ const targetState = JSON.parse(targetAppJson).expo?.extra?.powerappsNative;
453
+ if (targetState.templateVersion !== expectedTargetVersion) throw new Error(`Migration must advance exactly one template version (${baseState.templateVersion} -> ${expectedTargetVersion})`);
362
454
 
363
- const basePackageContent = readGitFile(repositoryRoot, baseCommits[0], 'package.json');
364
- if (!basePackageContent) throw new Error(`Version tag ${baseTags[0]} does not contain template package.json`);
455
+ const basePackageContent = readGitFile(repositoryRoot, base.commit, 'package.json');
456
+ if (!basePackageContent) throw new Error(`Version tag ${base.tag} does not contain template package.json`);
365
457
  const basePackage = JSON.parse(basePackageContent.toString('utf8'));
366
458
  const targetPackage = readJson(path.join(repositoryRoot, TEMPLATE_PATH, 'package.json'));
367
- const profilePath = path.join(repositoryRoot, 'packages/power-apps-native-host/compatibility/current.json');
368
- const profile = readJson(profilePath);
369
- if (profile.targetTemplateVersion !== undefined && profile.targetTemplateVersion !== baseState.templateVersion) {
370
- throw new Error(`Current profile targets template ${profile.targetTemplateVersion}, but migration starts at ${baseState.templateVersion}`);
371
- }
372
- updateProfileFromTemplate(profile, repositoryRoot);
373
- profile.targetTemplateVersion = targetState.templateVersion;
374
459
  const step = {
375
460
  schemaVersion: 1,
376
- id: options.id,
377
- fromTag: baseTags[0],
378
- fromCommit: baseCommits[0],
461
+ id: migrationId,
462
+ fromTag: base.tag,
463
+ fromCommit: base.commit,
379
464
  fromTemplateVersion: baseState.templateVersion,
380
465
  toTemplateVersion: targetState.templateVersion,
381
- comments: records.map(record => record.comment),
382
- patchFile: `${options.id}.patch`,
466
+ comments: [migrationMessage],
467
+ patchFile: `${migrationId}.patch`,
383
468
  patchFiles,
384
- packageTransitions: {
385
- dependencies: Object.fromEntries(Object.entries(packageTransitions(basePackage.dependencies, targetPackage.dependencies))
386
- .filter(([name]) => name !== HOST_PACKAGE && name !== 'expo')),
387
- devDependencies: packageTransitions(basePackage.devDependencies, targetPackage.devDependencies),
388
- overrides: packageTransitions(basePackage.overrides, targetPackage.overrides),
469
+ packageTransitions: buildPackageTransitions(basePackage, targetPackage),
470
+ nativeRuntimeTransition: {
471
+ from: previousNativeRuntime,
472
+ to: profile.nativeRuntime,
389
473
  },
390
474
  ...(tsconfigTransitions ? { tsconfigTransitions } : {}),
475
+ ...(gitignoreTransition ? { gitignoreTransition } : {}),
391
476
  targetProfile: {
392
477
  id: profile.id,
393
- hostVersion: readJson(path.join(repositoryRoot, 'packages/power-apps-native-host/package.json')).version,
478
+ hostVersion: RELEASE_HOST_VERSION,
394
479
  expoSdk: profile.expoSdk,
395
480
  expoVersion: profile.expoVersion,
396
481
  expoDoctorVersion: profile.expoDoctorVersion,
397
482
  managedDependencies: profile.managedDependencies,
398
483
  managedDevDependencies: profile.managedDevDependencies,
399
484
  managedOverrides: profile.managedOverrides,
485
+ managedScripts: profile.managedScripts,
486
+ managedFields: profile.managedFields,
487
+ nativeRuntime: profile.nativeRuntime,
400
488
  },
401
489
  };
402
490
  if (!step.targetProfile.expoDoctorVersion) throw new Error('Current compatibility profile must define expoDoctorVersion');
@@ -407,62 +495,56 @@ function buildMigrationStep(repositoryRoot, options) {
407
495
  return readJson(metadataPath);
408
496
  });
409
497
  validateMigrationEdges([...existingEdges, step], targetState.templateVersion);
410
- if (!profile.migrations.includes(options.id)) profile.migrations.push(options.id);
498
+ if (!profile.migrations.includes(migrationId)) profile.migrations.push(migrationId);
411
499
 
412
500
  const migrationDirectory = path.join(repositoryRoot, 'packages/power-apps-native-host/compatibility/migrations');
413
- const migrationPath = path.join(migrationDirectory, `${options.id}.json`);
501
+ const migrationPath = path.join(migrationDirectory, `${migrationId}.json`);
414
502
  const patchPath = path.join(migrationDirectory, step.patchFile);
415
- if (fs.existsSync(migrationPath) || fs.existsSync(patchPath)) throw new Error(`Migration ${options.id} already exists`);
416
- console.log(`${options.dryRun ? '[dry-run] ' : ''}Build migration ${options.id} from ${records.length} changelog(s)`);
503
+ if (fs.existsSync(migrationPath) || fs.existsSync(patchPath)) throw new Error(`Migration ${migrationId} already exists`);
504
+ console.log(`${options.dryRun ? '[dry-run] ' : ''}Build migration ${migrationId} from ${base.tag}`);
417
505
  if (!options.dryRun) {
418
506
  fs.mkdirSync(migrationDirectory, { recursive: true });
419
507
  const temporarySuffix = `.tmp-${process.pid}-${Date.now()}`;
420
508
  const temporaryMigrationPath = `${migrationPath}${temporarySuffix}`;
421
509
  const temporaryPatchPath = `${patchPath}${temporarySuffix}`;
422
510
  const temporaryProfilePath = `${profilePath}${temporarySuffix}`;
511
+ const temporaryAppJsonPath = `${appJsonPath}${temporarySuffix}`;
423
512
  const originalProfile = fs.readFileSync(profilePath);
424
- const recordContents = new Map(records.map(record => {
425
- const recordPath = path.join(repositoryRoot, MIGRATION_CHANGE_PATH, record.name);
426
- return [recordPath, fs.readFileSync(recordPath)];
427
- }));
428
513
  const journalPath = path.join(repositoryRoot, AUTHORING_JOURNAL_PATH);
429
514
  const journal = {
430
515
  schemaVersion: 1,
431
516
  status: 'preparing',
432
517
  profilePath,
433
518
  originalProfile: originalProfile.toString('base64'),
519
+ appJsonPath,
520
+ originalAppJson: originalAppJson.toString('base64'),
434
521
  outputPaths: [migrationPath, patchPath],
435
- records: [...recordContents].map(([recordPath, content]) => ({
436
- path: recordPath,
437
- content: content.toString('base64'),
438
- })),
439
522
  };
440
523
  writeFileAtomic.sync(journalPath, `${JSON.stringify(journal, null, 2)}\n`, { mode: 0o600 });
441
524
  try {
442
525
  fs.writeFileSync(temporaryMigrationPath, `${JSON.stringify(step, null, 2)}\n`);
443
526
  fs.writeFileSync(temporaryPatchPath, patches.join('\n'), 'utf8');
444
527
  fs.writeFileSync(temporaryProfilePath, `${JSON.stringify(profile, null, 2)}\n`);
528
+ fs.writeFileSync(temporaryAppJsonPath, targetAppJson, 'utf8');
445
529
  readJson(temporaryMigrationPath);
446
530
  readJson(temporaryProfilePath);
531
+ readJson(temporaryAppJsonPath);
447
532
  parsePatch(fs.readFileSync(temporaryPatchPath, 'utf8'));
448
533
  fs.renameSync(temporaryPatchPath, patchPath);
449
534
  fs.renameSync(temporaryMigrationPath, migrationPath);
535
+ fs.renameSync(temporaryAppJsonPath, appJsonPath);
450
536
  fs.renameSync(temporaryProfilePath, profilePath);
451
- for (const recordPath of recordContents.keys()) fs.unlinkSync(recordPath);
452
537
  journal.status = 'committed';
453
538
  writeFileAtomic.sync(journalPath, `${JSON.stringify(journal, null, 2)}\n`, { mode: 0o600 });
454
539
  fs.rmSync(journalPath, { force: true });
455
540
  } catch (error) {
456
541
  fs.writeFileSync(profilePath, originalProfile);
542
+ fs.writeFileSync(appJsonPath, originalAppJson);
457
543
  fs.rmSync(migrationPath, { force: true });
458
544
  fs.rmSync(patchPath, { force: true });
459
- for (const temporaryPath of [temporaryMigrationPath, temporaryPatchPath, temporaryProfilePath]) {
545
+ for (const temporaryPath of [temporaryMigrationPath, temporaryPatchPath, temporaryProfilePath, temporaryAppJsonPath]) {
460
546
  fs.rmSync(temporaryPath, { force: true });
461
547
  }
462
- for (const [recordPath, content] of recordContents) {
463
- fs.mkdirSync(path.dirname(recordPath), { recursive: true });
464
- fs.writeFileSync(recordPath, content);
465
- }
466
548
  fs.rmSync(journalPath, { force: true });
467
549
  throw error;
468
550
  }
@@ -523,13 +605,25 @@ function validateMigrationEdges(migrations, targetVersion) {
523
605
  }
524
606
  if (migrations.length) {
525
607
  const firstVersion = Math.min(...fromVersions);
608
+ if (firstVersion !== 1) throw new Error('Migration graph must start at template version 1');
526
609
  for (let version = firstVersion; version < targetVersion; version += 1) {
527
610
  if (!fromVersions.has(version)) throw new Error(`Migration graph is missing template version ${version} -> ${version + 1}`);
528
611
  }
529
612
  if (Math.max(...migrations.map(migration => migration.toTemplateVersion)) !== targetVersion) {
530
613
  throw new Error(`Migration graph does not reach target template version ${targetVersion}`);
531
614
  }
615
+ } else if (targetVersion > 1) {
616
+ throw new Error(`Migration graph does not reach target template version ${targetVersion}`);
532
617
  }
533
618
  }
534
619
 
535
- module.exports = { getTemplateChanges, runAuthoringCommand };
620
+ module.exports = {
621
+ RELEASE_HOST_VERSION,
622
+ buildPackageTransitions,
623
+ buildTsconfigTransitions,
624
+ getTemplateChanges,
625
+ nextNativeRuntime,
626
+ runAuthoringCommand,
627
+ updateProfileFromPackage,
628
+ validateDevPlayerDependencies,
629
+ };