@vrdmr/fnx-test 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
- import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { readFileSync, existsSync } from 'node:fs';
2
3
  import { join, resolve as resolvePath, isAbsolute } from 'node:path';
3
4
  import { homedir } from 'node:os';
4
5
  import { fileURLToPath } from 'node:url';
5
6
 
6
7
  const CACHE_DIR = join(homedir(), '.fnx', 'profiles');
7
8
  const CACHE_FILE = join(CACHE_DIR, 'sku-profiles.json');
8
- const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
9
9
 
10
10
  const DEFAULT_CDN_URL = 'https://raw.githubusercontent.com/vrdmr/func-emulate/main/fnx/profiles/sku-profiles.json';
11
11
 
@@ -26,72 +26,67 @@ function isJsonString(str) {
26
26
  return str.trimStart().startsWith('{');
27
27
  }
28
28
 
29
- async function fetchRegistry() {
30
- // If an explicit source was provided (--profiles flag or inline JSON), use it directly
29
+ async function persistCache(rawJson) {
30
+ await mkdir(CACHE_DIR, { recursive: true });
31
+ await writeFile(CACHE_FILE, rawJson);
32
+ }
33
+
34
+ export async function fetchRegistryWithMeta() {
35
+ // If an explicit source was provided (--profiles flag or inline JSON), use it directly.
31
36
  if (profilesSource) {
32
- // Inline JSON string
33
37
  if (isJsonString(profilesSource)) {
34
- return JSON.parse(profilesSource);
38
+ return { registry: JSON.parse(profilesSource), source: 'inline-json' };
35
39
  }
36
40
 
37
- // URL (http/https)
38
41
  if (isUrl(profilesSource)) {
39
42
  try {
40
43
  const res = await fetch(profilesSource);
41
- if (res.ok) {
42
- const json = await res.text();
43
- await mkdir(CACHE_DIR, { recursive: true });
44
- await writeFile(CACHE_FILE, json);
45
- return JSON.parse(json);
46
- }
47
- } catch { /* fall through to error */ }
48
- throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
44
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
45
+ const json = await res.text();
46
+ await persistCache(json);
47
+ return { registry: JSON.parse(json), source: 'remote', url: profilesSource };
48
+ } catch {
49
+ throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
50
+ }
49
51
  }
50
52
 
51
- // Local file path
52
53
  const filePath = isAbsolute(profilesSource) ? profilesSource : resolvePath(process.cwd(), profilesSource);
53
54
  try {
54
- return JSON.parse(await readFile(filePath, 'utf-8'));
55
+ return { registry: JSON.parse(await readFile(filePath, 'utf-8')), source: 'local-file', path: filePath };
55
56
  } catch (err) {
56
57
  throw new Error(`Cannot read profiles file: ${filePath} (${err.message})`);
57
58
  }
58
59
  }
59
60
 
60
- // Default resolution chain: env var → cache → CDN stale cache bundled
61
+ // Default behavior: always attempt CDN first to detect upgrades/rollbacks quickly,
62
+ // then fall back to cache, then bundled profiles.
61
63
  const cdnUrl = process.env.FUNC_PROFILES_URL || DEFAULT_CDN_URL;
62
64
 
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
65
  try {
73
66
  const res = await fetch(cdnUrl);
74
67
  if (res.ok) {
75
68
  const json = await res.text();
76
- await mkdir(CACHE_DIR, { recursive: true });
77
- await writeFile(CACHE_FILE, json);
78
- return JSON.parse(json);
69
+ await persistCache(json);
70
+ return { registry: JSON.parse(json), source: 'remote', url: cdnUrl };
79
71
  }
80
72
  } catch { /* CDN unreachable */ }
81
73
 
82
- // 3. Try stale cache
83
74
  try {
84
- return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
85
- } catch { /* no cache at all */ }
75
+ return { registry: JSON.parse(await readFile(CACHE_FILE, 'utf-8')), source: 'cache' };
76
+ } catch { /* no cache */ }
86
77
 
87
- // 4. Fall back to bundled profiles
88
78
  try {
89
- return JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8'));
79
+ return { registry: JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8')), source: 'bundled' };
90
80
  } catch {
91
81
  throw new Error('Cannot load SKU profiles: CDN unreachable, no cache, no bundled profiles.');
92
82
  }
93
83
  }
94
84
 
