diff --git a/apps/use-cases-site/scripts/test-data-contract.mjs b/apps/use-cases-site/scripts/test-data-contract.mjs index c50c861..ca402a0 100644 --- a/apps/use-cases-site/scripts/test-data-contract.mjs +++ b/apps/use-cases-site/scripts/test-data-contract.mjs @@ -90,6 +90,7 @@ const deniedDisplayedStrings = [ await assertUseCasesMatchExecutableSamples(); await assertLiveRunnersMatchUseCaseContracts(); +await assertRuntimeApiUsesStaticFallbackOnPublicPages(); process.stdout.write("site-data-contract: passed\n"); async function assertUseCasesMatchExecutableSamples() { @@ -138,6 +139,46 @@ async function assertLiveRunnersMatchUseCaseContracts() { } } +async function assertRuntimeApiUsesStaticFallbackOnPublicPages() { + const { runLiveExample, shouldUseLocalRuntime } = await importRuntimeApi(); + + assert.equal(shouldUseLocalRuntime({ hostname: "workruntime.github.io" }, true), false); + assert.equal(shouldUseLocalRuntime({ hostname: "localhost" }, true), true); + assert.equal(shouldUseLocalRuntime({ hostname: "127.0.0.1" }, true), true); + assert.equal(shouldUseLocalRuntime({ hostname: "::1" }, true), true); + assert.equal(shouldUseLocalRuntime({ hostname: "localhost" }, false), false); + + const originalFetch = globalThis.fetch; + const hadLocation = Object.hasOwn(globalThis, "location"); + const originalLocation = globalThis.location; + let fetchCalled = false; + + Object.defineProperty(globalThis, "location", { + configurable: true, + value: { hostname: "workruntime.github.io" }, + }); + + globalThis.fetch = async () => { + fetchCalled = true; + throw new Error("Static Pages fallback must not call the local runtime API."); + }; + + try { + assert.equal(await runLiveExample("vibe-coding-agent"), null); + assert.equal(fetchCalled, false); + } finally { + globalThis.fetch = originalFetch; + if (hadLocation) { + Object.defineProperty(globalThis, "location", { + configurable: true, + value: originalLocation, + }); + } else { + delete globalThis.location; + } + } +} + function assertUseCaseLinesMatchSnapshot(useCase, result) { const rendered = [ ...Object.values(useCase.events).flat(), @@ -210,12 +251,20 @@ function runSample(samplePath) { } async function importUseCases() { + return importBundledTypeScript("src/data/useCases.ts", "useCases.mjs"); +} + +async function importRuntimeApi() { + return importBundledTypeScript("src/runtimeApi.ts", "runtimeApi.mjs"); +} + +async function importBundledTypeScript(relativePath, outputName) { const tempDir = mkdtempSync(join(tmpdir(), "workit-use-cases-")); - const outputPath = join(tempDir, "useCases.mjs"); + const outputPath = join(tempDir, outputName); try { await build({ - entryPoints: [resolve(siteRoot, "src", "data", "useCases.ts")], + entryPoints: [resolve(siteRoot, relativePath)], bundle: true, format: "esm", platform: "node", diff --git a/apps/use-cases-site/src/runtimeApi.ts b/apps/use-cases-site/src/runtimeApi.ts index 18c99fd..c2301d6 100644 --- a/apps/use-cases-site/src/runtimeApi.ts +++ b/apps/use-cases-site/src/runtimeApi.ts @@ -7,8 +7,27 @@ import type { ExampleRunResult } from "./types"; +interface RuntimeLocation { + hostname: string; +} + +const localRuntimeHosts = new Set(["localhost", "127.0.0.1", "::1"]); +const isViteDevRuntime = import.meta.env?.DEV === true; + +/** Return whether the browser can reach the local Node runtime API. */ +export function shouldUseLocalRuntime( + location: RuntimeLocation | undefined = globalThis.location, + isDevRuntime = isViteDevRuntime, +): boolean { + return isDevRuntime && location !== undefined && localRuntimeHosts.has(location.hostname); +} + /** Execute an example through the local Node runner when it is available. */ export async function runLiveExample(exampleId: string): Promise { + if (!shouldUseLocalRuntime()) { + return null; + } + try { const response = await fetch(`/api/examples/${encodeURIComponent(exampleId)}/run`, { headers: { accept: "application/json" },