runtime-resolver 0.3.12 → 0.3.14

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 (67) hide show
  1. package/bin/runtime-resolver.d.ts +1 -0
  2. package/bin/runtime-resolver.js +29 -261
  3. package/cli/commands/resolve.js +239 -0
  4. package/cli/schemas/json-schema.js +12 -0
  5. package/cli/schemas/response.js +86 -0
  6. package/data/bun-defaults.js +850 -0
  7. package/data/deno-defaults.js +1526 -0
  8. package/data/node-defaults.js +4400 -0
  9. package/errors/AuthenticationError.js +35 -0
  10. package/errors/CacheError.js +32 -0
  11. package/errors/FreshnessError.js +38 -0
  12. package/errors/InvalidInputError.js +33 -0
  13. package/errors/NetworkError.js +34 -0
  14. package/errors/ParseError.js +33 -0
  15. package/errors/RateLimitError.js +39 -0
  16. package/errors/VersionNotFoundError.js +37 -0
  17. package/index.d.ts +2577 -2608
  18. package/index.js +158 -132
  19. package/layers/AutoBunCacheLive.js +47 -0
  20. package/layers/AutoDenoCacheLive.js +47 -0
  21. package/layers/AutoNodeCacheLive.js +58 -0
  22. package/layers/BunReleaseCacheLive.js +25 -0
  23. package/layers/BunResolverLive.js +85 -0
  24. package/layers/BunVersionFetcherLive.js +55 -0
  25. package/layers/DenoReleaseCacheLive.js +25 -0
  26. package/layers/DenoResolverLive.js +84 -0
  27. package/layers/DenoVersionFetcherLive.js +51 -0
  28. package/layers/FreshBunCacheLive.js +60 -0
  29. package/layers/FreshDenoCacheLive.js +60 -0
  30. package/layers/FreshNodeCacheLive.js +55 -0
  31. package/layers/GitHubAppAuth.js +58 -0
  32. package/layers/GitHubAutoAuth.js +70 -0
  33. package/layers/GitHubClientLive.js +130 -0
  34. package/layers/GitHubTokenAuth.js +50 -0
  35. package/layers/NodeReleaseCacheLive.js +77 -0
  36. package/layers/NodeResolverLive.js +107 -0
  37. package/layers/NodeScheduleFetcherLive.js +37 -0
  38. package/layers/NodeVersionFetcherLive.js +55 -0
  39. package/layers/OfflineBunCacheLive.js +40 -0
  40. package/layers/OfflineDenoCacheLive.js +40 -0
  41. package/layers/OfflineNodeCacheLive.js +39 -0
  42. package/layers/RuntimeCacheLive.js +78 -0
  43. package/lib/retry.js +27 -0
  44. package/lib/semver.js +21 -0
  45. package/package.json +57 -68
  46. package/schemas/bun-release.js +61 -0
  47. package/schemas/deno-release.js +61 -0
  48. package/schemas/github.js +74 -0
  49. package/schemas/node-release.js +130 -0
  50. package/schemas/node-schedule.js +110 -0
  51. package/schemas/node.js +68 -0
  52. package/services/BunReleaseCache.js +33 -0
  53. package/services/BunResolver.js +36 -0
  54. package/services/BunVersionFetcher.js +41 -0
  55. package/services/DenoReleaseCache.js +33 -0
  56. package/services/DenoResolver.js +36 -0
  57. package/services/DenoVersionFetcher.js +41 -0
  58. package/services/GitHubClient.js +40 -0
  59. package/services/NodeReleaseCache.js +41 -0
  60. package/services/NodeResolver.js +44 -0
  61. package/services/NodeScheduleFetcher.js +38 -0
  62. package/services/NodeVersionFetcher.js +39 -0
  63. package/services/OctokitInstance.js +44 -0
  64. package/tsdoc-metadata.json +11 -11
  65. package/612.js +0 -23
  66. package/711.js +0 -7554
  67. package/json-schema.js +0 -95