85
+ async function fetchRegistry() {
86
+ const { registry } = await fetchRegistryWithMeta();
87
+ return registry;
88
+ }
89
+
95
90
  export async function resolveProfile(skuName) {
96
91
  const registry = await fetchRegistry();
97
92
  const profile = registry.profiles[skuName];
@@ -116,3 +111,17 @@ export async function listProfiles() {
116
111
  }
117
112
  console.log(`\n Last updated: ${registry.updatedAt}`);
118
113
  }
114
+
115
+ /** Synchronous read: cached profiles → bundled fallback. No network. */
116
+ export function readProfilesSync() {
117
+ try {
118
+ if (existsSync(CACHE_FILE)) {
119
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
120
+ }
121
+ } catch { /* fall through */ }
122
+ try {
123
+ return JSON.parse(readFileSync(BUNDLED_PROFILES_PATH, 'utf-8'));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
package/lib/warmup.js CHANGED
@@ -2,8 +2,9 @@ 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
+ import { title, info, success, warning, dim } from './colors.js';
7
8
 
8
9
  const FNX_DIR = join(homedir(), '.fnx');
9
10
  const META_FILE = join(FNX_DIR, '_meta.json');
@@ -57,9 +58,9 @@ async function runWarmup(args) {
57
58
  const rid = getPlatformRid();
58
59
 
59
60
  console.log();
60
- console.log(`fnx warmup — pre-downloading assets for ${all ? 'ALL SKUs' : 'offline use'}`);
61
+ console.log(title(`fnx warmup — pre-downloading assets for ${all ? 'ALL SKUs' : 'offline use'}`));
61
62
  console.log();
62
- console.log(` Platform: ${rid}`);
63
+ console.log(` ${dim('Platform:')} ${info(rid)}`);
63
64
 
64
65
  const meta = await loadMeta();
65
66
 
@@ -68,10 +69,10 @@ async function runWarmup(args) {
68
69
  console.log();
69
70
  const profile = await resolveProfile(skuName);
70
71
 
71
- console.log(` Target SKU: ${profile.displayName} (${skuName})`);
72
- console.log(` Host Version: ${profile.hostVersion}`);
72
+ console.log(` ${dim('Target SKU:')} ${info(`${profile.displayName} (${skuName})`)}`);
73
+ console.log(` ${dim('Host Version:')} ${info(profile.hostVersion)}`);
73
74
  if (profile.maxExtensionBundleVersion) {
74
- console.log(` Bundle Range: ${profile.extensionBundleVersion} (max: ${profile.maxExtensionBundleVersion})`);
75
+ console.log(` ${dim('Bundle Range:')} ${info(`${profile.extensionBundleVersion} (max: ${profile.maxExtensionBundleVersion})`)}`);
75
76
  }
76
77
  console.log();
77
78
 
@@ -99,9 +100,9 @@ async function runWarmup(args) {
99
100
  }
100
101
 
101
102
  console.log();
102
- console.log(` ✓ fnx start --sku ${skuName} will work offline.`);
103
+ console.log(success(` ✓ fnx start --sku ${skuName} will work offline.`));
103
104
  } catch (err) {
104
- console.error(` ⚠️ Warmup failed for ${skuName}: ${err.message}`);
105
+ console.error(warning(` ⚠️ Warmup failed for ${skuName}: ${err.message}`));
105
106
  }
106
107
  }
107
108
 
@@ -113,7 +114,7 @@ async function runWarmup(args) {
113
114
  }
114
115
 
115
116
  console.log();
116
- console.log(' Done.');
117
+ console.log(success(' Done.'));
117
118
  console.log();
118
119
  }
119
120
 
@@ -143,9 +144,8 @@ function findAnyCachedBundle(bundleDir) {
143
144
 
144
145
  async function getAllSkuNames() {
145
146
  try {
146
- const profilesPath = new URL('../profiles/sku-profiles.json', import.meta.url).pathname;
147
- const registry = JSON.parse(await readFile(profilesPath, 'utf-8'));
148
- return Object.keys(registry.profiles);
147
+ const { registry } = await fetchRegistryWithMeta();
148
+ return Object.keys(registry.profiles || {});
149
149
  } catch {
150
150
  return ['flex', 'linux-premium', 'windows-consumption', 'windows-dedicated', 'linux-consumption'];
151
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrdmr/fnx-test",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "SKU-aware Azure Functions local emulator",
5
5
  "type": "module",
6
6
  "bin": {