From d89ce2aab92bcefe1209bdfded4d42537aa1fe56 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Jan 2026 00:50:17 +0900 Subject: [PATCH] fix(@probitas/probitas): support JSR URL template resolution in subprocess The subprocess template resolver failed when running from JSR URLs (e.g., Homebrew installations) because it only accepted file:// modules. This prevented the CLI from working in compiled/remote execution contexts. Changes: - Add getLocalModulePrefix() to determine "local" module prefix by protocol - Support file://, JSR (https://jsr.io/@scope/package/version/), and other URLs - Add comprehensive tests for different URL types This enables Homebrew and other JSR-based distributions to work without pre-embedded templates. --- src/cli/subprocess_template.ts | 40 ++++++++++++++++++++-- src/cli/subprocess_template_test.ts | 51 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/cli/subprocess_template_test.ts diff --git a/src/cli/subprocess_template.ts b/src/cli/subprocess_template.ts index 22e5ab0..64c635e 100644 --- a/src/cli/subprocess_template.ts +++ b/src/cli/subprocess_template.ts @@ -35,9 +35,13 @@ export async function resolveSubprocessTemplate( // Build dependency graph to find all imports const graph = await createGraph(templateUrl.href); - // Collect all local (file://) modules from the graph + // Determine which modules are "local" (should be bundled) + // For file:// URLs, all file:// modules are local + // For https:// URLs (JSR), modules under the same package are local + const localModulePrefix = getLocalModulePrefix(templateUrl); + const localModules = graph.modules.filter( - (m) => m.specifier.startsWith("file://") && !m.error, + (m) => m.specifier.startsWith(localModulePrefix) && !m.error, ); if (localModules.length === 0) { @@ -87,6 +91,31 @@ export async function resolveSubprocessTemplate( return files; } +/** + * Get the prefix for identifying "local" modules that should be bundled + * + * - For file:// URLs: all file:// modules are considered local + * - For JSR URLs (https://jsr.io/@scope/package/version/...): modules under the same package + * - For other remote URLs: modules in the same directory tree as the template + */ +function getLocalModulePrefix(templateUrl: URL): string { + if (templateUrl.protocol === "file:") { + return "file://"; + } + + // For JSR URLs: https://jsr.io/@scope/package/version/... + // Extract the package base URL to include all modules from the same package + const jsrMatch = templateUrl.href.match( + /^(https?:\/\/jsr\.io\/@[^/]+\/[^/]+\/[^/]+\/)/, + ); + if (jsrMatch) { + return jsrMatch[1]; + } + + // For other remote URLs, use the directory of the template + return templateUrl.href.substring(0, templateUrl.href.lastIndexOf("/") + 1); +} + /** * Find the common base directory for a list of file:// URLs */ @@ -258,3 +287,10 @@ function resolveModuleDependencies( return resolved; } + +/** + * @internal + */ +export const _internal = { + getLocalModulePrefix, +}; diff --git a/src/cli/subprocess_template_test.ts b/src/cli/subprocess_template_test.ts new file mode 100644 index 0000000..495f061 --- /dev/null +++ b/src/cli/subprocess_template_test.ts @@ -0,0 +1,51 @@ +import { assertEquals } from "@std/assert"; +import { _internal } from "./subprocess_template.ts"; + +const { getLocalModulePrefix } = _internal; + +Deno.test("getLocalModulePrefix", async (t) => { + await t.step("returns file:// for local file URLs", () => { + const url = new URL("file:///path/to/template.ts"); + assertEquals(getLocalModulePrefix(url), "file://"); + }); + + await t.step("extracts JSR package base for JSR URLs", () => { + const url = new URL( + "https://jsr.io/@probitas/probitas/0.19.0/src/cli/_templates/run.ts", + ); + assertEquals( + getLocalModulePrefix(url), + "https://jsr.io/@probitas/probitas/0.19.0/", + ); + }); + + await t.step("handles scoped JSR packages with different versions", () => { + const url = new URL( + "https://jsr.io/@std/path/1.0.0/mod.ts", + ); + assertEquals( + getLocalModulePrefix(url), + "https://jsr.io/@std/path/1.0.0/", + ); + }); + + await t.step("returns directory for non-JSR remote URLs", () => { + const url = new URL( + "https://example.com/modules/template.ts", + ); + assertEquals( + getLocalModulePrefix(url), + "https://example.com/modules/", + ); + }); + + await t.step("handles HTTP (non-HTTPS) JSR URLs", () => { + const url = new URL( + "http://jsr.io/@probitas/probitas/0.19.0/src/cli/_templates/run.ts", + ); + assertEquals( + getLocalModulePrefix(url), + "http://jsr.io/@probitas/probitas/0.19.0/", + ); + }); +});