package/index.js CHANGED
@@ -1,121 +1,63 @@
1
- import { Data, Effect, Layer } from "effect";
2
- import { SemVerParserLive, VersionCacheLive } from "semver-effect";
3
- import { DenoReleaseCacheLive, BunRelease, AutoDenoCacheLive, NodeReleaseCacheLive, NodeReleaseCache, AutoNodeCacheLive, denoDefaultInputs, BunReleaseCache, nodeDefaultInputs, NodeResolverLive, BunVersionFetcherLive, DenoResolverLive, GitHubClientLive, BunVersionFetcher, NodeScheduleFetcher, DenoResolver, NodeScheduleFetcherLive, DenoVersionFetcherLive, NodeVersionFetcher, DenoReleaseCache, BunReleaseCacheLive, BunResolver, DenoRelease, bunDefaultInputs, NodeVersionFetcherLive, GitHubAutoAuth, BunResolverLive, NodeResolver, AutoBunCacheLive, DenoVersionFetcher, nodeDefaultScheduleData } from "./711.js";
4
- class CacheError extends Data.TaggedError("CacheError") {
5
- }
6
- class FreshnessError extends Data.TaggedError("FreshnessError") {
7
- }
8
- class InvalidInputError extends Data.TaggedError("InvalidInputError") {
9
- }
10
- const freshSetup = Effect.gen(function*() {
11
- const cache = yield* BunReleaseCache;
12
- const fetcher = yield* BunVersionFetcher;
13
- const { inputs } = yield* fetcher.fetch().pipe(Effect.catchTags({
14
- AuthenticationError: (err)=>Effect.fail(new FreshnessError({
15
- strategy: "api",
16
- message: `Fresh data required but authentication failed: ${err.message}`
17
- })),
18
- NetworkError: (err)=>Effect.fail(new FreshnessError({
19
- strategy: "api",
20
- message: `Fresh data required but network unavailable: ${err.message}`
21
- })),
22
- ParseError: (err)=>Effect.fail(new FreshnessError({
23
- strategy: "api",
24
- message: `Fresh data required but parse failed: ${err.message}`
25
- })),
26
- RateLimitError: (err)=>Effect.fail(new FreshnessError({
27
- strategy: "api",
28
- message: `Fresh data required but rate limit exceeded: ${err.message}`
29
- }))
30
- }));
31
- const releases = yield* Effect.all(inputs.map((input)=>BunRelease.fromInput(input)), {
32
- concurrency: "unbounded"
33
- }).pipe(Effect.orElseSucceed(()=>[]));
34
- yield* cache.load(releases);
35
- });
36
- const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
37
- const BaseCacheLayer = BunReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
38
- const FreshBunCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx)=>freshSetup.pipe(Effect.provide(ctx))));
39
- const FreshDenoCacheLive_freshSetup = Effect.gen(function*() {
40
- const cache = yield* DenoReleaseCache;
41
- const fetcher = yield* DenoVersionFetcher;
42
- const { inputs } = yield* fetcher.fetch().pipe(Effect.catchTags({
43
- AuthenticationError: (err)=>Effect.fail(new FreshnessError({
44
- strategy: "api",
45
- message: `Fresh data required but authentication failed: ${err.message}`
46
- })),
47
- NetworkError: (err)=>Effect.fail(new FreshnessError({
48
- strategy: "api",
49
- message: `Fresh data required but network unavailable: ${err.message}`
50
- })),
51
- ParseError: (err)=>Effect.fail(new FreshnessError({
52
- strategy: "api",
53
- message: `Fresh data required but parse failed: ${err.message}`
54
- })),
55
- RateLimitError: (err)=>Effect.fail(new FreshnessError({
56
- strategy: "api",
57
- message: `Fresh data required but rate limit exceeded: ${err.message}`
58
- }))
59
- }));
60
- const releases = yield* Effect.all(inputs.map((input)=>DenoRelease.fromInput(input)), {
61
- concurrency: "unbounded"
62
- }).pipe(Effect.orElseSucceed(()=>[]));
63
- yield* cache.load(releases);
64
- });
65
- const FreshDenoCacheLive_SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
66
- const FreshDenoCacheLive_BaseCacheLayer = DenoReleaseCacheLive.pipe(Layer.provide(FreshDenoCacheLive_SemVerLayer));
67
- const FreshDenoCacheLive = FreshDenoCacheLive_BaseCacheLayer.pipe(Layer.tap((ctx)=>FreshDenoCacheLive_freshSetup.pipe(Effect.provide(ctx))));
68
- const FreshNodeCacheLive_freshSetup = Effect.gen(function*() {
69
- const cache = yield* NodeReleaseCache;
70
- const versionFetcher = yield* NodeVersionFetcher;
71
- const scheduleFetcher = yield* NodeScheduleFetcher;
72
- const [fetchResult, scheduleData] = yield* Effect.all([
73
- versionFetcher.fetch(),
74
- scheduleFetcher.fetch()
75
- ]).pipe(Effect.catchTags({
76
- NetworkError: (err)=>Effect.fail(new FreshnessError({
77
- strategy: "api",
78
- message: `Fresh data required but network unavailable: ${err.message}`
79
- })),
80
- ParseError: (err)=>Effect.fail(new FreshnessError({
81
- strategy: "api",
82
- message: `Fresh data required but response could not be parsed: ${err.message}`
83
- }))
84
- }));
85
- yield* cache.updateSchedule(scheduleData);
86
- yield* cache.loadFromInputs(fetchResult.inputs);
87
- });
88
- const FreshNodeCacheLive_SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
89
- const FreshNodeCacheLive_BaseCacheLayer = NodeReleaseCacheLive.pipe(Layer.provide(FreshNodeCacheLive_SemVerLayer));
90
- const FreshNodeCacheLive = FreshNodeCacheLive_BaseCacheLayer.pipe(Layer.tap((ctx)=>FreshNodeCacheLive_freshSetup.pipe(Effect.provide(ctx))));
91
- const offlineSetup = Effect.gen(function*() {
92
- const cache = yield* BunReleaseCache;
93
- const releases = yield* Effect.all(bunDefaultInputs.map((input)=>BunRelease.fromInput(input)), {
94
- concurrency: "unbounded"
95
- }).pipe(Effect.orElseSucceed(()=>[]));
96
- yield* cache.load(releases);
97
- });
98
- const OfflineBunCacheLive_SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
99
- const OfflineBunCacheLive_BaseCacheLayer = BunReleaseCacheLive.pipe(Layer.provide(OfflineBunCacheLive_SemVerLayer));
100
- const OfflineBunCacheLive = OfflineBunCacheLive_BaseCacheLayer.pipe(Layer.tap((ctx)=>offlineSetup.pipe(Effect.provide(ctx))));
101
- const OfflineDenoCacheLive_offlineSetup = Effect.gen(function*() {
102
- const cache = yield* DenoReleaseCache;
103
- const releases = yield* Effect.all(denoDefaultInputs.map((input)=>DenoRelease.fromInput(input)), {
104
- concurrency: "unbounded"
105
- }).pipe(Effect.orElseSucceed(()=>[]));
106
- yield* cache.load(releases);
107
- });
108
- const OfflineDenoCacheLive_SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
109
- const OfflineDenoCacheLive_BaseCacheLayer = DenoReleaseCacheLive.pipe(Layer.provide(OfflineDenoCacheLive_SemVerLayer));
110
- const OfflineDenoCacheLive = OfflineDenoCacheLive_BaseCacheLayer.pipe(Layer.tap((ctx)=>OfflineDenoCacheLive_offlineSetup.pipe(Effect.provide(ctx))));
111
- const OfflineNodeCacheLive_offlineSetup = Effect.gen(function*() {
112
- const cache = yield* NodeReleaseCache;
113
- yield* cache.updateSchedule(nodeDefaultScheduleData);
114
- yield* cache.loadFromInputs(nodeDefaultInputs);
115
- });
116
- const OfflineNodeCacheLive_SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
117
- const OfflineNodeCacheLive_BaseCacheLayer = NodeReleaseCacheLive.pipe(Layer.provide(OfflineNodeCacheLive_SemVerLayer));
118
- const OfflineNodeCacheLive = OfflineNodeCacheLive_BaseCacheLayer.pipe(Layer.tap((ctx)=>OfflineNodeCacheLive_offlineSetup.pipe(Effect.provide(ctx))));
1
+ import { BunRelease } from "./schemas/bun-release.js";
2
+ import { BunReleaseCache } from "./services/BunReleaseCache.js";
3
+ import { BunVersionFetcher } from "./services/BunVersionFetcher.js";
4
+ import { createRuntimeCache } from "./layers/RuntimeCacheLive.js";
5
+ import { BunReleaseCacheLive } from "./layers/BunReleaseCacheLive.js";
6
+ import { AutoBunCacheLive } from "./layers/AutoBunCacheLive.js";
7
+ import { DenoRelease } from "./schemas/deno-release.js";
8
+ import { DenoReleaseCache } from "./services/DenoReleaseCache.js";
9
+ import { DenoVersionFetcher } from "./services/DenoVersionFetcher.js";
10
+ import { DenoReleaseCacheLive } from "./layers/DenoReleaseCacheLive.js";
11
+ import { AutoDenoCacheLive } from "./layers/AutoDenoCacheLive.js";
12
+ import { NodeReleaseCache } from "./services/NodeReleaseCache.js";
13
+ import { NodeScheduleFetcher } from "./services/NodeScheduleFetcher.js";
14
+ import { NodeVersionFetcher } from "./services/NodeVersionFetcher.js";
15
+ import { NodeRelease } from "./schemas/node-release.js";
16
+ import { NodeSchedule } from "./schemas/node-schedule.js";
17
+ import { NodeReleaseCacheLive } from "./layers/NodeReleaseCacheLive.js";
18
+ import { AutoNodeCacheLive } from "./layers/AutoNodeCacheLive.js";
19
+ import { VersionNotFoundError } from "./errors/VersionNotFoundError.js";
20
+ import { BunResolver } from "./services/BunResolver.js";
21
+ import { BunResolverLive } from "./layers/BunResolverLive.js";
22
+ import { GitHubClient } from "./services/GitHubClient.js";
23
+ import { BunVersionFetcherLive } from "./layers/BunVersionFetcherLive.js";
24
+ import { DenoResolver } from "./services/DenoResolver.js";
25
+ import { DenoResolverLive } from "./layers/DenoResolverLive.js";
26
+ import { DenoVersionFetcherLive } from "./layers/DenoVersionFetcherLive.js";
27
+ import { OctokitInstance } from "./services/OctokitInstance.js";
28
+ import { AuthenticationError } from "./errors/AuthenticationError.js";
29
+ import { GitHubAppAuth } from "./layers/GitHubAppAuth.js";
30
+ import { GitHubAutoAuth } from "./layers/GitHubAutoAuth.js";
31
+ import { NetworkError } from "./errors/NetworkError.js";
32
+ import { ParseError } from "./errors/ParseError.js";
33
+ import { RateLimitError } from "./errors/RateLimitError.js";
34
+ import { GitHubClientLive } from "./layers/GitHubClientLive.js";
35
+ import { NodeResolver } from "./services/NodeResolver.js";
36
+ import { NodeResolverLive } from "./layers/NodeResolverLive.js";
37
+ import { NodeScheduleFetcherLive } from "./layers/NodeScheduleFetcherLive.js";
38
+ import { NodeVersionFetcherLive } from "./layers/NodeVersionFetcherLive.js";
39
+ import { CacheError } from "./errors/CacheError.js";
40
+ import { FreshnessError } from "./errors/FreshnessError.js";
41
+ import { InvalidInputError } from "./errors/InvalidInputError.js";
42
+ import { FreshBunCacheLive } from "./layers/FreshBunCacheLive.js";
43
+ import { FreshDenoCacheLive } from "./layers/FreshDenoCacheLive.js";
44
+ import { FreshNodeCacheLive } from "./layers/FreshNodeCacheLive.js";
45
+ import { GitHubTokenAuth, GitHubTokenAuthFromToken } from "./layers/GitHubTokenAuth.js";
46
+ import { OfflineBunCacheLive } from "./layers/OfflineBunCacheLive.js";
47
+ import { OfflineDenoCacheLive } from "./layers/OfflineDenoCacheLive.js";
48
+ import { OfflineNodeCacheLive } from "./layers/OfflineNodeCacheLive.js";
49
+ import { Effect, Layer } from "effect";
50
+
51
+ //#region src/index.ts
52
+ /**
53
+ * runtime-resolver
54
+ *
55
+ * Resolve semver-compatible versions of Node.js, Bun, and Deno runtimes.
56
+ * Promise-based API with offline fallback via build-time cache.
57
+ * Also exports Effect services, layers, and errors for advanced composition.
58
+ *
59
+ * @packageDocumentation
60
+ */
119
61
  const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
