@zkov/pi-md-viewer 0.1.2 → 0.1.3

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.
@@ -9,7 +9,7 @@ import { discover, portRangeFromEnv, sendFiles, shutdownInstance } from "./disco
9
9
  import { FileRegistry } from "./registry.js";
10
10
  import { MarkdownRenderer } from "./renderer.js";
11
11
  import { ViewerServer } from "./server.js";
12
- import { launchViewer } from "./viewer-launcher.js";
12
+ import { launchViewer, runPlaywrightViewerHost } from "./viewer-launcher.js";
13
13
  const STARTUP_TIMEOUT_MS = 5000;
14
14
  function operationResult(payload) {
15
15
  return { url: null, added: [], rejected: [], viewerClients: 0, ...payload };
@@ -50,11 +50,20 @@ function parseArgs(argv, cwd) {
50
50
  parsed.viewer = requireValue(argv, ++index, arg);
51
51
  else if (arg === "--viewer-command")
52
52
  parsed.viewerCommand = requireValue(argv, ++index, arg);
53
+ else if (arg === "--playwright-viewer")
54
+ parsed.playwrightViewer = requireValue(argv, ++index, arg);
55
+ else if (arg === "--browser")
56
+ parsed.browser = parseBrowser(requireValue(argv, ++index, arg));
53
57
  else
54
58
  parsed.paths.push(arg);
55
59
  }
56
60
  return parsed;
57
61
  }
