@microsoft/power-apps-native-host 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/compatibility/current.json +1 -1
- package/lib/NOTICE.txt +2957 -371
- package/lib/commonjs/auth/AuthContext.js +49 -22
- package/lib/commonjs/auth/targetAuthorization.js +53 -0
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +33 -2
- package/lib/commonjs/bridge/PowerPagesBridge.js +1 -3
- package/lib/commonjs/bridge/setupNativeHost.js +6 -0
- package/lib/commonjs/config/expoConfig.js +14 -0
- package/lib/commonjs/config/runtimeTenantMode.js +16 -0
- package/lib/commonjs/config/runtimeTenantMode.web.js +9 -0
- package/lib/commonjs/config/tenantMode.js +68 -0
- package/lib/commonjs/connections/PowerPlatformConnectionApi.js +24 -27
- package/lib/commonjs/connections/connectionMetadata.js +2 -2
- package/lib/commonjs/connections/resolveConnections.js +8 -3
- package/lib/commonjs/connections/types.js +9 -2
- package/lib/commonjs/connections/useConnectionRefs.js +19 -10
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/platform/CanvasAppResolution.js +33 -0
- package/lib/commonjs/platform/CanvasAppResolutionApi.js +69 -0
- package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +50 -13
- package/lib/commonjs/platform/GlobalDiscovery.js +40 -1
- package/lib/commonjs/provider/PowerAppsHostProvider.js +47 -8
- package/lib/commonjs/provider/PowerAppsProvider.js +172 -6
- package/lib/commonjs/target/DefaultPowerAppsExperience.js +564 -0
- package/lib/commonjs/target/PowerAppsExperience.js +41 -0
- package/lib/commonjs/target/PowerAppsTargetBootstrap.js +534 -0
- package/lib/commonjs/target/errors.js +24 -0
- package/lib/commonjs/target/types.js +1 -0
- package/lib/module/auth/AuthContext.js +49 -22
- package/lib/module/auth/targetAuthorization.js +48 -0
- package/lib/module/bridge/NativePowerAppsBridge.js +33 -2
- package/lib/module/bridge/PowerPagesBridge.js +1 -3
- package/lib/module/bridge/setupNativeHost.js +5 -0
- package/lib/module/config/expoConfig.js +14 -0
- package/lib/module/config/runtimeTenantMode.js +12 -0
- package/lib/module/config/runtimeTenantMode.web.js +5 -0
- package/lib/module/config/tenantMode.js +61 -0
- package/lib/module/connections/PowerPlatformConnectionApi.js +24 -27
- package/lib/module/connections/connectionMetadata.js +2 -2
- package/lib/module/connections/resolveConnections.js +9 -4
- package/lib/module/connections/types.js +9 -2
- package/lib/module/connections/useConnectionRefs.js +20 -11
- package/lib/module/index.js +4 -1
- package/lib/module/platform/CanvasAppResolution.js +27 -0
- package/lib/module/platform/CanvasAppResolutionApi.js +65 -0
- package/lib/module/platform/DataverseGlobalDiscoveryApi.js +49 -14
- package/lib/module/platform/GlobalDiscovery.js +37 -1
- package/lib/module/provider/PowerAppsHostProvider.js +47 -8
- package/lib/module/provider/PowerAppsProvider.js +176 -10
- package/lib/module/target/DefaultPowerAppsExperience.js +559 -0
- package/lib/module/target/PowerAppsExperience.js +34 -0
- package/lib/module/target/PowerAppsTargetBootstrap.js +529 -0
- package/lib/module/target/errors.js +19 -0
- package/lib/module/target/types.js +1 -0
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +11 -2
- package/lib/typescript/commonjs/auth/targetAuthorization.d.ts +15 -0
- package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +6 -0
- package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +2 -0
- package/lib/typescript/commonjs/config/runtimeTenantMode.d.ts +3 -0
- package/lib/typescript/commonjs/config/runtimeTenantMode.web.d.ts +3 -0
- package/lib/typescript/commonjs/config/tenantMode.d.ts +14 -0
- package/lib/typescript/commonjs/connections/PowerPlatformConnectionApi.d.ts +5 -2
- package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +1 -1
- package/lib/typescript/commonjs/connections/types.d.ts +7 -1
- package/lib/typescript/commonjs/connections/useConnectionRefs.d.ts +11 -2
- package/lib/typescript/commonjs/index.d.ts +5 -1
- package/lib/typescript/commonjs/platform/CanvasAppResolution.d.ts +4 -0
- package/lib/typescript/commonjs/platform/CanvasAppResolutionApi.d.ts +6 -0
- package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
- package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +12 -0
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +9 -1
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +13 -15
- package/lib/typescript/commonjs/target/DefaultPowerAppsExperience.d.ts +6 -0
- package/lib/typescript/commonjs/target/PowerAppsExperience.d.ts +61 -0
- package/lib/typescript/commonjs/target/PowerAppsTargetBootstrap.d.ts +15 -0
- package/lib/typescript/commonjs/target/errors.d.ts +13 -0
- package/lib/typescript/commonjs/target/types.d.ts +34 -0
- package/lib/typescript/module/auth/AuthContext.d.ts +11 -2
- package/lib/typescript/module/auth/targetAuthorization.d.ts +15 -0
- package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +6 -0
- package/lib/typescript/module/bridge/setupNativeHost.d.ts +2 -0
- package/lib/typescript/module/config/runtimeTenantMode.d.ts +3 -0
- package/lib/typescript/module/config/runtimeTenantMode.web.d.ts +3 -0
- package/lib/typescript/module/config/tenantMode.d.ts +14 -0
- package/lib/typescript/module/connections/PowerPlatformConnectionApi.d.ts +5 -2
- package/lib/typescript/module/connections/connectionMetadata.d.ts +1 -1
- package/lib/typescript/module/connections/types.d.ts +7 -1
- package/lib/typescript/module/connections/useConnectionRefs.d.ts +11 -2
- package/lib/typescript/module/index.d.ts +5 -1
- package/lib/typescript/module/platform/CanvasAppResolution.d.ts +4 -0
- package/lib/typescript/module/platform/CanvasAppResolutionApi.d.ts +6 -0
- package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
- package/lib/typescript/module/platform/GlobalDiscovery.d.ts +12 -0
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +9 -1
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +13 -15
- package/lib/typescript/module/target/DefaultPowerAppsExperience.d.ts +6 -0
- package/lib/typescript/module/target/PowerAppsExperience.d.ts +61 -0
- package/lib/typescript/module/target/PowerAppsTargetBootstrap.d.ts +15 -0
- package/lib/typescript/module/target/errors.d.ts +13 -0
- package/lib/typescript/module/target/types.d.ts +34 -0
- package/package.json +12 -5
- package/scripts/build-codegen-package.js +34 -3
- package/scripts/dev.js +405 -0
- package/scripts/upgrade-authoring.js +85 -28
- package/scripts/wrap.js +42 -5
- package/src/config/generated-config.d.ts +21 -0
package/scripts/dev.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const { createConnection, createServer, isIP } = require('node:net');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { setTimeout: delay } = require('node:timers/promises');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PORT = 8081;
|
|
10
|
+
|
|
11
|
+
function parseArgs(args) {
|
|
12
|
+
const expoArgs = [];
|
|
13
|
+
let tunnel = false;
|
|
14
|
+
let tenantId;
|
|
15
|
+
|
|
16
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
+
const argument = args[index];
|
|
18
|
+
if (argument === '--tunnel') {
|
|
19
|
+
tunnel = true;
|
|
20
|
+
} else if (argument === '--tunnel-tenant') {
|
|
21
|
+
tenantId = args[index + 1];
|
|
22
|
+
if (!tenantId || tenantId.startsWith('--')) {
|
|
23
|
+
throw new Error('--tunnel-tenant requires an Entra tenant ID.');
|
|
24
|
+
}
|
|
25
|
+
index += 1;
|
|
26
|
+
} else {
|
|
27
|
+
expoArgs.push(argument);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const portIndex = expoArgs.findIndex(argument => argument === '--port' || argument === '-p');
|
|
32
|
+
const inlinePort = expoArgs.find(argument => argument.startsWith('--port='));
|
|
33
|
+
const port = portIndex >= 0
|
|
34
|
+
? Number(expoArgs[portIndex + 1])
|
|
35
|
+
: inlinePort
|
|
36
|
+
? Number(inlinePort.slice('--port='.length))
|
|
37
|
+
: DEFAULT_PORT;
|
|
38
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error('Expo port must be an integer between 1 and 65535.');
|
|
40
|
+
}
|
|
41
|
+
if (tenantId && !tunnel) {
|
|
42
|
+
throw new Error('--tunnel-tenant requires --tunnel.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { expoArgs, port, tenantId, tunnel };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildLaunchUrl(tunnelUrl, connectToken) {
|
|
49
|
+
const endpoint = new URL(tunnelUrl);
|
|
50
|
+
if (endpoint.protocol !== 'https:' || !endpoint.hostname.endsWith('.devtunnels.ms')) {
|
|
51
|
+
throw new Error(`Unexpected Dev Tunnels endpoint: ${tunnelUrl}`);
|
|
52
|
+
}
|
|
53
|
+
if (!connectToken) {
|
|
54
|
+
throw new Error('Dev Tunnels did not return a connect token.');
|
|
55
|
+
}
|
|
56
|
+
const launchUrl = new URL('powerapps-dev-player://launch');
|
|
57
|
+
launchUrl.searchParams.set('url', endpoint.toString());
|
|
58
|
+
launchUrl.searchParams.set('tunnelToken', connectToken);
|
|
59
|
+
return launchUrl.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function selectTunnelUrl(portForwardingUris) {
|
|
63
|
+
const tunnelUrl = portForwardingUris?.find(uri => {
|
|
64
|
+
try {
|
|
65
|
+
const endpoint = new URL(uri);
|
|
66
|
+
return endpoint.protocol === 'https:'
|
|
67
|
+
&& (!endpoint.port || endpoint.port === '443')
|
|
68
|
+
&& endpoint.hostname.endsWith('.devtunnels.ms');
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!tunnelUrl) {
|
|
74
|
+
throw new Error('Dev Tunnels did not return an HTTPS forwarding URL on port 443.');
|
|
75
|
+
}
|
|
76
|
+
return tunnelUrl;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function usesLanHost(expoArgs) {
|
|
80
|
+
if (expoArgs.includes('--localhost') || expoArgs.includes('--offline')) return false;
|
|
81
|
+
if (expoArgs.includes('--lan')) return true;
|
|
82
|
+
|
|
83
|
+
const hostIndex = expoArgs.indexOf('--host');
|
|
84
|
+
const inlineHost = expoArgs.find(argument => argument.startsWith('--host='));
|
|
85
|
+
const host = hostIndex >= 0
|
|
86
|
+
? expoArgs[hostIndex + 1]
|
|
87
|
+
: inlineHost?.slice('--host='.length);
|
|
88
|
+
return host ? host === 'lan' : true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveLanUrl(port, resolveNetwork) {
|
|
92
|
+
const lanNetwork = resolveNetwork ?? require('lan-network').lanNetwork;
|
|
93
|
+
const { address } = await lanNetwork();
|
|
94
|
+
const addressFamily = isIP(address);
|
|
95
|
+
if (!addressFamily || address === '127.0.0.1' || address === '::1') {
|
|
96
|
+
throw new Error('Could not determine a LAN address. Use --tunnel or pass --host localhost.');
|
|
97
|
+
}
|
|
98
|
+
const hostname = addressFamily === 6 ? `[${address}]` : address;
|
|
99
|
+
return `http://${hostname}:${port}/`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startExpo(projectRoot, expoArgs, environment = {}, spawnProcess = spawn) {
|
|
103
|
+
const expoCli = require.resolve('expo/bin/cli', { paths: [projectRoot] });
|
|
104
|
+
return spawnProcess(process.execPath, [expoCli, 'start', ...expoArgs], {
|
|
105
|
+
cwd: projectRoot,
|
|
106
|
+
env: { ...process.env, ...environment },
|
|
107
|
+
stdio: 'inherit',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasExplicitPort(expoArgs) {
|
|
112
|
+
return expoArgs.some(argument => argument === '--port' || argument === '-p' || argument.startsWith('--port='));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isPortAvailable(port) {
|
|
116
|
+
return new Promise(resolve => {
|
|
117
|
+
const server = createServer();
|
|
118
|
+
server.once('error', () => resolve(false));
|
|
119
|
+
server.listen(port, () => server.close(() => resolve(true)));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function resolveMetroOptions(options, checkPort = isPortAvailable) {
|
|
124
|
+
if (hasExplicitPort(options.expoArgs)) {
|
|
125
|
+
if (!await checkPort(options.port)) {
|
|
126
|
+
throw new Error(`Expo port ${options.port} is already in use. Choose another port with --port.`);
|
|
127
|
+
}
|
|
128
|
+
return { expoArgs: options.expoArgs, port: options.port };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let port = options.port; port <= 65535; port += 1) {
|
|
132
|
+
if (await checkPort(port)) {
|
|
133
|
+
return { expoArgs: [...options.expoArgs, '--port', String(port)], port };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`No available Expo port was found at or above ${options.port}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function waitForMetro(port, expo, timeoutMs = 60_000) {
|
|
140
|
+
const deadline = Date.now() + timeoutMs;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
if (expo.exitCode !== null) {
|
|
143
|
+
throw new Error(`Expo exited before Metro started on port ${port}.`);
|
|
144
|
+
}
|
|
145
|
+
const connected = await new Promise(resolve => {
|
|
146
|
+
const socket = createConnection({ host: '127.0.0.1', port });
|
|
147
|
+
socket.once('connect', () => {
|
|
148
|
+
socket.destroy();
|
|
149
|
+
resolve(true);
|
|
150
|
+
});
|
|
151
|
+
socket.once('error', () => resolve(false));
|
|
152
|
+
});
|
|
153
|
+
if (connected) return;
|
|
154
|
+
await delay(100);
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Timed out waiting for Metro to start on port ${port}.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function startExpoThenTunnel(startMetro, waitUntilReady, createTunnel) {
|
|
160
|
+
const expo = startMetro();
|
|
161
|
+
try {
|
|
162
|
+
await waitUntilReady(expo);
|
|
163
|
+
return { expo, tunnelState: await createTunnel() };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
expo.kill?.('SIGTERM');
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function startLan(projectRoot, options, resolveUrl = resolveLanUrl, spawnProcess = spawn) {
|
|
171
|
+
const qrcode = require('qrcode-terminal');
|
|
172
|
+
const lanUrl = await resolveUrl(options.port);
|
|
173
|
+
console.log(`\nLAN development server: ${lanUrl}`);
|
|
174
|
+
console.log('Scan this QR code with Power Apps Dev Player:');
|
|
175
|
+
qrcode.generate(lanUrl, { small: true });
|
|
176
|
+
console.log();
|
|
177
|
+
return startExpo(projectRoot, options.expoArgs, {
|
|
178
|
+
EXPO_NO_QR_CODE: '1',
|
|
179
|
+
EXPO_PACKAGER_PROXY_URL: lanUrl,
|
|
180
|
+
}, spawnProcess);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isTunnelLimitError(error) {
|
|
184
|
+
if (!error) return false;
|
|
185
|
+
const text = [
|
|
186
|
+
error.message,
|
|
187
|
+
error.response?.data?.detail,
|
|
188
|
+
error.response?.data?.title,
|
|
189
|
+
typeof error.response?.data === 'string' ? error.response.data : '',
|
|
190
|
+
]
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.join(' ')
|
|
193
|
+
.toLowerCase();
|
|
194
|
+
|
|
195
|
+
return /tunnelsperuser(?:percluster)?/.test(text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getTunnelCreatedTime(tunnel) {
|
|
199
|
+
if (!tunnel?.created) return 0;
|
|
200
|
+
const time = new Date(tunnel.created).getTime();
|
|
201
|
+
return Number.isNaN(time) ? 0 : time;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getTunnelHostConnectionCount(tunnel) {
|
|
205
|
+
const count = tunnel?.status?.hostConnectionCount;
|
|
206
|
+
if (typeof count === 'number') return count;
|
|
207
|
+
return typeof count?.current === 'number' ? count.current : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function deleteOlderTunnel(managementClient, tunnels) {
|
|
211
|
+
if (!Array.isArray(tunnels) || tunnels.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const candidates = tunnels
|
|
216
|
+
.filter(t => Array.isArray(t.labels)
|
|
217
|
+
&& t.labels.includes('power-apps-native')
|
|
218
|
+
&& getTunnelHostConnectionCount(t) === 0)
|
|
219
|
+
.sort((a, b) => getTunnelCreatedTime(a) - getTunnelCreatedTime(b));
|
|
220
|
+
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
try {
|
|
223
|
+
const result = await managementClient.deleteTunnel(candidate);
|
|
224
|
+
if (result === true) return candidate;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.warn(
|
|
227
|
+
`[power-apps-dev] Failed to delete idle managed tunnel (${candidate.tunnelId || candidate.name || 'unnamed'}).`,
|
|
228
|
+
error,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function createTunnelWithLimitHandling(managementClient, tunnelDefinition, options) {
|
|
237
|
+
try {
|
|
238
|
+
return await managementClient.createTunnel(tunnelDefinition, options);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!isTunnelLimitError(error) || !tunnelDefinition.clusterId) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let tunnels;
|
|
245
|
+
try {
|
|
246
|
+
tunnels = await managementClient.listTunnels(tunnelDefinition.clusterId);
|
|
247
|
+
} catch (listError) {
|
|
248
|
+
console.warn('[power-apps-dev] Failed to list tunnels during quota recovery.', listError);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
const deleted = await deleteOlderTunnel(managementClient, tunnels);
|
|
252
|
+
if (!deleted) {
|
|
253
|
+
console.warn('[power-apps-dev] No idle managed tunnel was available; skipping tunnel deletion.');
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.warn(
|
|
258
|
+
`[power-apps-dev] Dev Tunnel limit reached. Deleted older tunnel (${deleted.tunnelId || deleted.name || 'unnamed'}) to make room.`,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return await managementClient.createTunnel(tunnelDefinition, options);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function startAuthenticatedTunnel(projectRoot, options) {
|
|
266
|
+
const { AzureCliCredential } = require('@azure/identity');
|
|
267
|
+
const {
|
|
268
|
+
TunnelAccessScopes,
|
|
269
|
+
TunnelProtocol,
|
|
270
|
+
TunnelServiceProperties,
|
|
271
|
+
} = require('@microsoft/dev-tunnels-contracts');
|
|
272
|
+
const {
|
|
273
|
+
ManagementApiVersions,
|
|
274
|
+
TunnelAccessTokenProperties,
|
|
275
|
+
TunnelManagementHttpClient,
|
|
276
|
+
} = require('@microsoft/dev-tunnels-management');
|
|
277
|
+
const { TunnelRelayTunnelHost } = require('@microsoft/dev-tunnels-connections');
|
|
278
|
+
const qrcode = require('qrcode-terminal');
|
|
279
|
+
const packageJson = require('../package.json');
|
|
280
|
+
|
|
281
|
+
const metroOptions = await resolveMetroOptions(options);
|
|
282
|
+
const credential = new AzureCliCredential(options.tenantId ? { tenantId: options.tenantId } : {});
|
|
283
|
+
const scope = `${TunnelServiceProperties.production.serviceAppId}/.default`;
|
|
284
|
+
const getAuthorization = async () => {
|
|
285
|
+
const result = await credential.getToken(scope);
|
|
286
|
+
if (!result?.token) {
|
|
287
|
+
throw new Error('Azure CLI did not return a Dev Tunnels access token. Run `az login` and try again.');
|
|
288
|
+
}
|
|
289
|
+
return `Bearer ${result.token}`;
|
|
290
|
+
};
|
|
291
|
+
const managementClient = new TunnelManagementHttpClient(
|
|
292
|
+
[{ name: packageJson.name, version: packageJson.version }],
|
|
293
|
+
ManagementApiVersions.Version20230927preview,
|
|
294
|
+
getAuthorization,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
let tunnel;
|
|
298
|
+
let host;
|
|
299
|
+
let expo;
|
|
300
|
+
let cleaningUp = false;
|
|
301
|
+
const cleanup = async () => {
|
|
302
|
+
if (cleaningUp) return;
|
|
303
|
+
cleaningUp = true;
|
|
304
|
+
expo?.kill('SIGTERM');
|
|
305
|
+
await host?.dispose().catch(() => undefined);
|
|
306
|
+
if (tunnel) await managementClient.deleteTunnel(tunnel).catch(() => undefined);
|
|
307
|
+
await managementClient.dispose().catch(() => undefined);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const started = await startExpoThenTunnel(
|
|
312
|
+
() => startExpo(projectRoot, metroOptions.expoArgs, { EXPO_NO_QR_CODE: '1' }),
|
|
313
|
+
child => waitForMetro(metroOptions.port, child),
|
|
314
|
+
async () => {
|
|
315
|
+
tunnel = await createTunnelWithLimitHandling(
|
|
316
|
+
managementClient,
|
|
317
|
+
{ labels: ['power-apps-native', 'development'] },
|
|
318
|
+
{ tokenScopes: [TunnelAccessScopes.Host] },
|
|
319
|
+
);
|
|
320
|
+
const tunnelPort = await managementClient.createTunnelPort(
|
|
321
|
+
tunnel,
|
|
322
|
+
{ portNumber: metroOptions.port, protocol: TunnelProtocol.Http, isDefault: true },
|
|
323
|
+
{ tokenScopes: [TunnelAccessScopes.Connect] },
|
|
324
|
+
);
|
|
325
|
+
tunnel.ports = [tunnelPort];
|
|
326
|
+
host = new TunnelRelayTunnelHost(managementClient);
|
|
327
|
+
await host.connect(tunnel);
|
|
328
|
+
return tunnelPort;
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
expo = started.expo;
|
|
332
|
+
const tunnelPort = started.tunnelState;
|
|
333
|
+
|
|
334
|
+
const tunnelUrl = selectTunnelUrl(tunnelPort.portForwardingUris);
|
|
335
|
+
const connectToken = TunnelAccessTokenProperties.getTunnelAccessToken(
|
|
336
|
+
tunnelPort,
|
|
337
|
+
TunnelAccessScopes.Connect,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const launchUrl = buildLaunchUrl(tunnelUrl, connectToken);
|
|
341
|
+
console.log(`\nAuthenticated Dev Tunnel: ${tunnelUrl}`);
|
|
342
|
+
console.log('Scan this QR code with Power Apps Dev Player:');
|
|
343
|
+
qrcode.generate(launchUrl, { small: true });
|
|
344
|
+
console.log('\nThe tunnel is private and will be deleted when this process exits.\n');
|
|
345
|
+
|
|
346
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
347
|
+
const signals = ['SIGINT', 'SIGTERM'];
|
|
348
|
+
const finish = result => {
|
|
349
|
+
for (const signal of signals) process.removeListener(signal, stop);
|
|
350
|
+
resolve(result);
|
|
351
|
+
};
|
|
352
|
+
const stop = () => {
|
|
353
|
+
expo.kill('SIGTERM');
|
|
354
|
+
finish(0);
|
|
355
|
+
};
|
|
356
|
+
for (const signal of signals) process.once(signal, stop);
|
|
357
|
+
expo.once('error', error => {
|
|
358
|
+
for (const signal of signals) process.removeListener(signal, stop);
|
|
359
|
+
reject(error);
|
|
360
|
+
});
|
|
361
|
+
expo.once('exit', code => finish(code ?? 1));
|
|
362
|
+
});
|
|
363
|
+
await cleanup();
|
|
364
|
+
return exitCode;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
await cleanup();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function main() {
|
|
372
|
+
const options = parseArgs(process.argv.slice(2));
|
|
373
|
+
const projectRoot = process.cwd();
|
|
374
|
+
if (!options.tunnel) {
|
|
375
|
+
const expo = usesLanHost(options.expoArgs)
|
|
376
|
+
? await startLan(projectRoot, options)
|
|
377
|
+
: startExpo(projectRoot, options.expoArgs);
|
|
378
|
+
expo.once('exit', code => process.exit(code ?? 1));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
process.exitCode = await startAuthenticatedTunnel(projectRoot, options);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (require.main === module) {
|
|
386
|
+
main().catch(error => {
|
|
387
|
+
console.error(`[power-apps-dev] ${error instanceof Error ? error.message : String(error)}`);
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
buildLaunchUrl,
|
|
394
|
+
createTunnelWithLimitHandling,
|
|
395
|
+
deleteOlderTunnel,
|
|
396
|
+
isTunnelLimitError,
|
|
397
|
+
parseArgs,
|
|
398
|
+
resolveMetroOptions,
|
|
399
|
+
resolveLanUrl,
|
|
400
|
+
selectTunnelUrl,
|
|
401
|
+
startExpo,
|
|
402
|
+
startExpoThenTunnel,
|
|
403
|
+
startLan,
|
|
404
|
+
usesLanHost,
|
|
405
|
+
};
|
|
@@ -10,7 +10,7 @@ const { HOST_PACKAGE, TEMPLATE_VERSION_PATH, assertSafeFilePath, hashContent, is
|
|
|
10
10
|
|
|
11
11
|
const AUTHORING_LOCK_PATH = 'packages/power-apps-native-host/compatibility/.migration-build.lock';
|
|
12
12
|
const AUTHORING_JOURNAL_PATH = 'packages/power-apps-native-host/compatibility/.migration-build-journal.json';
|
|
13
|
-
const TEMPLATE_PATH = '
|
|
13
|
+
const TEMPLATE_PATH = 'packages/power-apps-native-template/template';
|
|
14
14
|
const TSCONFIG_PATH = 'tsconfig.json';
|
|
15
15
|
const GITIGNORE_PATH = '.gitignore';
|
|
16
16
|
const GITIGNORE_START = '# POWER APPS MANAGED START - DO NOT REMOVE OR RENAME THE COMMENT';
|
|
@@ -18,10 +18,14 @@ const GITIGNORE_END = '# POWER APPS MANAGED END - DO NOT REMOVE OR RENAME THE CO
|
|
|
18
18
|
const MANAGED_PACKAGE_FIELDS = ['main'];
|
|
19
19
|
const PROFILE_PATH = 'packages/power-apps-native-host/compatibility/current.json';
|
|
20
20
|
const TEMPLATE_BASELINE_TAG_PREFIX = 'standalone-template-migration-baseline-v';
|
|
21
|
-
const LEGACY_TEMPLATE_BASELINE_TAG_PATTERN = /^template-migration-baseline-v\d+$/;
|
|
22
21
|
const RELEASE_HOST_VERSION = '__RELEASE_HOST_VERSION__';
|
|
23
22
|
const NATIVE_RUNTIME_FINGERPRINT_SCRIPT = 'scripts/repository/native-runtime-fingerprint.js';
|
|
24
23
|
const NATIVE_RUNTIME_PLATFORMS = ['android', 'ios'];
|
|
24
|
+
const SCAFFOLD_ONLY_TEMPLATE_PATHS = new Set([
|
|
25
|
+
'AGENTS.md',
|
|
26
|
+
'CLAUDE.md',
|
|
27
|
+
'.github/copilot-instructions.md',
|
|
28
|
+
]);
|
|
25
29
|
const CUSTOMER_SECTIONS = [
|
|
26
30
|
{
|
|
27
31
|
name: 'customer app settings',
|
|
@@ -72,33 +76,84 @@ function inferBuildBaseTag(repositoryRoot) {
|
|
|
72
76
|
if (!Number.isInteger(profile.targetTemplateVersion)) throw new Error('Current compatibility profile has an invalid targetTemplateVersion');
|
|
73
77
|
const currentTag = templateBaselineTag(profile.targetTemplateVersion);
|
|
74
78
|
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
79
|
throw new Error(`Could not infer template migration baseline. Expected ${currentTag}.`);
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
function getTemplateChanges(repositoryRoot, fromTag) {
|
|
87
83
|
const base = resolveVersionTag(repositoryRoot, fromTag);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
.
|
|
92
|
-
|
|
84
|
+
const basePaths = templatePathsAtCommit(repositoryRoot, base.commit);
|
|
85
|
+
const targetPaths = templatePathsAtWorktree(repositoryRoot);
|
|
86
|
+
const paths = [...new Set([...basePaths, ...targetPaths])]
|
|
87
|
+
.filter(relativePath => !isScaffoldOnlyTemplatePath(relativePath))
|
|
88
|
+
.filter(relativePath => {
|
|
89
|
+
const baseContent = readGitFile(repositoryRoot, base.commit, relativePath);
|
|
90
|
+
const targetContent = readTemplateFile(repositoryRoot, relativePath);
|
|
91
|
+
return !buffersEqual(baseContent, targetContent)
|
|
92
|
+
|| normalizedFileMode(fileMode(repositoryRoot, base.commit, relativePath))
|
|
93
|
+
!== normalizedFileMode(fileMode(repositoryRoot, null, relativePath));
|
|
94
|
+
})
|
|
95
|
+
.sort();
|
|
93
96
|
return {
|
|
94
97
|
baseTag: base.tag,
|
|
95
98
|
baseCommit: base.commit,
|
|
96
|
-
files: paths.map(
|
|
97
|
-
return { path: filePath.slice(`${TEMPLATE_PATH}/`.length) };
|
|
98
|
-
}),
|
|
99
|
+
files: paths.map(relativePath => ({ path: relativePath })),
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
function isScaffoldOnlyTemplatePath(relativePath) {
|
|
104
|
+
// Git uses '/', while a native Windows caller may supply '\'. Keep exact-path matching.
|
|
105
|
+
return SCAFFOLD_ONLY_TEMPLATE_PATHS.has(relativePath.replace(/\\/g, '/'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function storedTemplatePath(relativePath) {
|
|
109
|
+
return relativePath === GITIGNORE_PATH ? 'gitignore' : relativePath;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function logicalTemplatePath(relativePath) {
|
|
113
|
+
return relativePath === 'gitignore' ? GITIGNORE_PATH : relativePath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function templatePathsAtCommit(repositoryRoot, commit) {
|
|
117
|
+
const paths = git(repositoryRoot, ['ls-tree', '-r', '--name-only', commit, '--', TEMPLATE_PATH])
|
|
118
|
+
.trim().split('\n').filter(Boolean)
|
|
119
|
+
.map(filePath => filePath.slice(`${TEMPLATE_PATH}/`.length))
|
|
120
|
+
.map(logicalTemplatePath)
|
|
121
|
+
.sort();
|
|
122
|
+
if (!paths.includes(TEMPLATE_VERSION_PATH)) {
|
|
123
|
+
throw new Error(`Commit ${commit} does not contain the standalone template at ${TEMPLATE_PATH}`);
|
|
124
|
+
}
|
|
125
|
+
return paths;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function templatePathsAtWorktree(repositoryRoot) {
|
|
129
|
+
const tracked = git(repositoryRoot, ['ls-files', '--', TEMPLATE_PATH])
|
|
130
|
+
.trim().split('\n').filter(Boolean);
|
|
131
|
+
const untracked = git(repositoryRoot, ['ls-files', '--others', '--exclude-standard', '--', TEMPLATE_PATH])
|
|
132
|
+
.trim().split('\n').filter(Boolean);
|
|
133
|
+
return [...new Set([...tracked, ...untracked])]
|
|
134
|
+
.map(filePath => filePath.slice(`${TEMPLATE_PATH}/`.length))
|
|
135
|
+
.map(logicalTemplatePath)
|
|
136
|
+
.sort();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readTemplateFile(repositoryRoot, relativePath) {
|
|
140
|
+
const filePath = path.join(repositoryRoot, TEMPLATE_PATH, storedTemplatePath(relativePath));
|
|
141
|
+
const stat = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
142
|
+
if (!stat) return null;
|
|
143
|
+
// Compare link text, never follow a template link outside the canonical tree.
|
|
144
|
+
return stat.isSymbolicLink() ? Buffer.from(fs.readlinkSync(filePath)) : fs.readFileSync(filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buffersEqual(left, right) {
|
|
148
|
+
if (left === null || right === null) return left === right;
|
|
149
|
+
return left.equals(right);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizedFileMode(mode) {
|
|
153
|
+
// Strip only the regular-file type prefix; retain symlink/submodule types.
|
|
154
|
+
return mode?.replace(/^100/, '000') || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
function assertNoCustomerOwnedChanges(files) {
|
|
103
158
|
const protectedFiles = files.filter(file => isCustomerOwnedPath(file.path));
|
|
104
159
|
if (protectedFiles.length) {
|
|
@@ -107,18 +162,22 @@ function assertNoCustomerOwnedChanges(files) {
|
|
|
107
162
|
}
|
|
108
163
|
|
|
109
164
|
function readGitFile(repositoryRoot, commit, relativeTemplatePath) {
|
|
110
|
-
const
|
|
165
|
+
const storedPath = storedTemplatePath(relativeTemplatePath);
|
|
166
|
+
const result = childProcess.spawnSync('git', ['show', `${commit}:${TEMPLATE_PATH}/${storedPath}`], { cwd: repositoryRoot, encoding: null });
|
|
111
167
|
if (result.error) throw result.error;
|
|
112
168
|
return result.status === 0 ? result.stdout : null;
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
function fileMode(repositoryRoot, commit, relativePath) {
|
|
116
172
|
if (commit) {
|
|
117
|
-
const
|
|
173
|
+
const storedPath = storedTemplatePath(relativePath);
|
|
174
|
+
const output = git(repositoryRoot, ['ls-tree', commit, `${TEMPLATE_PATH}/${storedPath}`], true);
|
|
118
175
|
return output?.trim().split(/\s+/)[0] || null;
|
|
119
176
|
}
|
|
120
|
-
const absolutePath = path.join(repositoryRoot, TEMPLATE_PATH, relativePath);
|
|
121
|
-
|
|
177
|
+
const absolutePath = path.join(repositoryRoot, TEMPLATE_PATH, storedTemplatePath(relativePath));
|
|
178
|
+
const stat = fs.lstatSync(absolutePath, { throwIfNoEntry: false });
|
|
179
|
+
if (!stat) return null;
|
|
180
|
+
return stat.isSymbolicLink() ? '120000' : (stat.mode & 0o777).toString(8).padStart(6, '0');
|
|
122
181
|
}
|
|
123
182
|
|
|
124
183
|
async function runAuthoringCommand(command, startPath, options) {
|
|
@@ -360,10 +419,7 @@ function buildMigrationStep(repositoryRoot, options) {
|
|
|
360
419
|
const fromTag = options.fromTag || inferBuildBaseTag(repositoryRoot);
|
|
361
420
|
const templateChanges = getTemplateChanges(repositoryRoot, fromTag);
|
|
362
421
|
const base = { tag: templateChanges.baseTag, commit: templateChanges.baseCommit };
|
|
363
|
-
const
|
|
364
|
-
const migrationFiles = isLegacyBootstrap
|
|
365
|
-
? templateChanges.files.filter(file => !isCustomerOwnedPath(file.path))
|
|
366
|
-
: templateChanges.files;
|
|
422
|
+
const migrationFiles = templateChanges.files;
|
|
367
423
|
assertNoCustomerOwnedChanges(migrationFiles);
|
|
368
424
|
if (!migrationFiles.length) {
|
|
369
425
|
if (options.skipIfNoChanges) {
|
|
@@ -407,8 +463,8 @@ function buildMigrationStep(repositoryRoot, options) {
|
|
|
407
463
|
let gitignoreTransition = null;
|
|
408
464
|
for (const relativePath of paths) {
|
|
409
465
|
const baseContent = readGitFile(repositoryRoot, base.commit, relativePath);
|
|
410
|
-
const targetPath = path.join(repositoryRoot, TEMPLATE_PATH, relativePath);
|
|
411
|
-
if (fs.
|
|
466
|
+
const targetPath = path.join(repositoryRoot, TEMPLATE_PATH, storedTemplatePath(relativePath));
|
|
467
|
+
if (fs.lstatSync(targetPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
412
468
|
throw new Error(`Symbolic link template change ${relativePath} is not supported`);
|
|
413
469
|
}
|
|
414
470
|
const targetContent = fs.existsSync(targetPath) ? fs.readFileSync(targetPath) : null;
|
|
@@ -622,6 +678,7 @@ module.exports = {
|
|
|
622
678
|
buildPackageTransitions,
|
|
623
679
|
buildTsconfigTransitions,
|
|
624
680
|
getTemplateChanges,
|
|
681
|
+
isScaffoldOnlyTemplatePath,
|
|
625
682
|
nextNativeRuntime,
|
|
626
683
|
runAuthoringCommand,
|
|
627
684
|
updateProfileFromPackage,
|
package/scripts/wrap.js
CHANGED
|
@@ -52,6 +52,11 @@ const { execFileSync } = require('child_process');
|
|
|
52
52
|
const path = require('path');
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const os = require('os');
|
|
55
|
+
const {
|
|
56
|
+
MULTI_TENANT_AUTHORITY,
|
|
57
|
+
resolveMultiTenantConfig,
|
|
58
|
+
resolveTenantMode,
|
|
59
|
+
} = require('../lib/commonjs/config/tenantMode.js');
|
|
55
60
|
|
|
56
61
|
// ─── Parse args ───────────────────────────────────────────────────────────────
|
|
57
62
|
const projectRoot = process.cwd();
|
|
@@ -62,6 +67,23 @@ if (platform !== 'android' && platform !== 'ios') {
|
|
|
62
67
|
process.exit(1);
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
const appConfigPath = path.join(projectRoot, 'app.json');
|
|
71
|
+
if (!fs.existsSync(appConfigPath)) {
|
|
72
|
+
console.error('✗ app.json not found.');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let appConfig;
|
|
77
|
+
let tenantMode;
|
|
78
|
+
try {
|
|
79
|
+
appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8'));
|
|
80
|
+
tenantMode = resolveTenantMode(appConfig);
|
|
81
|
+
if (tenantMode === 'multi') resolveMultiTenantConfig(appConfig);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(`✗ Invalid app.json Power Apps build configuration: ${e.message}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
// ─── Load wrap.config.json ────────────────────────────────────────────────────
|
|
66
88
|
const configPath = path.join(projectRoot, 'wrap.config.json');
|
|
67
89
|
if (!fs.existsSync(configPath)) {
|
|
@@ -86,17 +108,27 @@ for (const field of ['bundleIdentifier', 'displayName']) {
|
|
|
86
108
|
process.exit(1);
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
|
-
if (!config.msal?.clientId
|
|
90
|
-
console.error('✗ wrap.config.json: missing required
|
|
111
|
+
if (!config.msal?.clientId) {
|
|
112
|
+
console.error('✗ wrap.config.json: missing required field msal.clientId');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (tenantMode === 'single' && !config.msal.tenantId) {
|
|
116
|
+
console.error('✗ wrap.config.json: msal.tenantId is required in single-tenant mode');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (tenantMode === 'multi' && config.msal.tenantId && config.msal.tenantId !== MULTI_TENANT_AUTHORITY) {
|
|
120
|
+
console.error(`✗ wrap.config.json: msal.tenantId must be omitted or "${MULTI_TENANT_AUTHORITY}" in multi-tenant mode`);
|
|
91
121
|
process.exit(1);
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
const runtimeTenantId = tenantMode === 'multi' ? MULTI_TENANT_AUTHORITY : config.msal.tenantId;
|
|
125
|
+
|
|
94
126
|
const {
|
|
95
127
|
bundleIdentifier,
|
|
96
128
|
displayName,
|
|
97
129
|
version = '1.0.0',
|
|
98
130
|
versionCode = 1,
|
|
99
|
-
msal: { clientId
|
|
131
|
+
msal: { clientId },
|
|
100
132
|
outputPath: outputPathRel = './dist',
|
|
101
133
|
} = config;
|
|
102
134
|
|
|
@@ -132,7 +164,10 @@ function requireCommand(cmd, hint) {
|
|
|
132
164
|
// ─── Step: write auth.config.json ─────────────────────────────────────────────
|
|
133
165
|
function updateAuthConfig() {
|
|
134
166
|
const authPath = path.join(projectRoot, 'auth.config.json');
|
|
135
|
-
fs.writeFileSync(
|
|
167
|
+
fs.writeFileSync(
|
|
168
|
+
authPath,
|
|
169
|
+
JSON.stringify({ authContractVersion: 2, tenantMode, msal: { clientId, tenantId: runtimeTenantId } }, null, 2) + '\n',
|
|
170
|
+
);
|
|
136
171
|
ok(`auth.config.json updated (clientId: ${clientId})`);
|
|
137
172
|
}
|
|
138
173
|
|
|
@@ -156,7 +191,9 @@ function runPrebuild(plat) {
|
|
|
156
191
|
// ADAL / MAM — picked up by app.config.js → baked into Info.plist (iOS)
|
|
157
192
|
// and Android string resources (via the power-apps-native-host config plugin).
|
|
158
193
|
ADAL_CLIENT_ID: clientId,
|
|
159
|
-
ADAL_TENANT_ID:
|
|
194
|
+
ADAL_TENANT_ID: runtimeTenantId,
|
|
195
|
+
POWERAPPS_TENANT_MODE: tenantMode,
|
|
196
|
+
POWERAPPS_AUTH_CONTRACT_VERSION: '2',
|
|
160
197
|
// iOS auth-broker redirect URI — msauth.<bundleId>://auth
|
|
161
198
|
IOS_ADAL_REDIRECT_URI: `msauth.${bundleIdentifier}://auth`,
|
|
162
199
|
// App icon — 1024×1024 PNG; Expo generates all platform sizes at prebuild time.
|