Skip to content

Commit 69a8201

Browse files
authored
fix(backend): devel backend detection + max height calc (#51)
* feat(build): clean dist script leaves browsers and gitignore * feat(post-install): add script to set up local dx * fix(backend): devel backend detection + max height calc
1 parent dbe1c83 commit 69a8201

16 files changed

Lines changed: 250 additions & 108 deletions

File tree

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
layout python
22
layout node
3+
export PLAYWRIGHT_BROWSERS_PATH=./dist/.local-browsers

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ See: [lazy.nvim](https://github.com/folke/lazy.nvim)
7777
```lua
7878
{
7979
'mistweaverco/snap.nvim',
80-
version = 'v1.4.1',
80+
version = 'v1.4.2',
8181
---@type SnapUserConfig
8282
opts = {}
8383
},
@@ -93,7 +93,7 @@ See: [packer.nvim](https://github.com/wbthomason/packer.nvim)
9393
```lua
9494
use {
9595
'mistweaverco/snap.nvim',
96-
tag = 'v1.4.1',
96+
tag = 'v1.4.2',
9797
config = function()
9898

9999
---@type SnapUserConfig
@@ -112,7 +112,7 @@ use {
112112
```lua
113113
vim.pack.add({
114114
src = 'https://github.com/mistweaverco/snap.nvim.git',
115-
version = 'v1.4.1',
115+
version = 'v1.4.2',
116116
})
117117
---@type SnapUserConfig
118118
local cfg = {}
@@ -244,7 +244,7 @@ This would then translate to the following `font_settings`:
244244
```lua
245245
return {
246246
"mistweaverco/snap.nvim",
247-
version = 'v1.4.1',
247+
version = 'v1.4.2',
248248
---@type SnapUserConfig
249249
opts = {
250250
template = "linux",
@@ -373,7 +373,7 @@ by running `bun install` in the plugin directory.
373373
```lua
374374
{
375375
'mistweaverco/snap.nvim',
376-
version = 'v1.4.1',
376+
version = 'v1.4.2',
377377
opts = {
378378
timeout = 5000, -- Timeout for screenshot command in milliseconds
379379
log_level = "error", -- Log level for debugging (e.g., "trace", "debug", "info", "warn", "error", "off")

backend/bun/.prettierignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pnpm-lock.yaml
2+
package-lock.json
3+
yarn.lock
4+
templates/*.hbs
5+
dist/
6+
web/build/
7+
web/.svelte-kit/
8+
node_modules/

backend/bun/.prettierrc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"useTabs": false,
3+
"singleQuote": false,
4+
"trailingComma": "all",
5+
"printWidth": 120,
6+
"plugins": ["prettier-plugin-svelte"],
7+
"overrides": [
8+
{
9+
"files": "*.svelte",
10+
"options": {
11+
"parser": "svelte",
12+
"useTabs": true
13+
}
14+
}
15+
]
16+
}

backend/bun/eslint.config.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { defineConfig, globalIgnores } from "eslint/config";
2+
import ts from "typescript-eslint";
3+
import svelte from "eslint-plugin-svelte";
4+
import svelteParser from "svelte-eslint-parser";
5+
import markdown from "@eslint/markdown";
6+
import css from "@eslint/css";
7+
import { tailwind4 } from "tailwind-csstree";
8+
import prettier from "eslint-config-prettier";
9+
10+
export default defineConfig(
11+
globalIgnores([".DS_Store", "dist/", "node_modules/", "web/build/", "web/.svelte-kit/", ".direnv/"]),
12+
...ts.configs.recommended,
13+
{
14+
files: ["backend/bun/**.ts", "eslint.config.ts"],
15+
plugins: {
16+
"@typescript-eslint": ts.plugin,
17+
},
18+
languageOptions: {
19+
parser: ts.parser,
20+
parserOptions: {
21+
projectService: true,
22+
},
23+
},
24+
},
25+
{
26+
files: ["web/**/*.svelte"],
27+
plugins: { svelte },
28+
languageOptions: {
29+
parser: svelteParser,
30+
parserOptions: {
31+
parser: ts.parser,
32+
extraFileExtensions: [".svelte"],
33+
},
34+
},
35+
rules: {
36+
...svelte.configs["flat/recommended"][0].rules,
37+
},
38+
},
39+
{
40+
files: ["**/*.css"],
41+
plugins: { css },
42+
language: "css/css",
43+
languageOptions: { customSyntax: tailwind4 },
44+
rules: { "css/no-invalid-at-rules": "off" },
45+
},
46+
{
47+
files: ["**/*.md"],
48+
plugins: { markdown },
49+
language: "markdown/gfm",
50+
rules: {
51+
"markdown/no-missing-label-refs": [
52+
"error",
53+
{
54+
allowLabels: ["!NOTE", "!TIP", "!IMPORTANT", "!WARNING", "!CAUTION"],
55+
},
56+
],
57+
},
58+
},
59+
prettier,
60+
);

backend/bun/src/utils/browser.ts

Lines changed: 23 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,35 @@ function isExecutable(file: string): boolean {
1616
}
1717
}
1818

19+
const EXE_NAMES =
20+
process.platform === "win32"
21+
? ["chrome.exe", "chrome-headless-shell.exe"]
22+
: process.platform === "darwin"
23+
? ["chrome-headless-shell", "Chromium"]
24+
: ["chrome", "chrome-headless-shell"];
25+
1926
/**
20-
* Finds Chromium executable in a given directory
21-
* @param playwrightDir - Directory containing chromium files
27+
* Searches recursively for the Chromium executable in the given directory
28+
* and its subdirectories.
29+
* @param dir - Directory containing chromium files
2230
* @returns The path to the Chromium executable, or null if not found
2331
*/
24-
function findChromiumInDirectory(playwrightDir: string): string | null {
25-
if (!fs.existsSync(playwrightDir)) {
32+
function findChromiumInDirectory(dir: string): string | null {
33+
if (!fs.existsSync(dir)) {
2634
return null;
2735
}
36+
const entries = fs.readdirSync(dir, { withFileTypes: true });
37+
for (const entry of entries) {
38+
const fullPath = path.join(dir, entry.name);
2839

29-
// For bundled builds: executable is directly in playwright directory
30-
// For cache (development): executable is in chromium-* or chromium_headless_shell* subdirectories
31-
32-
// Executable names to search for
33-
const exeNames =
34-
process.platform === "win32"
35-
? ["chrome.exe", "chrome-headless-shell.exe"]
36-
: process.platform === "darwin"
37-
? ["chrome-headless-shell", "Chromium"]
38-
: ["chrome", "chrome-headless-shell"];
39-
40-
// First, check directly in playwright directory (bundled builds)
41-
for (const exeName of exeNames) {
42-
const directPath = path.join(playwrightDir, exeName);
43-
if (fs.existsSync(directPath) && isExecutable(directPath)) {
44-
return directPath;
45-
}
46-
}
47-
48-
// Fall back to cache structure: look for chromium-* or chromium_headless_shell* directories
49-
try {
50-
const entries = fs.readdirSync(playwrightDir, { withFileTypes: true });
51-
for (const entry of entries) {
52-
if (
53-
entry.isDirectory() &&
54-
(entry.name.startsWith("chromium-") || entry.name.startsWith("chromium_headless_shell"))
55-
) {
56-
const cacheDir = path.join(playwrightDir, entry.name);
57-
// Recursively search in cache directory (max depth 2)
58-
function search(dir: string, depth: number = 0): string | null {
59-
if (depth > 2) return null;
60-
try {
61-
const dirEntries = fs.readdirSync(dir, { withFileTypes: true });
62-
for (const dirEntry of dirEntries) {
63-
const fullPath = path.join(dir, dirEntry.name);
64-
if (dirEntry.isFile() && exeNames.includes(dirEntry.name) && isExecutable(fullPath)) {
65-
return fullPath;
66-
} else if (dirEntry.isDirectory() && !dirEntry.name.startsWith(".")) {
67-
const found = search(fullPath, depth + 1);
68-
if (found) return found;
69-
}
70-
}
71-
} catch {
72-
// Ignore errors
73-
}
74-
return null;
75-
}
76-
const found = search(cacheDir);
77-
if (found) return found;
40+
if (entry.isFile()) {
41+
if (EXE_NAMES.includes(entry.name) && isExecutable(fullPath)) {
42+
return fullPath;
7843
}
44+
} else if (entry.isDirectory()) {
45+
const found = findChromiumInDirectory(fullPath);
46+
if (found) return found;
7947
}
80-
} catch {
81-
// Ignore errors
8248
}
8349

8450
return null;
@@ -104,8 +70,8 @@ export function resolveChromiumExecutable(): string {
10470
return bundledExecutable;
10571
}
10672

107-
// Fall back to Playwright cache directory (development mode)
108-
const cacheDir = path.join(os.homedir(), ".cache", "ms-playwright");
73+
// Development mode
74+
const cacheDir = path.join(process.cwd(), "dist", ".local-browsers");
10975
const cacheExecutable = findChromiumInDirectory(cacheDir);
11076
if (cacheExecutable) {
11177
return cacheExecutable;

backend/bun/src/utils/htmlToImage.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ async function loadPlaywright() {
4343
return playwright.chromium;
4444
} catch (error) {
4545
throw new Error(
46-
`Failed to load playwright-core: ${
47-
error instanceof Error ? error.message : String(error)
48-
}. ` +
46+
`Failed to load playwright-core: ${error instanceof Error ? error.message : String(error)}. ` +
4947
"Make sure playwright-core is installed or bundled with the application in node_modules/playwright-core.",
5048
);
5149
}
@@ -56,9 +54,7 @@ async function loadPlaywright() {
5654
* @param options - Configuration options for the image generation
5755
* @returns Buffer containing the image data
5856
*/
59-
export async function htmlToImage(
60-
options: HtmlToImageOptions,
61-
): Promise<Buffer> {
57+
export async function htmlToImage(options: HtmlToImageOptions): Promise<Buffer> {
6258
const {
6359
html,
6460
output,
@@ -75,12 +71,7 @@ export async function htmlToImage(
7571
// Launch browser with appropriate settings
7672
const launchOptions: Parameters<typeof chromium.launch>[0] = {
7773
headless: true,
78-
args: [
79-
"--no-sandbox",
80-
"--disable-setuid-sandbox",
81-
"--disable-dev-shm-usage",
82-
"--disable-gpu",
83-
],
74+
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
8475
};
8576

8677
// If executable path is provided, use it explicitly
@@ -95,11 +86,7 @@ export async function htmlToImage(
9586

9687
// Set content with HTML
9788
await page.setContent(html, {
98-
waitUntil: waitUntil as
99-
| "load"
100-
| "domcontentloaded"
101-
| "networkidle"
102-
| "commit",
89+
waitUntil: waitUntil as "load" | "domcontentloaded" | "networkidle" | "commit",
10390
});
10491

10592
// Measure the actual rendered content width to respect max-width constraints
@@ -114,11 +101,7 @@ export async function htmlToImage(
114101

115102
// Use body dimensions which respect max-width constraints
116103
const width = bodyRect.width;
117-
const height = Math.max(
118-
body.scrollHeight,
119-
document.documentElement.scrollHeight,
120-
bodyRect.height,
121-
);
104+
const height = bodyRect.top + bodyRect.height;
122105

123106
return {
124107
width: Math.ceil(width),
@@ -156,9 +139,7 @@ export async function htmlToImage(
156139
});
157140

158141
// Convert to Buffer if needed
159-
const buffer = screenshotBuffer instanceof Buffer
160-
? screenshotBuffer
161-
: Buffer.from(screenshotBuffer);
142+
const buffer = screenshotBuffer instanceof Buffer ? screenshotBuffer : Buffer.from(screenshotBuffer);
162143

163144
return buffer;
164145
} finally {

lua/snap/export.lua

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ local function check_backend_health(callback)
2121
if backend_bin_path == "" then
2222
error(conf.development_mode.backend .. " executable not found in PATH")
2323
end
24-
cwd = payload.get_absolute_plugin_path("backend", conf.development_mode.backend)
25-
if not vim.fn.isdirectory(cwd) then
26-
error("Backend directory not found: " .. cwd)
27-
end
28-
system_args = { backend_bin_path, "run", "src/index.ts", "health" }
24+
cwd = payload.get_absolute_plugin_path()
25+
system_args = { backend_bin_path, "run", "backend/bun/src/index.ts", "health" }
2926
end
3027
end
3128

@@ -86,12 +83,12 @@ local function install_backend(progress_callback, completion_callback)
8683
if backend_bin_path == "" then
8784
error(conf.development_mode.backend .. " executable not found in PATH")
8885
end
89-
cwd = payload.get_absolute_plugin_path("backend", conf.development_mode.backend)
86+
cwd = payload.get_absolute_plugin_path()
9087
if not vim.fn.isdirectory(cwd) then
9188
error("Backend directory not found: " .. cwd)
9289
end
9390
-- Use src/index.ts explicitly to ensure command line arguments are passed correctly
94-
system_args = { backend_bin_path, "run", "src/index.ts", "install" }
91+
system_args = { backend_bin_path, "run", "backend/bun/src/index.ts", "install" }
9592
end
9693
end
9794

@@ -115,15 +112,17 @@ local function install_backend(progress_callback, completion_callback)
115112
debounce_timer = vim.fn.timer_start(150, function()
116113
if pending_progress and progress_callback then
117114
-- Only report 100% or "completed" once
118-
if (pending_progress.progress == 100 or pending_progress.status == "completed") then
115+
if pending_progress.progress == 100 or pending_progress.status == "completed" then
119116
if not progress_100_reported then
120117
progress_callback(pending_progress)
121118
progress_100_reported = true
122119
end
123120
else
124121
-- Only report if status or progress value changed
125-
if pending_progress.status ~= last_progress_status or
126-
(pending_progress.progress and pending_progress.progress ~= last_progress_value) then
122+
if
123+
pending_progress.status ~= last_progress_status
124+
or (pending_progress.progress and pending_progress.progress ~= last_progress_value)
125+
then
127126
progress_callback(pending_progress)
128127
last_progress_status = pending_progress.status
129128
last_progress_value = pending_progress.progress
@@ -195,7 +194,9 @@ local function install_backend(progress_callback, completion_callback)
195194
end
196195
-- Flush any pending progress immediately
197196
if pending_progress and progress_callback then
198-
if not (pending_progress.progress == 100 or pending_progress.status == "completed") or not progress_100_reported then
197+
if
198+
not (pending_progress.progress == 100 or pending_progress.status == "completed") or not progress_100_reported
199+
then
199200
progress_callback(pending_progress)
200201
if pending_progress.progress == 100 or pending_progress.status == "completed" then
201202
progress_100_reported = true
@@ -294,11 +295,11 @@ local function run_backend_export(opts, export_type, success_message)
294295
if backend_bin_path == "" then
295296
error(conf.development_mode.backend .. " executable not found in PATH")
296297
end
297-
cwd = payload.get_absolute_plugin_path("backend", conf.development_mode.backend)
298+
cwd = payload.get_absolute_plugin_path()
298299
if not vim.fn.isdirectory(cwd) then
299300
error("Backend directory not found: " .. cwd)
300301
end
301-
system_args = { backend_bin_path, "run", "." }
302+
system_args = { backend_bin_path, "run", "backend/bun/src/index.ts" }
302303
end
303304
end
304305

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
return "1.4.0"
1+
return "1.4.2"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
return "1.4.1"
1+
return "1.4.2"

0 commit comments

Comments
 (0)