62
+ function parseBrowser(value) {
63
+ if (value === "chromium" || value === "firefox" || value === "webkit")
64
+ return value;
65
+ throw new Error("--browser must be chromium, firefox, or webkit");
66
+ }
58
67
  function requireValue(argv, index, flag) {
59
68
  const value = argv[index];
60
69
  if (value === undefined)
@@ -78,6 +87,10 @@ export async function main(argv, io = {}) {
78
87
  let args;
79
88
  try {
80
89
  args = parseArgs(argv, process.cwd());
90
+ if (args.playwrightViewer) {
91
+ await runPlaywrightViewerHost({ url: args.playwrightViewer, browserName: args.browser ?? "chromium", env });
92
+ return 0;
93
+ }
81
94
  if (args.serve)
82
95
  return await serve(args.port, args.paths);
83
96
  const result = await operate(args, env);
@@ -1,4 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { headedPlaywrightUnavailableReason, systemOpenCommand } from "./platform.js";
3
6
  import { expandViewerCommand, parseViewerCommand } from "./viewer-command.js";
4
7
  async function defaultSpawnDetached(command, args) {
@@ -51,37 +54,129 @@ function playwrightLaunchOptions(browserName, platform, env) {
51
54
  return options;
52
55
  }
53
56
  async function launchPlaywright(request) {
54
- const reason = headedPlaywrightUnavailableReason(request.platform, request.env);
57
+ const browserName = request.selection.config.playwright.browser;
58
+ if (!request.importPlaywright) {
59
+ await launchDetachedPlaywrightHost(request.url, browserName, request.spawnPlaywrightHost ?? defaultSpawnPlaywrightHost);
60
+ return;
61
+ }
62
+ await openPlaywrightPage({
63
+ url: request.url,
64
+ browserName,
65
+ platform: request.platform,
66
+ env: request.env,
67
+ importPlaywright: request.importPlaywright,
68
+ });
69
+ }
70
+ async function openPlaywrightPage(options) {
71
+ const reason = headedPlaywrightUnavailableReason(options.platform, options.env);
55
72
  if (reason === "unsupported-platform")
56
73
  throw new Error('Viewer "playwright" is not available on this platform');
57
74
  if (reason)
58
75
  throw new Error(`Viewer "playwright" is not available: ${reason}`);
59
- const browserName = request.selection.config.playwright.browser;
60
76
  let imported;
61
77
  try {
62
- imported = request.importPlaywright ? await request.importPlaywright() : await import("playwright");
78
+ imported = options.importPlaywright ? await options.importPlaywright() : await import("playwright");
63
79
  }
64
80
  catch (error) {
65
81
  if (isMissingPlaywrightPackage(error))
66
- throw new Error(playwrightInstallHint(browserName));
82
+ throw new Error(playwrightInstallHint(options.browserName));
67
83
  throw error;
68
84
  }
69
85
  const api = imported;
70
- const browserType = api[browserName];
86
+ const browserType = api[options.browserName];
71
87
  if (!browserType)
72
- throw new Error(`Playwright browser "${browserName}" is unavailable`);
88
+ throw new Error(`Playwright browser "${options.browserName}" is unavailable`);
73
89
  let browser;
74
90
  try {
75
- browser = await browserType.launch(playwrightLaunchOptions(browserName, request.platform, request.env));
91
+ browser = await browserType.launch(playwrightLaunchOptions(options.browserName, options.platform, options.env));
76
92
  }
77
93
  catch (error) {
78
94
  if (isMissingPlaywrightBrowser(error))
79
- throw new Error(playwrightBrowserInstallHint(browserName));
95
+ throw new Error(playwrightBrowserInstallHint(options.browserName));
80
96
  throw error;
81
97
  }
82
98
  const context = await browser.newContext({ viewport: null });
83
99
  const page = await context.newPage();
84
- await page.goto(request.url);
100
+ await page.goto(options.url);
101
+ return browser;
102
+ }
103
+ async function launchDetachedPlaywrightHost(url, browserName, spawnPlaywrightHost) {
104
+ const result = await spawnPlaywrightHost(["--playwright-viewer", url, "--browser", browserName]);
105
+ if (!result.ok)
106
+ throw new Error(result.message);
107
+ }
108
+ function defaultSpawnPlaywrightHost(args) {
109
+ return new Promise((resolve, reject) => {
110
+ const child = spawn(process.execPath, [entrypointPath(), ...args], {
111
+ detached: true,
112
+ stdio: ["ignore", "pipe", "pipe", "ignore"],
113
+ windowsHide: true,
114
+ env: { ...process.env, NODE_NO_WARNINGS: "1" },
115
+ });
116
+ let stdout = "";
117
+ let stderr = "";
118
+ let settled = false;
119
+ const timeout = setTimeout(() => finish({ ok: false, message: "Timed out waiting for Playwright viewer to open" }), 10_000);
120
+ function finish(result) {
121
+ if (settled)
122
+ return;
123
+ settled = true;
124
+ clearTimeout(timeout);
125
+ child.stdout?.destroy();
126
+ child.stderr?.destroy();
127
+ if (result.ok)
128
+ child.unref();
129
+ resolve(result);
130
+ }
131
+ child.once("error", reject);
132
+ child.once("exit", (code) => {
133
+ if (!settled)
134
+ finish({ ok: false, message: stderr.trim() || `Playwright viewer exited with status ${code ?? "unknown"}` });
135
+ });
136
+ child.stdout?.setEncoding("utf8");
137
+ child.stdout?.on("data", (chunk) => {
138
+ stdout += chunk;
139
+ const newline = stdout.indexOf("\n");
140
+ if (newline === -1)
141
+ return;
142
+ const line = stdout.slice(0, newline).trim();
143
+ try {
144
+ const payload = JSON.parse(line);
145
+ finish(payload.ok === true ? { ok: true } : { ok: false, message: typeof payload.message === "string" ? payload.message : "Playwright viewer failed to open" });
146
+ }
147
+ catch (error) {
148
+ finish({ ok: false, message: `Invalid Playwright viewer response: ${error instanceof Error ? error.message : String(error)}` });
149
+ }
150
+ });
151
+ child.stderr?.setEncoding("utf8");
152
+ child.stderr?.on("data", (chunk) => { stderr += chunk; });
153
+ });
154
+ }
155
+ function entrypointPath() {
156
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
157
+ const dist = path.join(root, "dist", "src-ts", "main.js");
158
+ if (existsSync(dist))
159
+ return dist;
160
+ return fileURLToPath(import.meta.url).replace(/viewer-launcher\.(ts|js)$/, "main.js");
161
+ }
162
+ export async function runPlaywrightViewerHost(options) {
163
+ const output = options.stdout ?? console.log;
164
+ try {
165
+ const browser = await openPlaywrightPage({
166
+ url: options.url,
167
+ browserName: options.browserName,
168
+ platform: options.platform ?? process.platform,
169
+ env: options.env ?? process.env,
170
+ importPlaywright: options.importPlaywright,
171
+ });
172
+ output(JSON.stringify({ ok: true }));
173
+ await new Promise((resolve) => {
174
+ browser.on?.("disconnected", resolve);
175
+ });
176
+ }
177
+ catch (error) {
178
+ output(JSON.stringify({ ok: false, message: error instanceof Error ? error.message : String(error) }));
179
+ }
85
180
  }
86
181
  export async function launchViewer(request) {
87
182
  const spawnDetached = request.spawnDetached ?? defaultSpawnDetached;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkov/pi-md-viewer",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local loopback Markdown viewer tool for pi with configurable browser launchers",
5
5
  "author": "Zkov <zkov@yandex.ru>",
6
6
  "license": "MIT",
@@ -247,22 +247,26 @@ hr { margin: 40px 0; border: 0; border-top: 1px solid var(--line); }
247
247
  @media (max-width: 980px) {
248
248
  .mobile-bar {
249
249
  position: sticky;
250
- z-index: 20;
250
+ z-index: 40;
251
251
  top: 0;
252
252
  display: flex;
253
253
  align-items: center;
254
254
  justify-content: space-between;
255
+ min-height: 49px;
255
256
  padding: 10px 16px;
256
257
  border-bottom: 1px solid var(--line);
257
258
  background: rgba(10, 13, 18, .94);
258
259
  }
259
- .layout { display: block; padding: 20px 18px 80px; }
260
+ .layout { display: block; padding: 28px 18px 80px; }
260
261
  .files-panel {
261
262
  display: none;
262
- position: relative;
263
- top: auto;
264
- max-height: 48vh;
265
- margin: 0 auto 28px;
263
+ position: fixed;
264
+ z-index: 35;
265
+ top: 57px;
266
+ left: 18px;
267
+ right: 18px;
268
+ max-height: calc(100vh - 73px);
269
+ margin: 0 auto;
266
270
  max-width: 820px;
267
271
  }
268
272
  .files-panel.open { display: block; }