@vrdmr/fnx-test 0.1.2 → 0.2.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 +12 -0
- package/lib/cli.js +153 -27
- package/lib/host-launcher.js +23 -10
- package/lib/host-manager.js +133 -15
- package/lib/pack.js +140 -0
- package/lib/profile-resolver.js +29 -35
- package/lib/warmup.js +3 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,9 +56,21 @@ fnx start --sku flex
|
|
|
56
56
|
fnx start --sku <sku> --scriptroot <path> # Run function app
|
|
57
57
|
fnx start --sku list # List available SKUs
|
|
58
58
|
fnx warmup [--sku <sku>] [--all] # Pre-download host + bundle
|
|
59
|
+
fnx sync [host|extensions] [--sku <sku>] # Reconcile cache with latest catalog / rollback
|
|
59
60
|
fnx templates-mcp # Start MCP server for AI agents
|
|
61
|
+
fnx pack --scriptroot <path> # Package function app as deployment zip
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
|
|
65
|
+
## Upgrades, Rollbacks, and Cache Retention
|
|
66
|
+
|
|
67
|
+
- On `fnx start`, `fnx warmup`, and `fnx sync`, fnx attempts to refresh the SKU catalog from CDN first.
|
|
68
|
+
- If catalog host version is newer than your local cache, fnx highlights that and recommends `fnx sync` (or `fnx sync host`).
|
|
69
|
+
- If catalog host version is lower than a locally cached version (service rollback), fnx warns and recommends syncing back to the supported host.
|
|
70
|
+
- `fnx sync` defaults to keeping only the latest 2 host and bundle versions to avoid cache bloat (`--keep <n>` to override).
|
|
71
|
+
- fnx also checks npm for newer fnx CLI releases and prints an upgrade tip when available.
|
|
72
|
+
- Version comparisons use numeric dot-segment precedence (e.g. `4.1047.100 > 4.1046.999`) with support for `v` prefixes and pre-release suffixes.
|
|
73
|
+
|
|
62
74
|
## MCP Server (for AI Agents)
|
|
63
75
|
|
|
64
76
|
fnx includes an MCP server that exposes Azure Functions templates to AI coding assistants:
|
package/lib/cli.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { resolve as resolvePath, dirname, join } from 'node:path';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { resolveProfile, listProfiles, setProfilesSource } from './profile-resolver.js';
|
|
6
|
-
import { ensureHost, ensureBundle } from './host-manager.js';
|
|
6
|
+
import { resolveProfile, listProfiles, setProfilesSource, fetchRegistryWithMeta } from './profile-resolver.js';
|
|
7
|
+
import { ensureHost, ensureBundle, getCachedHostVersions, compareVersions, DEFAULT_KEEP_VERSIONS } from './host-manager.js';
|
|
7
8
|
import { launchHost, createHostState } from './host-launcher.js';
|
|
8
9
|
import { startLiveMcpServer } from './live-mcp-server.js';
|
|
9
10
|
import { detectDotnetModel, printInProcessError } from './dotnet-detector.js';
|
|
11
|
+
import { detectRuntimeFromConfig, packFunctionApp } from './pack.js';
|
|
12
|
+
|
|
13
|
+
const FNX_HOME = join(homedir(), '.fnx');
|
|
14
|
+
const VERSION_CHECK_FILE = join(FNX_HOME, 'version-check.json');
|
|
15
|
+
const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
16
|
|
|
11
17
|
function isPortFree(port) {
|
|
12
18
|
return new Promise((resolve) => {
|
|
13
19
|
const srv = createServer();
|
|
14
20
|
srv.once('error', () => resolve(false));
|
|
15
|
-
|
|
21
|
+
// Host binds 0.0.0.0, so check that address (catches both IPv4 wildcard and loopback)
|
|
22
|
+
srv.listen(port, '0.0.0.0', () => { srv.close(() => resolve(true)); });
|
|
16
23
|
});
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -32,11 +39,7 @@ export async function main(args) {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
if (cmd === '-v' || cmd === '--version') {
|
|
35
|
-
const
|
|
36
|
-
const { fileURLToPath } = await import('node:url');
|
|
37
|
-
const { dirname, join } = await import('node:path');
|
|
38
|
-
const dir = dirname(fileURLToPath(import.meta.url));
|
|
39
|
-
const pkg = JSON.parse(readFileSync(join(dir, '..', 'package.json'), 'utf-8'));
|
|
42
|
+
const pkg = await getFnxPackage();
|
|
40
43
|
console.log(`fnx v${pkg.version}`);
|
|
41
44
|
process.exit(0);
|
|
42
45
|
}
|
|
@@ -52,12 +55,28 @@ export async function main(args) {
|
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
if (cmd === 'sync') {
|
|
59
|
+
await runSync(args.slice(1));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (cmd === 'pack') {
|
|
64
|
+
const scriptRoot = getFlag(args, '--scriptroot') || process.cwd();
|
|
65
|
+
const runtime = getFlag(args, '--runtime') || await detectRuntimeFromConfig(scriptRoot);
|
|
66
|
+
const outputPath = getFlag(args, '--output');
|
|
67
|
+
const noBuild = args.includes('--no-build');
|
|
68
|
+
await packFunctionApp({ scriptRoot, runtime, outputPath, noBuild });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
if (cmd !== 'start') {
|
|
56
73
|
console.error(`Unknown command: ${cmd}\n`);
|
|
57
74
|
printHelp();
|
|
58
75
|
process.exit(1);
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
await maybeWarnForCliUpgrade();
|
|
79
|
+
|
|
61
80
|
const scriptRoot = getFlag(args, '--scriptroot') || process.cwd();
|
|
62
81
|
const requestedPort = parseInt(getFlag(args, '--port') || '7071');
|
|
63
82
|
const port = await findOpenPort(requestedPort);
|
|
@@ -109,29 +128,25 @@ export async function main(args) {
|
|
|
109
128
|
} else {
|
|
110
129
|
console.log(`Resolving SKU profile: ${sku}...`);
|
|
111
130
|
}
|
|
112
|
-
|
|
131
|
+
|
|
132
|
+
const { registry, source } = await fetchRegistryWithMeta();
|
|
133
|
+
const profile = registry.profiles[sku];
|
|
134
|
+
if (!profile) {
|
|
135
|
+
const valid = Object.keys(registry.profiles).join(', ');
|
|
136
|
+
throw new Error(`Unknown SKU '${sku}'. Available: ${valid}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
profile.name = sku;
|
|
113
140
|
console.log(` Target SKU: ${profile.displayName}`);
|
|
114
141
|
console.log(` Host Version: ${profile.hostVersion}`);
|
|
115
142
|
console.log(` Extension Bundle: ${profile.extensionBundleVersion}`);
|
|
116
143
|
if (profile.maxExtensionBundleVersion) {
|
|
117
144
|
console.log(` Max Bundle Cap: ${profile.maxExtensionBundleVersion}`);
|
|
118
145
|
}
|
|
146
|
+
console.log(` Profile Source: ${source}`);
|
|
119
147
|
console.log();
|
|
120
148
|
|
|
121
|
-
//
|
|
122
|
-
const hostDir = await ensureHost(profile);
|
|
123
|
-
console.log(` Host path: ${hostDir}`);
|
|
124
|
-
|
|
125
|
-
// 3. Pre-download the correct extension bundle for this SKU
|
|
126
|
-
// This resolves the exact version from CDN index, capped by maxExtensionBundleVersion,
|
|
127
|
-
// and downloads it so the host finds it cached and never fetches a wrong version.
|
|
128
|
-
const resolvedBundleVersion = await ensureBundle(profile);
|
|
129
|
-
if (resolvedBundleVersion) {
|
|
130
|
-
console.log(` Bundle resolved: ${resolvedBundleVersion}`);
|
|
131
|
-
}
|
|
132
|
-
console.log();
|
|
133
|
-
|
|
134
|
-
// 4. Merge config: app.config.json Values + local.settings.json Values
|
|
149
|
+
// Early validation: merge config and check runtime before downloading anything
|
|
135
150
|
const mergedValues = {
|
|
136
151
|
...(appConfig?.Values || {}),
|
|
137
152
|
...(localSettings?.Values || {}),
|
|
@@ -154,7 +169,22 @@ export async function main(args) {
|
|
|
154
169
|
}
|
|
155
170
|
}
|
|
156
171
|
|
|
157
|
-
|
|
172
|
+
printHostDriftWarning(profile.hostVersion);
|
|
173
|
+
|
|
174
|
+
// 2. Ensure host is downloaded
|
|
175
|
+
const hostDir = await ensureHost(profile, { keepVersions: DEFAULT_KEEP_VERSIONS });
|
|
176
|
+
console.log(` Host path: ${hostDir}`);
|
|
177
|
+
|
|
178
|
+
// 3. Pre-download the correct extension bundle for this SKU
|
|
179
|
+
// This resolves the exact version from CDN index, capped by maxExtensionBundleVersion,
|
|
180
|
+
// and downloads it so the host finds it cached and never fetches a wrong version.
|
|
181
|
+
const resolvedBundleVersion = await ensureBundle(profile, { keepVersions: DEFAULT_KEEP_VERSIONS });
|
|
182
|
+
if (resolvedBundleVersion) {
|
|
183
|
+
console.log(` Bundle resolved: ${resolvedBundleVersion}`);
|
|
184
|
+
}
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
// 4. Create shared host state and start live MCP server
|
|
158
188
|
const hostState = createHostState();
|
|
159
189
|
|
|
160
190
|
if (!noMcp) {
|
|
@@ -200,6 +230,91 @@ export async function main(args) {
|
|
|
200
230
|
});
|
|
201
231
|
}
|
|
202
232
|
|
|
233
|
+
async function runSync(args) {
|
|
234
|
+
const target = ['host', 'extensions'].includes(args[0]) ? args[0] : 'all';
|
|
235
|
+
const force = args.includes('--force');
|
|
236
|
+
const keep = parseInt(getFlag(args, '--keep') || String(DEFAULT_KEEP_VERSIONS), 10);
|
|
237
|
+
const sku = getFlag(args, '--sku') || 'flex';
|
|
238
|
+
const profilesSource = getFlag(args, '--profiles');
|
|
239
|
+
if (profilesSource) setProfilesSource(profilesSource);
|
|
240
|
+
|
|
241
|
+
if (sku === 'list') {
|
|
242
|
+
await listProfiles();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const profile = await resolveProfile(sku);
|
|
247
|
+
profile.name = sku;
|
|
248
|
+
|
|
249
|
+
console.log(`Syncing SKU '${sku}' (${profile.displayName})...`);
|
|
250
|
+
if (target === 'all' || target === 'host') {
|
|
251
|
+
await ensureHost(profile, { force, keepVersions: keep });
|
|
252
|
+
console.log(' ✓ Host synchronized.');
|
|
253
|
+
}
|
|
254
|
+
if (target === 'all' || target === 'extensions') {
|
|
255
|
+
const bundle = await ensureBundle(profile, { force, keepVersions: keep });
|
|
256
|
+
console.log(` ✓ Extensions synchronized (${bundle || 'cached'}).`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(` Retention policy: keep latest ${keep} version(s).`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function printHostDriftWarning(targetHostVersion) {
|
|
263
|
+
const cached = getCachedHostVersions();
|
|
264
|
+
if (cached.length === 0) return;
|
|
265
|
+
|
|
266
|
+
const highest = [...cached].sort(compareVersions).pop();
|
|
267
|
+
if (!highest) return;
|
|
268
|
+
|
|
269
|
+
if (compareVersions(targetHostVersion, highest) > 0) {
|
|
270
|
+
console.log(` ℹ️ New host available: ${targetHostVersion} (local latest: ${highest}).`);
|
|
271
|
+
console.log(' Run `fnx sync` or `fnx sync host` to download it.\n');
|
|
272
|
+
} else if (compareVersions(targetHostVersion, highest) < 0) {
|
|
273
|
+
console.log(` ⚠️ Host rollback detected: local ${highest}, catalog ${targetHostVersion}.`);
|
|
274
|
+
console.log(' Run `fnx sync` or `fnx sync host` to align with the supported version.\n');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function maybeWarnForCliUpgrade() {
|
|
279
|
+
try {
|
|
280
|
+
const pkg = await getFnxPackage();
|
|
281
|
+
const current = pkg.version;
|
|
282
|
+
const cached = await readJsonFile(VERSION_CHECK_FILE);
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
|
|
285
|
+
if (cached?.checkedAt && now - new Date(cached.checkedAt).getTime() < VERSION_CHECK_TTL_MS) {
|
|
286
|
+
if (cached.latestVersion && compareVersions(cached.latestVersion, current) > 0) {
|
|
287
|
+
printUpgradeTip(cached.latestVersion);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`;
|
|
293
|
+
const res = await fetch(registryUrl);
|
|
294
|
+
if (!res.ok) return;
|
|
295
|
+
const latest = await res.json();
|
|
296
|
+
|
|
297
|
+
await mkdir(FNX_HOME, { recursive: true });
|
|
298
|
+
await writeFile(VERSION_CHECK_FILE, JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion: latest.version }, null, 2));
|
|
299
|
+
|
|
300
|
+
if (latest.version && compareVersions(latest.version, current) > 0) {
|
|
301
|
+
printUpgradeTip(latest.version);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
// non-fatal: offline/private registry/etc.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function printUpgradeTip(latestVersion) {
|
|
309
|
+
console.log(` ℹ️ A newer fnx version is available (${latestVersion}).`);
|
|
310
|
+
console.log(' Run `npm i -g @vrdmr/fnx-test@latest` to upgrade.\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function getFnxPackage() {
|
|
314
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
315
|
+
return JSON.parse(await readFile(join(dir, '..', 'package.json'), 'utf-8'));
|
|
316
|
+
}
|
|
317
|
+
|
|
203
318
|
async function startTemplatesMcp() {
|
|
204
319
|
const { runStdioMcpServer } = await import('./mcp-server.js');
|
|
205
320
|
const { getTemplateTools } = await import('./mcp-tools/templates.js');
|
|
@@ -238,6 +353,10 @@ Usage: fnx <action> [-/--options]
|
|
|
238
353
|
Actions:
|
|
239
354
|
start Launch the Azure Functions host runtime for a specific SKU.
|
|
240
355
|
Downloads and caches the correct host version automatically.
|
|
356
|
+
sync Sync cached host/extensions with current catalog profile.
|
|
357
|
+
Use: fnx sync, fnx sync host, fnx sync extensions.
|
|
358
|
+
pack Package a Functions app into a deployment zip (func pack equivalent).
|
|
359
|
+
Supports python, node, java, powershell, and dotnet-isolated.
|
|
241
360
|
warmup Pre-download host binaries and extension bundles for offline use.
|
|
242
361
|
Runs automatically as postinstall hook. Use --dry-run to preview.
|
|
243
362
|
templates-mcp Start the Azure Functions templates MCP server (stdio transport).
|
|
@@ -252,14 +371,20 @@ Options:
|
|
|
252
371
|
Must contain host.json and either app.config.json or local.settings.json.
|
|
253
372
|
--port <port> Port for the host HTTP listener. Default: 7071.
|
|
254
373
|
--mcp-port <p> Port for the live MCP server. Default: host port + 1 (7072).
|
|
255
|
-
--no-mcp Disable the live MCP server (host-only mode).
|
|
256
|
-
--no-azurite Skip automatic Azurite start (for users who manage Azurite separately).
|
|
257
374
|
--profiles <src> SKU profiles source. Can be:
|
|
258
375
|
• A URL (http/https) to a profiles JSON endpoint
|
|
259
376
|
• A local file path to a profiles JSON file
|
|
260
377
|
• Inline JSON string (e.g. '{"profiles":{...}}')
|
|
261
378
|
Default: FUNC_PROFILES_URL env var, or http://localhost:4566/api/profiles.
|
|
379
|
+
--keep <n> For sync only: keep latest N host/bundle versions in cache (default: 2).
|
|
380
|
+
--force For sync only: re-download assets even if already cached.
|
|
381
|
+
--no-mcp Disable the live MCP server (host-only mode).
|
|
382
|
+
--no-azurite Skip automatic Azurite start (for users who manage Azurite separately).
|
|
262
383
|
--verbose Show all host output (unfiltered). Default: clean output only.
|
|
384
|
+
--runtime <name> Runtime used by pack. If omitted, reads FUNCTIONS_WORKER_RUNTIME
|
|
385
|
+
from app.config.json/local.settings.json.
|
|
386
|
+
--output <file> Output zip path for pack. Default: <scriptroot-name>.zip.
|
|
387
|
+
--no-build Skip build steps for java/dotnet-isolated during pack.
|
|
263
388
|
-v, --version Display the version of fnx.
|
|
264
389
|
-h, --help Display this help information.
|
|
265
390
|
|
|
@@ -286,6 +411,7 @@ Examples:
|
|
|
286
411
|
fnx start --sku list List all available SKU profiles with host versions
|
|
287
412
|
fnx start --sku flex --port 8080 Start on a custom port
|
|
288
413
|
fnx start --scriptroot ./my-app Start from a specific function app directory
|
|
414
|
+
fnx pack --scriptroot ./my-app Package function app as zip deployment artifact
|
|
289
415
|
|
|
290
416
|
Side-by-side comparison:
|
|
291
417
|
# Terminal 1: Run as Flex Consumption
|
package/lib/host-launcher.js
CHANGED
|
@@ -360,14 +360,24 @@ export async function launchHost(hostDir, opts) {
|
|
|
360
360
|
|
|
361
361
|
const filter = createLogFilter(verbose, hostState);
|
|
362
362
|
|
|
363
|
+
// Start host in its own process group so we can kill it + all child workers together
|
|
363
364
|
const child = spawn(hostExe, [], {
|
|
364
365
|
env,
|
|
365
366
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
366
367
|
cwd: opts.scriptRoot,
|
|
368
|
+
detached: true, // creates new process group (pgid = child.pid)
|
|
367
369
|
});
|
|
368
370
|
|
|
369
371
|
hostState.pid = child.pid;
|
|
370
372
|
|
|
373
|
+
// Kill the entire process group (host + Python/Node workers it spawns)
|
|
374
|
+
function killHostGroup(signal) {
|
|
375
|
+
try { process.kill(-child.pid, signal); } catch { /* already dead */ }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Ensure the host process group is killed if Node exits unexpectedly
|
|
379
|
+
process.on('exit', () => killHostGroup('SIGKILL'));
|
|
380
|
+
|
|
371
381
|
// Process stdout and stderr through the log filter
|
|
372
382
|
for (const stream of [child.stdout, child.stderr]) {
|
|
373
383
|
const rl = createInterface({ input: stream });
|
|
@@ -382,18 +392,21 @@ export async function launchHost(hostDir, opts) {
|
|
|
382
392
|
});
|
|
383
393
|
}
|
|
384
394
|
|
|
385
|
-
|
|
395
|
+
function cleanup(signal) {
|
|
386
396
|
stopAzurite();
|
|
387
|
-
|
|
397
|
+
killHostGroup(signal);
|
|
388
398
|
if (hostState._mcpServer) hostState._mcpServer.close();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
399
|
+
// Give the host 2s to shut down gracefully, then force kill the group
|
|
400
|
+
const forceTimer = setTimeout(() => {
|
|
401
|
+
killHostGroup('SIGKILL');
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}, 2000);
|
|
404
|
+
forceTimer.unref();
|
|
405
|
+
child.once('exit', () => process.exit(0));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
process.on('SIGINT', () => cleanup('SIGINT'));
|
|
409
|
+
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
|
397
410
|
|
|
398
411
|
return new Promise((resolve, reject) => {
|
|
399
412
|
child.on('error', (err) => {
|
package/lib/host-manager.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { mkdir, chmod, rm, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
4
5
|
import { homedir, platform } from 'node:os';
|
|
5
6
|
import { createWriteStream } from 'node:fs';
|
|
6
7
|
import { execSync } from 'node:child_process';
|
|
7
8
|
import { arch } from 'node:os';
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const FNX_HOME = join(homedir(), '.fnx');
|
|
11
|
+
const HOST_CACHE = join(FNX_HOME, 'hosts');
|
|
12
|
+
const BUNDLE_CACHE = join(FNX_HOME, 'bundles');
|
|
11
13
|
const BUNDLE_CDN = 'https://functionscdn.azureedge.net/public/ExtensionBundles';
|
|
12
14
|
const BUNDLE_ID = 'Microsoft.Azure.Functions.ExtensionBundle';
|
|
15
|
+
const HOST_META_FILE = join(HOST_CACHE, '.metadata.json');
|
|
16
|
+
const BUNDLE_META_FILE = join(BUNDLE_CACHE, '.metadata.json');
|
|
17
|
+
const DEFAULT_KEEP_VERSIONS = 2;
|
|
13
18
|
|
|
14
19
|
function getPlatformRid() {
|
|
15
20
|
const os = platform();
|
|
@@ -25,12 +30,108 @@ function getHostExeName() {
|
|
|
25
30
|
: 'Microsoft.Azure.WebJobs.Script.WebHost';
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
function parseVersion(version) {
|
|
34
|
+
// Accepts versions like 4.1047.100, 1.2.3-beta.1, v4.5.6.
|
|
35
|
+
// Non-numeric suffixes are ignored for numeric precedence.
|
|
36
|
+
const cleaned = String(version || '').trim().replace(/^v/i, '');
|
|
37
|
+
return cleaned.split('.').map((part) => {
|
|
38
|
+
const match = part.match(/^(\d+)/);
|
|
39
|
+
return match ? Number(match[1]) : 0;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compareVersions(a, b) {
|
|
44
|
+
const pa = parseVersion(a);
|
|
45
|
+
const pb = parseVersion(b);
|
|
46
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
47
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
48
|
+
if (diff !== 0) return diff;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Stable tie-breaker when numeric parts are equal.
|
|
52
|
+
return String(a).localeCompare(String(b));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readMetadata(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return { versions: {} };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeMetadata(filePath, meta) {
|
|
64
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
65
|
+
await writeFile(filePath, JSON.stringify(meta, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toIsoNow() {
|
|
69
|
+
return new Date().toISOString();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function updateVersionMetadata(filePath, version, updates) {
|
|
73
|
+
const meta = await readMetadata(filePath);
|
|
74
|
+
const existing = meta.versions[version] || {};
|
|
75
|
+
meta.versions[version] = {
|
|
76
|
+
...existing,
|
|
77
|
+
version,
|
|
78
|
+
...updates,
|
|
79
|
+
};
|
|
80
|
+
await writeMetadata(filePath, meta);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getSortedVersionEntries(meta) {
|
|
84
|
+
return Object.values(meta.versions || {}).sort((a, b) => compareVersions(b.version, a.version));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function pruneHostCache({ keep = DEFAULT_KEEP_VERSIONS, protect = [] } = {}) {
|
|
88
|
+
const meta = await readMetadata(HOST_META_FILE);
|
|
89
|
+
const protectedSet = new Set(protect);
|
|
90
|
+
const sorted = getSortedVersionEntries(meta);
|
|
91
|
+
|
|
92
|
+
for (let i = keep; i < sorted.length; i++) {
|
|
93
|
+
const entry = sorted[i];
|
|
94
|
+
if (protectedSet.has(entry.version)) continue;
|
|
95
|
+
const dir = join(HOST_CACHE, entry.version);
|
|
96
|
+
if (existsSync(dir)) {
|
|
97
|
+
await rm(dir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
delete meta.versions[entry.version];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await writeMetadata(HOST_META_FILE, meta);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function pruneBundleCache({ keep = DEFAULT_KEEP_VERSIONS, protect = [] } = {}) {
|
|
106
|
+
const meta = await readMetadata(BUNDLE_META_FILE);
|
|
107
|
+
const protectedSet = new Set(protect);
|
|
108
|
+
const sorted = getSortedVersionEntries(meta);
|
|
109
|
+
|
|
110
|
+
for (let i = keep; i < sorted.length; i++) {
|
|
111
|
+
const entry = sorted[i];
|
|
112
|
+
if (protectedSet.has(entry.version)) continue;
|
|
113
|
+
const dir = join(BUNDLE_CACHE, BUNDLE_ID, entry.version);
|
|
114
|
+
if (existsSync(dir)) {
|
|
115
|
+
await rm(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
delete meta.versions[entry.version];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await writeMetadata(BUNDLE_META_FILE, meta);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function ensureHost(profile, { force = false, keepVersions = DEFAULT_KEEP_VERSIONS } = {}) {
|
|
29
124
|
const hostDir = join(HOST_CACHE, profile.hostVersion);
|
|
30
125
|
const hostExe = join(hostDir, getHostExeName());
|
|
31
126
|
|
|
32
127
|
if (!force && existsSync(hostExe)) {
|
|
33
128
|
console.log(' Host cached, skipping download.');
|
|
129
|
+
await updateVersionMetadata(HOST_META_FILE, profile.hostVersion, {
|
|
130
|
+
lastUsedAt: toIsoNow(),
|
|
131
|
+
sku: profile.name || profile.displayName,
|
|
132
|
+
rid: getPlatformRid(),
|
|
133
|
+
dependencies: { extensionBundleVersion: profile.extensionBundleVersion },
|
|
134
|
+
});
|
|
34
135
|
return hostDir;
|
|
35
136
|
}
|
|
36
137
|
|
|
@@ -103,6 +204,15 @@ export async function ensureHost(profile, { force = false } = {}) {
|
|
|
103
204
|
// Patch worker configs: replace 'python' → 'python3' on Unix where python3 exists but python doesn't
|
|
104
205
|
await patchWorkerConfigs(hostDir);
|
|
105
206
|
|
|
207
|
+
await updateVersionMetadata(HOST_META_FILE, profile.hostVersion, {
|
|
208
|
+
downloadedAt: toIsoNow(),
|
|
209
|
+
lastUsedAt: toIsoNow(),
|
|
210
|
+
sku: profile.name || profile.displayName,
|
|
211
|
+
rid,
|
|
212
|
+
dependencies: { extensionBundleVersion: profile.extensionBundleVersion },
|
|
213
|
+
});
|
|
214
|
+
await pruneHostCache({ keep: keepVersions, protect: [profile.hostVersion] });
|
|
215
|
+
|
|
106
216
|
return hostDir;
|
|
107
217
|
}
|
|
108
218
|
|
|
@@ -152,16 +262,6 @@ function parseVersionRange(range) {
|
|
|
152
262
|
};
|
|
153
263
|
}
|
|
154
264
|
|
|
155
|
-
function compareVersions(a, b) {
|
|
156
|
-
const pa = a.split('.').map(Number);
|
|
157
|
-
const pb = b.split('.').map(Number);
|
|
158
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
159
|
-
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
160
|
-
if (diff !== 0) return diff;
|
|
161
|
-
}
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
265
|
function findBestBundleVersion(allVersions, range, maxVersion) {
|
|
166
266
|
const parsed = parseVersionRange(range);
|
|
167
267
|
if (!parsed) return null;
|
|
@@ -183,7 +283,7 @@ function findBestBundleVersion(allVersions, range, maxVersion) {
|
|
|
183
283
|
return candidates[candidates.length - 1]; // highest valid version
|
|
184
284
|
}
|
|
185
285
|
|
|
186
|
-
export async function ensureBundle(profile, { force = false } = {}) {
|
|
286
|
+
export async function ensureBundle(profile, { force = false, keepVersions = DEFAULT_KEEP_VERSIONS } = {}) {
|
|
187
287
|
const bundleDir = join(BUNDLE_CACHE, BUNDLE_ID);
|
|
188
288
|
const range = profile.extensionBundleVersion;
|
|
189
289
|
const maxVersion = profile.maxExtensionBundleVersion;
|
|
@@ -214,6 +314,11 @@ export async function ensureBundle(profile, { force = false } = {}) {
|
|
|
214
314
|
const versionDir = join(bundleDir, bestVersion);
|
|
215
315
|
if (!force && existsSync(join(versionDir, 'bundle.json'))) {
|
|
216
316
|
console.log(` Bundle ${bestVersion} cached.`);
|
|
317
|
+
await updateVersionMetadata(BUNDLE_META_FILE, bestVersion, {
|
|
318
|
+
lastUsedAt: toIsoNow(),
|
|
319
|
+
sku: profile.name || profile.displayName,
|
|
320
|
+
dependencies: { hostVersion: profile.hostVersion },
|
|
321
|
+
});
|
|
217
322
|
return bestVersion;
|
|
218
323
|
}
|
|
219
324
|
|
|
@@ -264,6 +369,14 @@ export async function ensureBundle(profile, { force = false } = {}) {
|
|
|
264
369
|
try { await rm(tempZip); } catch { /* ignore */ }
|
|
265
370
|
}
|
|
266
371
|
|
|
372
|
+
await updateVersionMetadata(BUNDLE_META_FILE, bestVersion, {
|
|
373
|
+
downloadedAt: toIsoNow(),
|
|
374
|
+
lastUsedAt: toIsoNow(),
|
|
375
|
+
sku: profile.name || profile.displayName,
|
|
376
|
+
dependencies: { hostVersion: profile.hostVersion },
|
|
377
|
+
});
|
|
378
|
+
await pruneBundleCache({ keep: keepVersions, protect: [bestVersion] });
|
|
379
|
+
|
|
267
380
|
return bestVersion;
|
|
268
381
|
}
|
|
269
382
|
|
|
@@ -277,4 +390,9 @@ function findCachedBundle(bundleDir, range, maxVersion) {
|
|
|
277
390
|
return best;
|
|
278
391
|
}
|
|
279
392
|
|
|
280
|
-
export
|
|
393
|
+
export function getCachedHostVersions() {
|
|
394
|
+
if (!existsSync(HOST_CACHE)) return [];
|
|
395
|
+
return readdirSync(HOST_CACHE).filter((v) => existsSync(join(HOST_CACHE, v, getHostExeName())));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export { getHostExeName, getPlatformRid, compareVersions, parseVersion, DEFAULT_KEEP_VERSIONS };
|
package/lib/pack.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { basename, resolve as resolvePath, join } from 'node:path';
|
|
2
|
+
import { access, constants, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const RUNTIME_ALIASES = new Map([
|
|
7
|
+
['node', 'node'],
|
|
8
|
+
['nodejs', 'node'],
|
|
9
|
+
['javascript', 'node'],
|
|
10
|
+
['typescript', 'node'],
|
|
11
|
+
['python', 'python'],
|
|
12
|
+
['py', 'python'],
|
|
13
|
+
['java', 'java'],
|
|
14
|
+
['powershell', 'powershell'],
|
|
15
|
+
['pwsh', 'powershell'],
|
|
16
|
+
['dotnet-isolated', 'dotnet-isolated'],
|
|
17
|
+
['dotnetisolated', 'dotnet-isolated'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const SUPPORTED_PACK_RUNTIMES = ['python', 'node', 'java', 'powershell', 'dotnet-isolated'];
|
|
21
|
+
|
|
22
|
+
function normalizeRuntime(input) {
|
|
23
|
+
return String(input || '').trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolvePackRuntime(input) {
|
|
27
|
+
const normalized = normalizeRuntime(input);
|
|
28
|
+
|
|
29
|
+
if (!normalized) {
|
|
30
|
+
throw new Error('Missing runtime. Set FUNCTIONS_WORKER_RUNTIME or pass --runtime.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (normalized === 'dotnet') {
|
|
34
|
+
throw new Error('Only .NET isolated worker is supported for packing. Use dotnet-isolated runtime.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const runtime = RUNTIME_ALIASES.get(normalized);
|
|
38
|
+
if (!runtime) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unsupported runtime '${input}'. Supported values: ${SUPPORTED_PACK_RUNTIMES.join(', ')}.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return runtime;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runCommand(command, args, options = {}) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(command, args, {
|
|
50
|
+
cwd: options.cwd,
|
|
51
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let stderr = '';
|
|
56
|
+
if (child.stderr) {
|
|
57
|
+
child.stderr.on('data', (chunk) => {
|
|
58
|
+
stderr += chunk.toString();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
child.on('error', (err) => reject(err));
|
|
63
|
+
child.on('close', (code) => {
|
|
64
|
+
if (code === 0) return resolve();
|
|
65
|
+
reject(new Error(`${command} ${args.join(' ')} failed with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function ensureExists(pathToCheck) {
|
|
71
|
+
await access(pathToCheck, constants.F_OK);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function zipDirectory(sourceDir, outputZip) {
|
|
75
|
+
await runCommand('zip', ['-r', '-q', outputZip, '.'], { cwd: sourceDir });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function stageJavaBuild(scriptRoot) {
|
|
79
|
+
await runCommand('mvn', ['clean', 'package', '-DskipTests'], { cwd: scriptRoot });
|
|
80
|
+
const targetDir = resolvePath(scriptRoot, 'target', 'azure-functions');
|
|
81
|
+
await ensureExists(targetDir);
|
|
82
|
+
return targetDir;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function stageDotnetIsolatedBuild(scriptRoot, tempRoot) {
|
|
86
|
+
const publishDir = resolvePath(tempRoot, 'publish');
|
|
87
|
+
await runCommand('dotnet', ['publish', '--configuration', 'Release', '--output', publishDir], { cwd: scriptRoot });
|
|
88
|
+
await ensureExists(publishDir);
|
|
89
|
+
return publishDir;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function detectRuntimeFromConfig(scriptRoot) {
|
|
93
|
+
const appConfigPath = resolvePath(scriptRoot, 'app.config.json');
|
|
94
|
+
const localSettingsPath = resolvePath(scriptRoot, 'local.settings.json');
|
|
95
|
+
|
|
96
|
+
const parseIfExists = async (filePath) => {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const appConfig = await parseIfExists(appConfigPath);
|
|
106
|
+
const localSettings = await parseIfExists(localSettingsPath);
|
|
107
|
+
return appConfig?.Values?.FUNCTIONS_WORKER_RUNTIME || localSettings?.Values?.FUNCTIONS_WORKER_RUNTIME || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function packFunctionApp({ scriptRoot, runtime, outputPath, noBuild = false }) {
|
|
111
|
+
const root = resolvePath(scriptRoot || process.cwd());
|
|
112
|
+
const resolvedRuntime = resolvePackRuntime(runtime);
|
|
113
|
+
const resolvedOutput = resolvePath(outputPath || `${basename(root)}.zip`);
|
|
114
|
+
|
|
115
|
+
const tempRoot = await mkdtemp(join(tmpdir(), 'fnx-pack-'));
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
let sourceDir = root;
|
|
119
|
+
|
|
120
|
+
if (!noBuild) {
|
|
121
|
+
if (resolvedRuntime === 'java') {
|
|
122
|
+
sourceDir = await stageJavaBuild(root);
|
|
123
|
+
} else if (resolvedRuntime === 'dotnet-isolated') {
|
|
124
|
+
sourceDir = await stageDotnetIsolatedBuild(root, tempRoot);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`Packing runtime '${resolvedRuntime}' from ${sourceDir}`);
|
|
129
|
+
await zipDirectory(sourceDir, resolvedOutput);
|
|
130
|
+
console.log(`Created package: ${resolvedOutput}`);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
runtime: resolvedRuntime,
|
|
134
|
+
sourceDir,
|
|
135
|
+
outputPath: resolvedOutput,
|
|
136
|
+
};
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
package/lib/profile-resolver.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve as resolvePath, isAbsolute } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const CACHE_DIR = join(homedir(), '.fnx', 'profiles');
|
|
7
7
|
const CACHE_FILE = join(CACHE_DIR, 'sku-profiles.json');
|
|
8
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
9
8
|
|
|
10
9
|
const DEFAULT_CDN_URL = 'https://raw.githubusercontent.com/vrdmr/func-emulate/main/fnx/profiles/sku-profiles.json';
|
|
11
10
|
|
|
@@ -26,72 +25,67 @@ function isJsonString(str) {
|
|
|
26
25
|
return str.trimStart().startsWith('{');
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
async function
|
|
30
|
-
|
|
28
|
+
async function persistCache(rawJson) {
|
|
29
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
30
|
+
await writeFile(CACHE_FILE, rawJson);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function fetchRegistryWithMeta() {
|
|
34
|
+
// If an explicit source was provided (--profiles flag or inline JSON), use it directly.
|
|
31
35
|
if (profilesSource) {
|
|
32
|
-
// Inline JSON string
|
|
33
36
|
if (isJsonString(profilesSource)) {
|
|
34
|
-
return JSON.parse(profilesSource);
|
|
37
|
+
return { registry: JSON.parse(profilesSource), source: 'inline-json' };
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
// URL (http/https)
|
|
38
40
|
if (isUrl(profilesSource)) {
|
|
39
41
|
try {
|
|
40
42
|
const res = await fetch(profilesSource);
|
|
41
|
-
if (res.ok) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
|
|
43
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
44
|
+
const json = await res.text();
|
|
45
|
+
await persistCache(json);
|
|
46
|
+
return { registry: JSON.parse(json), source: 'remote', url: profilesSource };
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
|
|
49
|
+
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
// Local file path
|
|
52
52
|
const filePath = isAbsolute(profilesSource) ? profilesSource : resolvePath(process.cwd(), profilesSource);
|
|
53
53
|
try {
|
|
54
|
-
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
54
|
+
return { registry: JSON.parse(await readFile(filePath, 'utf-8')), source: 'local-file', path: filePath };
|
|
55
55
|
} catch (err) {
|
|
56
56
|
throw new Error(`Cannot read profiles file: ${filePath} (${err.message})`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Default
|
|
60
|
+
// Default behavior: always attempt CDN first to detect upgrades/rollbacks quickly,
|
|
61
|
+
// then fall back to cache, then bundled profiles.
|
|
61
62
|
const cdnUrl = process.env.FUNC_PROFILES_URL || DEFAULT_CDN_URL;
|
|
62
63
|
|
|
63
|
-
// 1. Try cache (if fresh)
|
|
64
|
-
try {
|
|
65
|
-
const cacheStat = await stat(CACHE_FILE);
|
|
66
|
-
if (Date.now() - cacheStat.mtimeMs < CACHE_TTL_MS) {
|
|
67
|
-
return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
|
|
68
|
-
}
|
|
69
|
-
} catch { /* no cache or stale */ }
|
|
70
|
-
|
|
71
|
-
// 2. Try CDN
|
|
72
64
|
try {
|
|
73
65
|
const res = await fetch(cdnUrl);
|
|
74
66
|
if (res.ok) {
|
|
75
67
|
const json = await res.text();
|
|
76
|
-
await
|
|
77
|
-
|
|
78
|
-
return JSON.parse(json);
|
|
68
|
+
await persistCache(json);
|
|
69
|
+
return { registry: JSON.parse(json), source: 'remote', url: cdnUrl };
|
|
79
70
|
}
|
|
80
71
|
} catch { /* CDN unreachable */ }
|
|
81
72
|
|
|
82
|
-
// 3. Try stale cache
|
|
83
73
|
try {
|
|
84
|
-
return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
|
|
85
|
-
} catch { /* no cache
|
|
74
|
+
return { registry: JSON.parse(await readFile(CACHE_FILE, 'utf-8')), source: 'cache' };
|
|
75
|
+
} catch { /* no cache */ }
|
|
86
76
|
|
|
87
|
-
// 4. Fall back to bundled profiles
|
|
88
77
|
try {
|
|
89
|
-
return JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8'));
|
|
78
|
+
return { registry: JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8')), source: 'bundled' };
|
|
90
79
|
} catch {
|
|
91
80
|
throw new Error('Cannot load SKU profiles: CDN unreachable, no cache, no bundled profiles.');
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
83
|
|
|
84
|
+
async function fetchRegistry() {
|
|
85
|
+
const { registry } = await fetchRegistryWithMeta();
|
|
86
|
+
return registry;
|
|
87
|
+
}
|
|
88
|
+
|
|
95
89
|
export async function resolveProfile(skuName) {
|
|
96
90
|
const registry = await fetchRegistry();
|
|
97
91
|
const profile = registry.profiles[skuName];
|
package/lib/warmup.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { resolveProfile, listProfiles } from './profile-resolver.js';
|
|
5
|
+
import { resolveProfile, listProfiles, fetchRegistryWithMeta } from './profile-resolver.js';
|
|
6
6
|
import { ensureHost, ensureBundle, getHostExeName, getPlatformRid } from './host-manager.js';
|
|
7
7
|
|
|
8
8
|
const FNX_DIR = join(homedir(), '.fnx');
|
|
@@ -143,9 +143,8 @@ function findAnyCachedBundle(bundleDir) {
|
|
|
143
143
|
|
|
144
144
|
async function getAllSkuNames() {
|
|
145
145
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
return Object.keys(registry.profiles);
|
|
146
|
+
const { registry } = await fetchRegistryWithMeta();
|
|
147
|
+
return Object.keys(registry.profiles || {});
|
|
149
148
|
} catch {
|
|
150
149
|
return ['flex', 'linux-premium', 'windows-consumption', 'windows-dedicated', 'linux-consumption'];
|
|
151
150
|
}
|