Archify Diagram Viewer 0.1.0
VS Code extension that previews Archify diagrams from their JSON sources (live, as you type) and opens rendered Archify HTML in a viewer tab. Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime. Adds validation diagnostics, JSON schema help, source-link navigation, export saving, render-to-file and open-in-browser commands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+146
@@ -0,0 +1,146 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
const OPENERS = {
|
||||
darwin: {
|
||||
command: 'open',
|
||||
method: 'open',
|
||||
timeoutMs: 5000,
|
||||
args: (target) => [target],
|
||||
},
|
||||
linux: {
|
||||
command: 'xdg-open',
|
||||
method: 'xdg-open',
|
||||
timeoutMs: 5000,
|
||||
args: (target) => [target],
|
||||
},
|
||||
win32: {
|
||||
command: 'powershell.exe',
|
||||
method: 'powershell',
|
||||
// PowerShell cold starts can approach five seconds on hosted Windows
|
||||
// runners. Keep the launch bounded without treating normal startup as a
|
||||
// timeout.
|
||||
timeoutMs: 15000,
|
||||
// Keep the command constant and pass the target through a child-only
|
||||
// environment variable. Paths are never interpolated into executable source.
|
||||
args: () => [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'Start-Process -FilePath $env:ARCHIFY_OPEN_TARGET',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function failureDetails(result, opener, timeoutMs) {
|
||||
const error = result?.error;
|
||||
if (error?.code === 'ENOENT') {
|
||||
return {
|
||||
code: 'opener/unavailable',
|
||||
reason: `Could not find ${opener.command}. Install or enable the platform opener, then open the target manually.`,
|
||||
systemCode: 'ENOENT',
|
||||
};
|
||||
}
|
||||
if (error?.code === 'ETIMEDOUT') {
|
||||
return {
|
||||
code: 'opener/timeout',
|
||||
reason: `${opener.command} did not finish within ${timeoutMs}ms. Open the target manually or retry when the system is less busy.`,
|
||||
systemCode: 'ETIMEDOUT',
|
||||
timeoutMs,
|
||||
};
|
||||
}
|
||||
if (error) {
|
||||
return {
|
||||
code: 'opener/spawn-failed',
|
||||
reason: error.message || `${opener.command} could not be started. Open the target manually.`,
|
||||
...(error.code ? { systemCode: String(error.code) } : {}),
|
||||
};
|
||||
}
|
||||
if (result?.signal) {
|
||||
return {
|
||||
code: 'opener/signaled',
|
||||
reason: `${opener.command} was terminated by ${result.signal}. Open the target manually.`,
|
||||
signal: result.signal,
|
||||
};
|
||||
}
|
||||
if (result?.status !== 0) {
|
||||
return {
|
||||
code: 'opener/nonzero-exit',
|
||||
reason: `${opener.command} exited with status ${result?.status ?? 'unknown'}. Open the target manually.`,
|
||||
exitCode: result?.status ?? null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function launchTimeout(value, fallback) {
|
||||
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
function launchTarget(target, options = {}) {
|
||||
const platform = options.platform || process.platform;
|
||||
const opener = OPENERS[platform];
|
||||
if (!opener) {
|
||||
return {
|
||||
requested: true,
|
||||
status: 'unsupported',
|
||||
target,
|
||||
method: null,
|
||||
};
|
||||
}
|
||||
|
||||
const spawn = options.spawn || spawnSync;
|
||||
const timeoutMs = launchTimeout(options.timeoutMs, opener.timeoutMs);
|
||||
let result;
|
||||
try {
|
||||
const spawnOptions = {
|
||||
encoding: 'utf8',
|
||||
shell: false,
|
||||
stdio: 'ignore',
|
||||
timeout: timeoutMs,
|
||||
windowsHide: true,
|
||||
};
|
||||
if (platform === 'win32') {
|
||||
spawnOptions.env = {
|
||||
...process.env,
|
||||
ARCHIFY_OPEN_TARGET: target,
|
||||
};
|
||||
}
|
||||
result = spawn(opener.command, opener.args(target), spawnOptions);
|
||||
} catch (error) {
|
||||
result = { error };
|
||||
}
|
||||
|
||||
const failure = failureDetails(result, opener, timeoutMs);
|
||||
let status = 'opened';
|
||||
if (failure?.code === 'opener/unavailable') status = 'unsupported';
|
||||
else if (failure) status = 'failed';
|
||||
|
||||
return {
|
||||
requested: true,
|
||||
status,
|
||||
target,
|
||||
method: opener.method,
|
||||
...(failure ? { failure } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function openArtifact(target, options = {}) {
|
||||
return launchTarget(path.resolve(target), options);
|
||||
}
|
||||
|
||||
export function openLoopbackUrl(target, options = {}) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(target);
|
||||
} catch {
|
||||
throw new TypeError('Preview URL must be a valid loopback HTTP URL.');
|
||||
}
|
||||
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port) {
|
||||
throw new TypeError('Preview URL must be a loopback URL using http://127.0.0.1:<port>.');
|
||||
}
|
||||
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
||||
throw new TypeError('Preview URL must target the loopback preview root.');
|
||||
}
|
||||
return launchTarget(url.href, options);
|
||||
}
|
||||
Reference in New Issue
Block a user