120
62
  const NodeFetchersLayer = Layer.merge(NodeVersionFetcherLive.pipe(Layer.provide(GitHubLayer)), NodeScheduleFetcherLive.pipe(Layer.provide(GitHubLayer)));
121
63
  const NodeCacheLayer = AutoNodeCacheLive.pipe(Layer.provide(NodeFetchersLayer));
@@ -124,17 +66,101 @@ const BunCacheLayer = AutoBunCacheLive.pipe(Layer.provide(BunVersionFetcherLive.
124
66
  const BunLayer = BunResolverLive.pipe(Layer.provide(BunCacheLayer));
125
67
  const DenoCacheLayer = AutoDenoCacheLive.pipe(Layer.provide(DenoVersionFetcherLive.pipe(Layer.provide(GitHubLayer))));
126
68
  const DenoLayer = DenoResolverLive.pipe(Layer.provide(DenoCacheLayer));
127
- const resolveNode = (options)=>Effect.runPromise(Effect.gen(function*() {
128
- const resolver = yield* NodeResolver;
129
- return yield* resolver.resolve(options);
130
- }).pipe(Effect.provide(NodeLayer)));
131
- const resolveBun = (options)=>Effect.runPromise(Effect.gen(function*() {
132
- const resolver = yield* BunResolver;
133
- return yield* resolver.resolve(options);
134
- }).pipe(Effect.provide(BunLayer)));
135
- const resolveDeno = (options)=>Effect.runPromise(Effect.gen(function*() {
136
- const resolver = yield* DenoResolver;
137
- return yield* resolver.resolve(options);
138
- }).pipe(Effect.provide(DenoLayer)));
139
- export { AuthenticationError, AutoBunCacheLive, AutoDenoCacheLive, AutoNodeCacheLive, BunRelease, BunReleaseCache, BunReleaseCacheLive, BunResolver, BunResolverLive, BunVersionFetcher, BunVersionFetcherLive, DenoRelease, DenoReleaseCache, DenoReleaseCacheLive, DenoResolver, DenoResolverLive, DenoVersionFetcher, DenoVersionFetcherLive, GitHubAppAuth, GitHubAutoAuth, GitHubClient, GitHubClientLive, GitHubTokenAuth, GitHubTokenAuthFromToken, NetworkError, NodeRelease, NodeReleaseCache, NodeReleaseCacheLive, NodeResolver, NodeResolverLive, NodeSchedule, NodeScheduleFetcher, NodeScheduleFetcherLive, NodeVersionFetcher, NodeVersionFetcherLive, OctokitInstance, ParseError, RateLimitError, VersionNotFoundError, createRuntimeCache } from "./711.js";
140
- export { CacheError, FreshBunCacheLive, FreshDenoCacheLive, FreshNodeCacheLive, FreshnessError, InvalidInputError, OfflineBunCacheLive, OfflineDenoCacheLive, OfflineNodeCacheLive, resolveBun, resolveDeno, resolveNode };
69
+ /**
70
+ * Resolve Node.js versions matching the given options.
71
+ *
72
+ * Fetches release data from the Node.js GitHub repository and the official
73
+ * release schedule API, then filters and ranks versions according to
74
+ * `options`. Authentication is read from `GITHUB_PERSONAL_ACCESS_TOKEN` or
75
+ * `GITHUB_TOKEN` environment variables. Falls back to a build-time offline
76
+ * cache when the network is unavailable.
77
+ *
78
+ * @param options - Filtering and ranking options. See {@link NodeResolverOptions}.
79
+ * @returns A promise that resolves to a {@link ResolvedVersions} object
80
+ * containing the matched version list, the highest matching version as
81
+ * `latest`, and optional `lts` / `default` strings.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import type { ResolvedVersions } from "runtime-resolver";
86
+ * import { resolveNode } from "runtime-resolver";
87
+ *
88
+ * const result: ResolvedVersions = await resolveNode({
89
+ * semverRange: ">=20.0.0",
90
+ * phases: ["active-lts"],
91
+ * });
92
+ * console.log(result.latest); // e.g. "22.11.0"
93
+ * console.log(result.versions); // all matching versions
94
+ * ```
95
+ *
96
+ * @see {@link NodeResolver} for the Effect-based API
97
+ * @see {@link NodeResolverOptions} for available options
98
+ *
99
+ * @public
100
+ */
101
+ const resolveNode = (options) => Effect.runPromise(Effect.gen(function* () {
102
+ return yield* (yield* NodeResolver).resolve(options);
103
+ }).pipe(Effect.provide(NodeLayer)));
104
+ /**
105
+ * Resolve Bun versions matching the given options.
106
+ *
107
+ * Fetches release data from the Bun GitHub repository, then filters and ranks
108
+ * versions according to `options`. Authentication is read from
109
+ * `GITHUB_PERSONAL_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
110
+ * Falls back to a build-time offline cache when the network is unavailable.
111
+ *
112
+ * @param options - Filtering and ranking options. See {@link BunResolverOptions}.
113
+ * @returns A promise that resolves to a {@link ResolvedVersions} object
114
+ * containing the matched version list and the highest matching version as
115
+ * `latest`.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * import type { ResolvedVersions } from "runtime-resolver";
120
+ * import { resolveBun } from "runtime-resolver";
121
+ *
122
+ * const result: ResolvedVersions = await resolveBun({ semverRange: "^1.0.0" });
123
+ * console.log(result.latest);
124
+ * ```
125
+ *
126
+ * @see {@link BunResolver} for the Effect-based API
127
+ * @see {@link BunResolverOptions} for available options
128
+ *
129
+ * @public
130
+ */
131
+ const resolveBun = (options) => Effect.runPromise(Effect.gen(function* () {
132
+ return yield* (yield* BunResolver).resolve(options);
133
+ }).pipe(Effect.provide(BunLayer)));
134
+ /**
135
+ * Resolve Deno versions matching the given options.
136
+ *
137
+ * Fetches release data from the Deno GitHub repository, then filters and ranks
138
+ * versions according to `options`. Authentication is read from
139
+ * `GITHUB_PERSONAL_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
140
+ * Falls back to a build-time offline cache when the network is unavailable.
141
+ *
142
+ * @param options - Filtering and ranking options. See {@link DenoResolverOptions}.
143
+ * @returns A promise that resolves to a {@link ResolvedVersions} object
144
+ * containing the matched version list and the highest matching version as
145
+ * `latest`.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import type { ResolvedVersions } from "runtime-resolver";
150
+ * import { resolveDeno } from "runtime-resolver";
151
+ *
152
+ * const result: ResolvedVersions = await resolveDeno({ semverRange: "^2.0.0" });
153
+ * console.log(result.latest);
154
+ * ```
155
+ *
156
+ * @see {@link DenoResolver} for the Effect-based API
157
+ * @see {@link DenoResolverOptions} for available options
158
+ *
159
+ * @public
160
+ */
161
+ const resolveDeno = (options) => Effect.runPromise(Effect.gen(function* () {
162
+ return yield* (yield* DenoResolver).resolve(options);
163
+ }).pipe(Effect.provide(DenoLayer)));
164
+
165
+ //#endregion
166
+ export { AuthenticationError, AutoBunCacheLive, AutoDenoCacheLive, AutoNodeCacheLive, BunRelease, BunReleaseCache, BunReleaseCacheLive, BunResolver, BunResolverLive, BunVersionFetcher, BunVersionFetcherLive, CacheError, DenoRelease, DenoReleaseCache, DenoReleaseCacheLive, DenoResolver, DenoResolverLive, DenoVersionFetcher, DenoVersionFetcherLive, FreshBunCacheLive, FreshDenoCacheLive, FreshNodeCacheLive, FreshnessError, GitHubAppAuth, GitHubAutoAuth, GitHubClient, GitHubClientLive, GitHubTokenAuth, GitHubTokenAuthFromToken, InvalidInputError, NetworkError, NodeRelease, NodeReleaseCache, NodeReleaseCacheLive, NodeResolver, NodeResolverLive, NodeSchedule, NodeScheduleFetcher, NodeScheduleFetcherLive, NodeVersionFetcher, NodeVersionFetcherLive, OctokitInstance, OfflineBunCacheLive, OfflineDenoCacheLive, OfflineNodeCacheLive, ParseError, RateLimitError, VersionNotFoundError, createRuntimeCache, resolveBun, resolveDeno, resolveNode };
@@ -0,0 +1,47 @@
1
+ import { bunDefaultInputs } from "../data/bun-defaults.js";
2
+ import { BunRelease } from "../schemas/bun-release.js";
3
+ import { BunReleaseCache } from "../services/BunReleaseCache.js";
4
+ import { BunVersionFetcher } from "../services/BunVersionFetcher.js";
5
+ import { BunReleaseCacheLive } from "./BunReleaseCacheLive.js";
6
+ import { Effect, Layer } from "effect";
7
+ import { SemVerParserLive, VersionCacheLive } from "semver-effect";
8
+
9
+ //#region src/layers/AutoBunCacheLive.ts
10
+ const setup = Effect.gen(function* () {
11
+ const cache = yield* BunReleaseCache;
12
+ const inputs = yield* (yield* BunVersionFetcher).fetch().pipe(Effect.map((result) => result.inputs), Effect.catchTags({
13
+ AuthenticationError: () => Effect.succeed(bunDefaultInputs),
14
+ NetworkError: () => Effect.succeed(bunDefaultInputs),
15
+ ParseError: () => Effect.succeed(bunDefaultInputs),
16
+ RateLimitError: () => Effect.succeed(bunDefaultInputs)
17
+ }));
18
+ const releases = yield* Effect.all(inputs.map((input) => BunRelease.fromInput(input)), { concurrency: "unbounded" }).pipe(Effect.orElseSucceed(() => []));
19
+ yield* cache.load(releases);
20
+ });
21
+ const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
22
+ const BaseCacheLayer = BunReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
23
+ /**
24
+ * Provides the {@link BunReleaseCache} service using the "Auto" cache strategy.
25
+ *
26
+ * Attempts to fetch live release data from the GitHub API via
27
+ * {@link BunVersionFetcher}. If the fetch fails for any reason — authentication
28
+ * errors, network errors, parse errors, or rate-limit errors — it silently falls
29
+ * back to the bundled default release data so the layer always succeeds.
30
+ *
31
+ * Use this strategy when you want the freshest data when available but can
32
+ * tolerate slightly stale bundled data in degraded environments.
33
+ *
34
+ * The three Bun cache strategy layers are:
35
+ * - `AutoBunCacheLive` — tries API first, falls back to bundled defaults on any error
36
+ * - {@link FreshBunCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
37
+ * - {@link OfflineBunCacheLive} — uses only bundled defaults, no network I/O
38
+ *
39
+ * @see {@link BunReleaseCache}
40
+ * @see {@link FreshBunCacheLive}
41
+ * @see {@link OfflineBunCacheLive}
42
+ * @public
43
+ */
44
+ const AutoBunCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => setup.pipe(Effect.provide(ctx))));
45
+
46
+ //#endregion
47
+ export { AutoBunCacheLive };
@@ -0,0 +1,47 @@
1
+ import { denoDefaultInputs } from "../data/deno-defaults.js";
2
+ import { DenoRelease } from "../schemas/deno-release.js";
3
+ import { DenoReleaseCache } from "../services/DenoReleaseCache.js";
4
+ import { DenoVersionFetcher } from "../services/DenoVersionFetcher.js";
5
+ import { DenoReleaseCacheLive } from "./DenoReleaseCacheLive.js";
6
+ import { Effect, Layer } from "effect";
7
+ import { SemVerParserLive, VersionCacheLive } from "semver-effect";
8
+
9
+ //#region src/layers/AutoDenoCacheLive.ts
10
+ const setup = Effect.gen(function* () {
11
+ const cache = yield* DenoReleaseCache;
12
+ const inputs = yield* (yield* DenoVersionFetcher).fetch().pipe(Effect.map((result) => result.inputs), Effect.catchTags({
13
+ AuthenticationError: () => Effect.succeed(denoDefaultInputs),
14
+ NetworkError: () => Effect.succeed(denoDefaultInputs),
15
+ ParseError: () => Effect.succeed(denoDefaultInputs),
16
+ RateLimitError: () => Effect.succeed(denoDefaultInputs)
17
+ }));
18
+ const releases = yield* Effect.all(inputs.map((input) => DenoRelease.fromInput(input)), { concurrency: "unbounded" }).pipe(Effect.orElseSucceed(() => []));
19
+ yield* cache.load(releases);
20
+ });
21
+ const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
22
+ const BaseCacheLayer = DenoReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
23
+ /**
24
+ * Provides the {@link DenoReleaseCache} service using the "Auto" cache strategy.
25
+ *
26
+ * Attempts to fetch live release data from the GitHub API via
27
+ * {@link DenoVersionFetcher}. If the fetch fails for any reason — authentication
28
+ * errors, network errors, parse errors, or rate-limit errors — it silently falls
29
+ * back to the bundled default release data so the layer always succeeds.
30
+ *
31
+ * Use this strategy when you want the freshest data when available but can
32
+ * tolerate slightly stale bundled data in degraded environments.
33
+ *
34
+ * The three Deno cache strategy layers are:
35
+ * - `AutoDenoCacheLive` — tries API first, falls back to bundled defaults on any error
36
+ * - {@link FreshDenoCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
37
+ * - {@link OfflineDenoCacheLive} — uses only bundled defaults, no network I/O
38
+ *
39
+ * @see {@link DenoReleaseCache}
40
+ * @see {@link FreshDenoCacheLive}
41
+ * @see {@link OfflineDenoCacheLive}
42
+ * @public
43
+ */
44
+ const AutoDenoCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => setup.pipe(Effect.provide(ctx))));
45
+
46
+ //#endregion
47
+ export { AutoDenoCacheLive };
@@ -0,0 +1,58 @@
1
+ import { nodeDefaultInputs, nodeDefaultScheduleData } from "../data/node-defaults.js";
2
+ import { NodeReleaseCache } from "../services/NodeReleaseCache.js";
3
+ import { NodeScheduleFetcher } from "../services/NodeScheduleFetcher.js";
4
+ import { NodeVersionFetcher } from "../services/NodeVersionFetcher.js";
5
+ import { NodeReleaseCacheLive } from "./NodeReleaseCacheLive.js";
6
+ import { Effect, Layer } from "effect";
7
+ import { SemVerParserLive, VersionCacheLive } from "semver-effect";
8
+
9
+ //#region src/layers/AutoNodeCacheLive.ts
10
+ const setup = Effect.gen(function* () {
11
+ const cache = yield* NodeReleaseCache;
12
+ const versionFetcher = yield* NodeVersionFetcher;
13
+ const scheduleFetcher = yield* NodeScheduleFetcher;
14
+ const nodeDefaults = {
15
+ inputs: nodeDefaultInputs,
16
+ scheduleData: nodeDefaultScheduleData
17
+ };
18
+ const { inputs, scheduleData } = yield* Effect.gen(function* () {
19
+ const [fetchResult, schedule] = yield* Effect.all([versionFetcher.fetch(), scheduleFetcher.fetch()]);
20
+ return {
21
+ inputs: fetchResult.inputs,
22
+ scheduleData: schedule
23
+ };
24
+ }).pipe(Effect.catchTags({
25
+ NetworkError: () => Effect.succeed(nodeDefaults),
26
+ ParseError: () => Effect.succeed(nodeDefaults)
27
+ }));
28
+ yield* cache.updateSchedule(scheduleData);
29
+ yield* cache.loadFromInputs(inputs);
30
+ });
31
+ const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
32
+ const BaseCacheLayer = NodeReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
33
+ /**
34
+ * Provides the {@link NodeReleaseCache} service using the "Auto" cache strategy.
35
+ *
36
+ * Attempts to fetch live release data from `nodejs.org/dist/index.json` via
37
+ * {@link NodeVersionFetcher} and the release schedule from the `nodejs/Release`
38
+ * repository via {@link NodeScheduleFetcher}. If either fetch fails — due to
39
+ * network errors or parse errors — the layer silently falls back to bundled
40
+ * default data (both version list and schedule) so it always succeeds.
41
+ *
42
+ * Node.js requires both a version fetcher and a schedule fetcher because LTS and
43
+ * maintenance phase information is tracked separately from the version index.
44
+ *
45
+ * The three Node cache strategy layers are:
46
+ * - `AutoNodeCacheLive` — tries API first, falls back to bundled defaults on any error
47
+ * - {@link FreshNodeCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
48
+ * - {@link OfflineNodeCacheLive} — uses only bundled defaults, no network I/O
49
+ *
50
+ * @see {@link NodeReleaseCache}
51
+ * @see {@link FreshNodeCacheLive}
52
+ * @see {@link OfflineNodeCacheLive}
53
+ * @public
54
+ */
55
+ const AutoNodeCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => setup.pipe(Effect.provide(ctx))));
56
+
57
+ //#endregion
58
+ export { AutoNodeCacheLive };
@@ -0,0 +1,25 @@
1
+ import { BunReleaseCache } from "../services/BunReleaseCache.js";
2
+ import { createRuntimeCache } from "./RuntimeCacheLive.js";
3
+ import { Layer } from "effect";
4
+
5
+ //#region src/layers/BunReleaseCacheLive.ts
6
+ /**
7
+ * Low-level layer that constructs the {@link BunReleaseCache} service backed by
8
+ * a `SemVerVersionCache` instance from `semver-effect`.
9
+ *
10
+ * This layer wires up the cache storage but does not populate it with any
11
+ * release data. Callers are responsible for loading releases after construction.
12
+ * In normal usage you should prefer the higher-level cache strategy layers
13
+ * instead of using this layer directly:
14
+ * - {@link AutoBunCacheLive} — API with fallback to bundled defaults
15
+ * - {@link FreshBunCacheLive} — API only, fails if unavailable
16
+ * - {@link OfflineBunCacheLive} — bundled defaults only
17
+ *
18
+ * @see {@link BunReleaseCache}
19
+ * @see {@link createRuntimeCache}
20
+ * @internal
21
+ */
22
+ const BunReleaseCacheLive = Layer.effect(BunReleaseCache, createRuntimeCache());
23
+
24
+ //#endregion
25
+ export { BunReleaseCacheLive };
@@ -0,0 +1,85 @@
1
+ import { BunReleaseCache } from "../services/BunReleaseCache.js";
2
+ import { VersionNotFoundError } from "../errors/VersionNotFoundError.js";
3
+ import { BunResolver } from "../services/BunResolver.js";
4
+ import { Effect, Layer } from "effect";
5
+ import { SemVer } from "semver-effect";
6
+
7
+ //#region src/layers/BunResolverLive.ts
8
+ /**
9
+ * Provides the {@link BunResolver} service backed by a {@link BunReleaseCache}.
10
+ *
11
+ * This layer composes with any of the three Bun cache strategy layers to form
12
+ * a complete resolver stack. It implements semver range filtering and increment
13
+ * grouping (latest per major, latest per minor, or all patch versions) using
14
+ * the releases already loaded into the cache.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { BunResolverLive, AutoBunCacheLive, BunVersionFetcherLive, GitHubClientLive, GitHubAutoAuth } from "runtime-resolver";
19
+ * import type { ResolvedVersions } from "runtime-resolver";
20
+ * import { BunResolver } from "runtime-resolver";
21
+ * import { Effect, Layer } from "effect";
22
+ *
23
+ * const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
24
+ * const CacheLayer = AutoBunCacheLive.pipe(Layer.provide(BunVersionFetcherLive.pipe(Layer.provide(GitHubLayer))));
25
+ * const ResolverLayer = BunResolverLive.pipe(Layer.provide(CacheLayer));
26
+ *
27
+ * const program = Effect.gen(function* () {
28
+ * const resolver = yield* BunResolver;
29
+ * const result = yield* resolver.resolve({ semverRange: "^1.0.0" });
30
+ * console.log(result.latest);
31
+ * });
32
+ *
33
+ * Effect.runPromise(program.pipe(Effect.provide(ResolverLayer)));
34
+ * ```
35
+ *
36
+ * @see {@link BunResolver}
37
+ * @see {@link BunReleaseCache}
38
+ * @see {@link AutoBunCacheLive}
39
+ * @see {@link FreshBunCacheLive}
40
+ * @see {@link OfflineBunCacheLive}
41
+ * @public
42
+ */
43
+ const BunResolverLive = Layer.effect(BunResolver, Effect.gen(function* () {
44
+ const cache = yield* BunReleaseCache;
45
+ return { resolve: (options) => Effect.gen(function* () {
46
+ const semverRange = options?.semverRange ?? "*";
47
+ const increments = options?.increments ?? "latest";
48
+ const matching = yield* cache.filter(semverRange).pipe(Effect.catchAll(() => Effect.succeed([])));
49
+ let resultReleases;
50
+ if (increments === "latest") {
51
+ const groups = /* @__PURE__ */ new Map();
52
+ for (const r of matching) {
53
+ const existing = groups.get(r.version.major);
54
+ if (!existing || SemVer.gt(r.version, existing.version)) groups.set(r.version.major, r);
55
+ }
56
+ resultReleases = [...groups.values()];
57
+ } else if (increments === "minor") {
58
+ const groups = /* @__PURE__ */ new Map();
59
+ for (const r of matching) {
60
+ const key = `${r.version.major}.${r.version.minor}`;
61
+ const existing = groups.get(key);
62
+ if (!existing || SemVer.gt(r.version, existing.version)) groups.set(key, r);
63
+ }
64
+ resultReleases = [...groups.values()];
65
+ } else resultReleases = [...matching];
66
+ if (resultReleases.length === 0) return yield* Effect.fail(new VersionNotFoundError({
67
+ runtime: "bun",
68
+ constraint: semverRange,
69
+ message: `No Bun versions found matching "${semverRange}"`
70
+ }));
71
+ const versions = SemVer.rsort(resultReleases.map((r) => r.version)).map((v) => v.toString());
72
+ const latest = versions[0];
73
+ let resolvedDefault;
74
+ if (options?.defaultVersion) resolvedDefault = yield* cache.resolve(options.defaultVersion).pipe(Effect.map((r) => r.version.toString()), Effect.catchAll(() => Effect.succeed(void 0)));
75
+ return {
76
+ source: "api",
77
+ versions,
78
+ latest,
79
+ ...resolvedDefault ? { default: resolvedDefault } : {}
80
+ };
81
+ }) };
82
+ }));
83
+
84
+ //#endregion
85
+ export { BunResolverLive };
@@ -0,0 +1,55 @@
1
+ import { BunVersionFetcher } from "../services/BunVersionFetcher.js";
2
+ import { retryOnRateLimit } from "../lib/retry.js";
3
+ import { tryParseSemVer } from "../lib/semver.js";
4
+ import { GitHubClient } from "../services/GitHubClient.js";
5
+ import { Effect, Layer, Option } from "effect";
6
+
7
+ //#region src/layers/BunVersionFetcherLive.ts
8
+ /**
9
+ * Strip bun- prefix and v/V prefix, parse to SemVer.
10
+ */
11
+ const normalizeBunTag = (tagName) => {
12
+ const version = tagName.startsWith("bun-") ? tagName.slice(4) : tagName;
13
+ return tryParseSemVer(version.startsWith("v") || version.startsWith("V") ? version.slice(1) : version);
14
+ };
15
+ /**
16
+ * Provides the {@link BunVersionFetcher} service using the live GitHub API.
17
+ *
18
+ * Fetches Bun releases from the `oven-sh/bun` GitHub repository using the
19
+ * provided {@link GitHubClient}. Draft releases and pre-releases are excluded.
20
+ * Tag names are normalized by stripping the `bun-` prefix and any leading `v`
21
+ * or `V` before parsing as semver.
22
+ *
23
+ * This layer is a required dependency of {@link AutoBunCacheLive} and
24
+ * {@link FreshBunCacheLive}. It is not needed by {@link OfflineBunCacheLive}.
25
+ *
26
+ * @see {@link BunVersionFetcher}
27
+ * @see {@link GitHubClient}
28
+ * @see {@link AutoBunCacheLive}
29
+ * @see {@link FreshBunCacheLive}
30
+ * @public
31
+ */
32
+ const BunVersionFetcherLive = Layer.effect(BunVersionFetcher, Effect.gen(function* () {
33
+ const client = yield* GitHubClient;
34
+ return { fetch: () => Effect.gen(function* () {
35
+ const releases = yield* retryOnRateLimit(client.listReleases("oven-sh", "bun", {
36
+ perPage: 100,
37
+ pages: 3
38
+ }));
39
+ const fallbackDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
40
+ const parsed = yield* Effect.forEach(releases.filter((r) => !r.draft && !r.prerelease), (release) => normalizeBunTag(release.tag_name).pipe(Effect.map((opt) => Option.isSome(opt) ? Option.some({
41
+ version: opt.value,
42
+ input: {
43
+ version: opt.value.toString(),
44
+ date: release.published_at ?? fallbackDate
45
+ }
46
+ }) : Option.none())), { concurrency: "unbounded" });
47
+ return {
48
+ versions: parsed.flatMap(Option.toArray).map(({ version }) => version),
49
+ inputs: parsed.flatMap(Option.toArray).map(({ input }) => input)
50
+ };
51
+ }) };
52
+ }));
53
+
54
+ //#endregion
55
+ export { BunVersionFetcherLive };
@@ -0,0 +1,25 @@
1
+ import { createRuntimeCache } from "./RuntimeCacheLive.js";
2
+ import { DenoReleaseCache } from "../services/DenoReleaseCache.js";
3
+ import { Layer } from "effect";
4
+
5
+ //#region src/layers/DenoReleaseCacheLive.ts
6
+ /**
7
+ * Low-level layer that constructs the {@link DenoReleaseCache} service backed by
8
+ * a `SemVerVersionCache` instance from `semver-effect`.
9
+ *
10
+ * This layer wires up the cache storage but does not populate it with any
11
+ * release data. Callers are responsible for loading releases after construction.
12
+ * In normal usage you should prefer the higher-level cache strategy layers
13
+ * instead of using this layer directly:
14
+ * - {@link AutoDenoCacheLive} — API with fallback to bundled defaults
15
+ * - {@link FreshDenoCacheLive} — API only, fails if unavailable
16
+ * - {@link OfflineDenoCacheLive} — bundled defaults only
17
+ *
18
+ * @see {@link DenoReleaseCache}
19
+ * @see {@link createRuntimeCache}
20
+ * @internal
21
+ */
22
+ const DenoReleaseCacheLive = Layer.effect(DenoReleaseCache, createRuntimeCache());
23
+
24
+ //#endregion
25
+ export { DenoReleaseCacheLive };