-
Notifications
You must be signed in to change notification settings - Fork 0
Show warning in glue dev if old glue-runtime version is being used #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,9 +37,10 @@ import { toLines } from "@std/streams/unstable-to-lines"; | |
| import { pushable, pushableV } from "it-pushable"; | ||
| import { Select } from "@cliffy/prompt/select"; | ||
| import { toSortedByTypeThenLabel } from "../ui/utils.ts"; | ||
|
|
||
| import { getGlueName } from "../lib/glueNaming.ts"; | ||
| import { getOutdatedStreakRuntimeVersion } from "../lib/runtimeVersionCheck.ts"; | ||
| import { once } from "node:events"; | ||
| import type { CommonCommandOptions } from "./common.ts"; | ||
|
|
||
| const GLUE_DEV_PORT = getAvailablePort({ preferredPort: 8001 }); | ||
| const DEFAULT_DEBUG_PORT = 9229; | ||
|
|
@@ -74,6 +75,7 @@ export async function dev(options: DevOptions, filename: string) { | |
| const lifelineReconnectionEvents = pushableV<void>({ objectMode: true }); | ||
|
|
||
| const glueName = await getGlueName(filename, options.name); | ||
| await warnIfStreakRuntimeIsOutdated(filename, options.verbose); | ||
|
|
||
| const glueCliWebsocketAddr = await wsListen(() => { | ||
| if (!lifelineHasConnected) { | ||
|
|
@@ -509,6 +511,21 @@ function analyzeCode(_filename: string): AnalysisResult { | |
| return { errors: [] }; | ||
| } | ||
|
|
||
| async function warnIfStreakRuntimeIsOutdated(filename: string, verbose: boolean): Promise<void> { | ||
| try { | ||
| const outdatedRuntime = await getOutdatedStreakRuntimeVersion(filename); | ||
| if (!outdatedRuntime) { | ||
| return; | ||
| } | ||
| devProgressProps.outdatedRuntimeWarningInfo = outdatedRuntime; | ||
| renderUI(); | ||
| } catch (e) { | ||
| if (verbose) { | ||
| console.error("Caught error:", e); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+514
to
+527
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When running the version check in the background, we should ensure that async function warnIfStreakRuntimeIsOutdated(filename: string, verbose: boolean): Promise<void> {
try {
const outdatedRuntime = await getOutdatedStreakRuntimeVersion(filename);
if (!outdatedRuntime) {
return;
}
devProgressProps.outdatedRuntimeWarningInfo = outdatedRuntime;
if (inkInstance) {
renderUI();
}
} catch (e) {
if (verbose) {
console.error("Caught error:", e);
}
}
} |
||
|
|
||
| async function discoverRegistrations(signal?: AbortSignal): Promise<Registrations> { | ||
| signal?.throwIfAborted(); | ||
| const res = await fetch( | ||
|
|
@@ -650,7 +667,7 @@ const ServerWebsocketMessage = z.object({ | |
| }); | ||
| type ServerWebsocketMessage = z.infer<typeof ServerWebsocketMessage>; | ||
|
|
||
| interface DevOptions { | ||
| interface DevOptions extends CommonCommandOptions { | ||
| name?: string; | ||
| debug?: boolean; | ||
| inspectWait?: boolean; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { exists } from "@std/fs/exists"; | ||
| import * as path from "@std/path"; | ||
|
|
||
| export interface DenoConfigPaths { | ||
| denoJsonPath?: string; | ||
| denoLockPath?: string; | ||
| } | ||
|
|
||
| export async function findDenoConfigPaths(startDir: string): Promise<DenoConfigPaths> { | ||
| const denoJsonPath = await findFileInDirectoryOrAbove(startDir, ["deno.json", "deno.jsonc"]); | ||
| if (!denoJsonPath) { | ||
| return {}; | ||
| } | ||
|
|
||
| const denoLockPath = path.join(path.dirname(denoJsonPath), "deno.lock"); | ||
| return { | ||
| denoJsonPath, | ||
| denoLockPath: await exists(denoLockPath) ? denoLockPath : undefined, | ||
| }; | ||
| } | ||
|
|
||
| /** Generator that yields a directory and its parent directories */ | ||
| export function* parentDirectories(startDir: string): Generator<string> { | ||
| let currentDir = path.resolve(startDir); | ||
| while (true) { | ||
| yield currentDir; | ||
| const parentDir = path.dirname(currentDir); | ||
| if (parentDir === currentDir) { | ||
| break; | ||
| } | ||
| currentDir = parentDir; | ||
| } | ||
| } | ||
|
|
||
| /** Find any of the `searchNames` files in fileDir or its parent directories */ | ||
| export async function findFileInDirectoryOrAbove( | ||
| fileDir: string, | ||
| searchNames: string[], | ||
| ): Promise<string | undefined> { | ||
| try { | ||
| for (const currentDir of parentDirectories(fileDir)) { | ||
| for (const searchName of searchNames) { | ||
| const currentPath = path.join(currentDir, searchName); | ||
| if (await exists(currentPath)) { | ||
| return currentPath; | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| if (globalThis.Deno?.errors?.NotCapable && err instanceof Deno.errors.NotCapable) { | ||
| // If we only have permissions to a specific directory, then we may hit | ||
| // this error when trying to access parent directories. Assume we're not | ||
| // meant to access them and stop searching. | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { assertEquals } from "@std/assert"; | ||
| import { join } from "@std/path"; | ||
| import { findDenoConfigPaths } from "./denoConfig.ts"; | ||
| import { | ||
| extractStreakRuntimeVersionFromDenoLockText, | ||
| getOutdatedStreakRuntimeVersion, | ||
| } from "./runtimeVersionCheck.ts"; | ||
|
|
||
| async function createTempDir(): Promise<{ path: string } & AsyncDisposable> { | ||
| const tempDir = await Deno.makeTempDir({ prefix: "runtime-version-check-" }); | ||
| return { | ||
| path: tempDir, | ||
| async [Symbol.asyncDispose]() { | ||
| await Deno.remove(tempDir, { recursive: true }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| Deno.test("findDenoConfigPaths finds deno.json and deno.lock above the entrypoint", async () => { | ||
| await using tempDir = await createTempDir(); | ||
| const projectDir = join(tempDir.path, "project"); | ||
| const nestedDir = join(projectDir, "src", "workers"); | ||
| await Deno.mkdir(nestedDir, { recursive: true }); | ||
|
|
||
| const denoJsonPath = join(projectDir, "deno.json"); | ||
| const denoLockPath = join(projectDir, "deno.lock"); | ||
| await Deno.writeTextFile(denoJsonPath, "{}\n"); | ||
| await Deno.writeTextFile(denoLockPath, "{}\n"); | ||
|
|
||
| assertEquals(await findDenoConfigPaths(nestedDir), { denoJsonPath, denoLockPath }); | ||
| }); | ||
|
|
||
| Deno.test("extractStreakRuntimeVersionFromDenoLockText prefers the resolved lock version", () => { | ||
| const text = JSON.stringify({ | ||
| version: "5", | ||
| specifiers: { | ||
| "jsr:@std/path@^1.1.4": "1.1.4", | ||
| "jsr:@streak-glue/runtime@~0.2.23": "0.2.31", | ||
| }, | ||
| }); | ||
| assertEquals(extractStreakRuntimeVersionFromDenoLockText(text), "0.2.31"); | ||
| }); | ||
|
|
||
| Deno.test("getOutdatedStreakRuntimeVersion returns latest mismatch and formats warning", async () => { | ||
| await using tempDir = await createTempDir(); | ||
| const projectDir = join(tempDir.path, "project"); | ||
| const sourceDir = join(projectDir, "src"); | ||
| await Deno.mkdir(sourceDir, { recursive: true }); | ||
|
|
||
| const filename = join(sourceDir, "myGlue.ts"); | ||
| await Deno.writeTextFile(filename, "console.log('hello');\n"); | ||
| await Deno.writeTextFile( | ||
| join(projectDir, "deno.json"), | ||
| JSON.stringify({ | ||
| imports: { | ||
| "@streak-glue/runtime": "jsr:@streak-glue/runtime@~0.2.22", | ||
| }, | ||
| }), | ||
| ); | ||
| await Deno.writeTextFile( | ||
| join(projectDir, "deno.lock"), | ||
| JSON.stringify({ | ||
| version: "5", | ||
| specifiers: { | ||
| "jsr:@streak-glue/runtime@~0.2.22": "0.2.23", | ||
| }, | ||
| }), | ||
| ); | ||
| const outdatedInfo = await getOutdatedStreakRuntimeVersion( | ||
| filename, | ||
| () => | ||
| Promise.resolve( | ||
| new Response(JSON.stringify({ scope: "@streak-glue", name: "runtime", latest: "0.2.33" }), { | ||
| status: 200, | ||
| }), | ||
| ), | ||
| ); | ||
| assertEquals(outdatedInfo, { | ||
| currentVersion: "0.2.23", | ||
| latestVersion: "0.2.33", | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import * as path from "@std/path"; | ||
| import z from "zod"; | ||
| import { findDenoConfigPaths } from "./denoConfig.ts"; | ||
| import { GLUE_RUNTIME_PACKAGE } from "../common.ts"; | ||
|
|
||
| const GLUE_RUNTIME_JSR_SPECIFIER = `jsr:${GLUE_RUNTIME_PACKAGE}`; | ||
| const GLUE_RUNTIME_META_URL = `https://jsr.io/${GLUE_RUNTIME_PACKAGE}/meta.json`; | ||
|
|
||
| export interface OutdatedRuntimeInfo { | ||
| currentVersion: string; | ||
| latestVersion: string; | ||
| } | ||
|
|
||
| export async function getOutdatedStreakRuntimeVersion( | ||
| filename: string, | ||
| fetchImpl: typeof fetch = fetch, | ||
| ): Promise<OutdatedRuntimeInfo | undefined> { | ||
| const { denoLockPath } = await findDenoConfigPaths(path.dirname(filename)); | ||
| if (!denoLockPath) { | ||
| return undefined; | ||
| } | ||
| const currentVersion = extractStreakRuntimeVersionFromDenoLockText( | ||
| await Deno.readTextFile(denoLockPath), | ||
| ); | ||
| if (!currentVersion) { | ||
| return undefined; | ||
| } | ||
| const latestVersion = await fetchLatestStreakRuntimeVersion(fetchImpl); | ||
| if (latestVersion === currentVersion) { | ||
| return undefined; | ||
| } | ||
| return { currentVersion, latestVersion }; | ||
|
Comment on lines
+29
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The check …which is both misleading and incorrect. A semantic version comparison (e.g., using the Consider importing import { greaterThan, parse } from "@std/semver";
// ...
if (!greaterThan(parse(latestVersion), parse(currentVersion))) {
return undefined;
} |
||
| } | ||
|
|
||
| const DenoLockFile = z.object({ | ||
| version: z.string(), | ||
| specifiers: z.record(z.string(), z.string()).optional(), | ||
| }); | ||
| type DenoLockFile = z.infer<typeof DenoLockFile>; | ||
|
|
||
| export function extractStreakRuntimeVersionFromDenoLockText(text: string): string | undefined { | ||
| const parsed = DenoLockFile.parse(JSON.parse(text)); | ||
|
Macil marked this conversation as resolved.
|
||
| if (!parsed.specifiers) { | ||
| return undefined; | ||
| } | ||
| const searchString = `${GLUE_RUNTIME_JSR_SPECIFIER}@`; | ||
| for (const [specifier, resolvedVersion] of Object.entries(parsed.specifiers)) { | ||
| if ( | ||
| specifier === GLUE_RUNTIME_JSR_SPECIFIER || | ||
| specifier.startsWith(searchString) | ||
| ) { | ||
| return resolvedVersion; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| const JsrPackageMeta = z.object({ | ||
| scope: z.string(), | ||
| name: z.string(), | ||
| latest: z.string(), | ||
| }); | ||
| type JsrPackageMeta = z.infer<typeof JsrPackageMeta>; | ||
|
|
||
| async function fetchLatestStreakRuntimeVersion( | ||
| fetchImpl: typeof fetch, | ||
| ): Promise<string> { | ||
| const response = await fetchImpl(GLUE_RUNTIME_META_URL); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Failed to fetch ${GLUE_RUNTIME_META_URL}: ${response.status} ${response.statusText}`, | ||
| ); | ||
| } | ||
| const payload = JsrPackageMeta.parse(await response.json()); | ||
| return payload.latest; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awaiting
warnIfStreakRuntimeIsOutdatedhere blocks the startup of the dev session until the network request to JSR completes. This can cause the CLI to feel unresponsive for several seconds on slower connections. It is better to trigger this check in the background to allow the dev server to start immediately.