playwright-deblock 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # playwright-deblock
2
+
3
+ Automatic fix for Playwright `file://` navigation blocks.
4
+ One `require()`, works forever.
5
+
6
+ ## Problem
7
+
8
+ Restricted Playwright runners block `file://` URLs:
9
+
10
+ ```
11
+ Access to 'file:' URL is blocked. Allowed protocols: http, https, about, data.
12
+ ```
13
+
14
+ ## Solution
15
+
16
+ ```bash
17
+ npm install playwright-deblock
18
+ ```
19
+
20
+ ```js
21
+ require("playwright-deblock");
22
+ ```
23
+
24
+ Done. Every `page.goto("file://...")` is now automatically served over `http://localhost`.
25
+
26
+ ## Usage
27
+
28
+ ### Auto-patch (recommended)
29
+
30
+ ```js
31
+ // Add this ONE line at the top of your script or test
32
+ require("playwright-deblock");
33
+
34
+ // Then use file:// like normal — it just works
35
+ const browser = await chromium.launch();
36
+ const page = await browser.newPage();
37
+ await page.goto("file:///home/user/page.html"); // auto-redirected to http://
38
+ ```
39
+
40
+ ### Playwright config (global)
41
+
42
+ ```js
43
+ // playwright.config.js
44
+ module.exports = {
45
+ globalSetup: require.resolve("playwright-deblock/register"),
46
+ };
47
+ ```
48
+
49
+ Or via CLI:
50
+
51
+ ```bash
52
+ npx playwright test --require playwright-deblock/register
53
+ ```
54
+
55
+ ### With options
56
+
57
+ ```js
58
+ const deblock = require("playwright-deblock");
59
+ deblock.apply({ strategy: "data", silent: true });
60
+ ```
61
+
62
+ ### Manual helper (no monkey-patching)
63
+
64
+ ```js
65
+ const { serveAndGoto, cleanup } = require("playwright-deblock/helper");
66
+
67
+ test("loads local HTML", async ({ page }) => {
68
+ await serveAndGoto(page, "/path/to/file.html");
69
+ await expect(page.locator("h1")).toBeVisible();
70
+ });
71
+
72
+ test.afterAll(() => cleanup());
73
+ ```
74
+
75
+ ## How it works
76
+
77
+ | Strategy | Protocol | Method |
78
+ |---|---|---|
79
+ | `http` (primary) | `http://` | Starts localhost server, serves the file's directory |
80
+ | `data` (fallback) | `data:` | Inlines CSS/JS/images into a data: URL |
81
+
82
+ - Original HTML files are **never modified** on disk
83
+ - One server per directory, reused across calls
84
+ - Servers auto-cleanup on process exit
85
+ - Works with `chromium`, `firefox`, `webkit`
86
+ - Works with `playwright` and `playwright-core`
87
+
88
+ ## API
89
+
90
+ | Export | Description |
91
+ |---|---|
92
+ | `apply(opts?)` | Re-apply patch with options |
93
+ | `cleanup()` | Stop all HTTP servers |
94
+ | `convertUrl(url)` | Convert a file:// URL manually |
95
+ | `serveAndGoto(page, path)` | Navigate without monkey-patching |
96
+ | `buildDataUrl(path)` | Build a data: URL from HTML file |
97
+ | `inlineAssets(path)` | Inline assets into HTML string |
98
+
99
+ ## License
100
+
101
+ MIT
package/helper.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Manual helper — no monkey-patching, explicit control.
3
+ *
4
+ * const { serveAndGoto, cleanup } = require("playwright-deblock/helper");
5
+ *
6
+ * test("my test", async ({ page }) => {
7
+ * await serveAndGoto(page, "/path/to/file.html");
8
+ * });
9
+ *
10
+ * test.afterAll(() => cleanup());
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const { getServer, stopAll } = require("./lib/server");
16
+ const { buildDataUrl } = require("./lib/inline");
17
+
18
+ /**
19
+ * Navigate a Playwright page to a local HTML file via allowed protocol.
20
+ *
21
+ * @param {import("playwright").Page} page
22
+ * @param {string} htmlPath — absolute or relative path to the HTML file
23
+ * @param {object} [opts]
24
+ * @param {"auto"|"http"|"data"} [opts.strategy="auto"]
25
+ * @param {number} [opts.timeout=30000]
26
+ * @param {string} [opts.waitUntil="networkidle"]
27
+ * @returns {Promise<string>} the URL used
28
+ */
29
+ async function serveAndGoto(page, htmlPath, opts = {}) {
30
+ const {
31
+ strategy = "auto",
32
+ timeout = 30000,
33
+ waitUntil = "networkidle",
34
+ } = opts;
35
+
36
+ const resolved = path.resolve(htmlPath);
37
+ if (!fs.existsSync(resolved)) {
38
+ throw new Error(`[playwright-deblock] File not found: ${resolved}`);
39
+ }
40
+
41
+ const rootDir = path.dirname(resolved);
42
+ const fileName = path.basename(resolved);
43
+
44
+ // Try HTTP
45
+ if (strategy === "http" || strategy === "auto") {
46
+ try {
47
+ const { port } = await getServer(rootDir);
48
+ const url = `http://127.0.0.1:${port}/${fileName}`;
49
+ await page.goto(url, { waitUntil, timeout });
50
+ return url;
51
+ } catch (e) {
52
+ if (strategy === "http") throw e;
53
+ }
54
+ }
55
+
56
+ // data: URL
57
+ const dataUrl = buildDataUrl(resolved);
58
+ await page.goto(dataUrl, { waitUntil: "load", timeout });
59
+ return "data:text/html;...";
60
+ }
61
+
62
+ module.exports = {
63
+ serveAndGoto,
64
+ cleanup: stopAll,
65
+ };
package/index.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { Page } from "playwright";
2
+
3
+ interface DeblockOptions {
4
+ /** Which strategy to use. Default: "auto" (http first, data: fallback) */
5
+ strategy?: "auto" | "http" | "data";
6
+ /** Suppress console output. Default: false */
7
+ silent?: boolean;
8
+ }
9
+
10
+ interface ServeOptions {
11
+ /** Which strategy to use. Default: "auto" */
12
+ strategy?: "auto" | "http" | "data";
13
+ /** Navigation timeout in ms. Default: 30000 */
14
+ timeout?: number;
15
+ /** Playwright waitUntil value. Default: "networkidle" */
16
+ waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
17
+ }
18
+
19
+ /** Re-apply the patch with different options */
20
+ export function apply(opts?: DeblockOptions): void;
21
+
22
+ /** Stop all internal HTTP servers */
23
+ export function cleanup(): void;
24
+
25
+ /** Number of currently active servers */
26
+ export function serverCount(): number;
27
+
28
+ /** Convert a file:// URL to an allowed URL */
29
+ export function convertUrl(url: string, opts?: DeblockOptions): Promise<string>;
30
+
31
+ /** Parse a file:// URL to an absolute file path, or null */
32
+ export function parseFileUrl(url: string): string | null;
33
+
34
+ /** Read HTML and inline all local CSS/JS/images */
35
+ export function inlineAssets(htmlPath: string): string;
36
+
37
+ /** Build a data: URL from an HTML file with inlined assets */
38
+ export function buildDataUrl(htmlPath: string): string;
39
+
40
+ /** Navigate a page to a local HTML file via allowed protocol */
41
+ export function serveAndGoto(page: Page, htmlPath: string, opts?: ServeOptions): Promise<string>;
package/index.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * playwright-deblock
3
+ *
4
+ * Automatic fix for Playwright file:// navigation blocks.
5
+ *
6
+ * Usage:
7
+ *
8
+ * // Option A: Auto-patch (recommended)
9
+ * require("playwright-deblock");
10
+ * // or:
11
+ * require("playwright-deblock/register");
12
+ *
13
+ * // Option B: With options
14
+ * const deblock = require("playwright-deblock");
15
+ * deblock.apply({ strategy: "http", silent: true });
16
+ *
17
+ * // Option C: Manual helper (no monkey-patching)
18
+ * const { serveAndGoto, cleanup } = require("playwright-deblock/helper");
19
+ * await serveAndGoto(page, "/path/to/file.html");
20
+ *
21
+ * After require(), all page.goto("file://...") calls are automatically
22
+ * converted to http://127.0.0.1:<port>/... (or data: URL as fallback).
23
+ */
24
+
25
+ const { apply } = require("./lib/patch");
26
+ const { stopAll, count } = require("./lib/server");
27
+ const { convertUrl, parseFileUrl } = require("./lib/convert");
28
+ const { inlineAssets, buildDataUrl } = require("./lib/inline");
29
+
30
+ // Auto-apply on require()
31
+ apply();
32
+
33
+ module.exports = {
34
+ /** Re-apply the patch with different options */
35
+ apply,
36
+
37
+ /** Stop all internal HTTP servers */
38
+ cleanup: stopAll,
39
+
40
+ /** Number of active servers */
41
+ serverCount: count,
42
+
43
+ /** Convert a file:// URL to http:// or data: */
44
+ convertUrl,
45
+
46
+ /** Parse a file:// URL to an absolute path */
47
+ parseFileUrl,
48
+
49
+ /** Inline assets into HTML string */
50
+ inlineAssets,
51
+
52
+ /** Build a data: URL from an HTML file */
53
+ buildDataUrl,
54
+ };
package/lib/convert.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Convert file:// URLs to http:// or data: URLs.
3
+ */
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { getServer } = require("./server");
8
+ const { buildDataUrl } = require("./inline");
9
+
10
+ /**
11
+ * Parse a file:// URL to an absolute file path. Returns null if not file://.
12
+ */
13
+ function parseFileUrl(urlStr) {
14
+ try {
15
+ const u = new URL(urlStr);
16
+ if (u.protocol !== "file:") return null;
17
+ let fp = decodeURIComponent(u.pathname);
18
+ // Windows: /C:/path → C:/path
19
+ if (/^\/[A-Za-z]:\//.test(fp)) fp = fp.slice(1);
20
+ return path.resolve(fp);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Convert a URL: if it's file://, reroute through localhost or data:.
28
+ * Non-file URLs pass through unchanged.
29
+ *
30
+ * @param {string} url
31
+ * @param {object} [opts]
32
+ * @param {"auto"|"http"|"data"} [opts.strategy="auto"]
33
+ * @param {boolean} [opts.silent=false]
34
+ * @returns {Promise<string>}
35
+ */
36
+ async function convertUrl(url, opts = {}) {
37
+ const { strategy = "auto", silent = false } = opts;
38
+ const filePath = parseFileUrl(url);
39
+ if (!filePath) return url;
40
+
41
+ if (!fs.existsSync(filePath)) {
42
+ throw new Error(`[playwright-deblock] File not found: ${filePath} (from ${url})`);
43
+ }
44
+
45
+ const rootDir = path.dirname(filePath);
46
+ const fileName = path.basename(filePath);
47
+
48
+ // HTTP strategy
49
+ if (strategy === "http" || strategy === "auto") {
50
+ try {
51
+ const { port } = await getServer(rootDir);
52
+ const httpUrl = `http://127.0.0.1:${port}/${fileName}`;
53
+ if (!silent) console.log(`[playwright-deblock] ${url} → ${httpUrl}`);
54
+ return httpUrl;
55
+ } catch (e) {
56
+ if (strategy === "http") throw e;
57
+ if (!silent) console.warn(`[playwright-deblock] HTTP failed (${e.message}), using data: URL`);
58
+ }
59
+ }
60
+
61
+ // data: URL strategy
62
+ const dataUrl = buildDataUrl(filePath);
63
+ if (!silent) console.log(`[playwright-deblock] ${url} → data: URL (${(dataUrl.length / 1024).toFixed(1)} KB)`);
64
+ return dataUrl;
65
+ }
66
+
67
+ module.exports = { parseFileUrl, convertUrl };
package/lib/inline.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Inline external assets into HTML for data: URL loading.
3
+ * CSS → <style>, JS → <script>, images → base64 data URIs.
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const MIME = require("./mime");
9
+
10
+ const MAX_IMG_SIZE = 2 * 1024 * 1024; // 2 MB
11
+
12
+ function _tryRead(filePath, encoding) {
13
+ try {
14
+ return encoding ? fs.readFileSync(filePath, encoding) : fs.readFileSync(filePath);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Read an HTML file and inline all local CSS, JS, and images.
22
+ * Returns the modified HTML string.
23
+ */
24
+ function inlineAssets(htmlPath) {
25
+ const dir = path.dirname(path.resolve(htmlPath));
26
+ let html = fs.readFileSync(htmlPath, "utf-8");
27
+
28
+ // Inline <link rel="stylesheet" href="..."> (both attribute orders)
29
+ const cssPattern =
30
+ /<link\s+(?=[^>]*rel\s*=\s*["']stylesheet["'])(?=[^>]*href\s*=\s*["']([^"']+)["'])[^>]*\/?>/gi;
31
+ html = html.replace(cssPattern, (match, href) => {
32
+ if (/^(https?:|data:|\/\/)/i.test(href)) return match;
33
+ const css = _tryRead(path.resolve(dir, href), "utf-8");
34
+ return css !== null ? `<style>/* ${href} */${css}</style>` : match;
35
+ });
36
+
37
+ // Catch remaining link+stylesheet combos the first regex missed
38
+ html = html.replace(
39
+ /<link\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*stylesheet[^>]*\/?>/gi,
40
+ (match, href) => {
41
+ if (/^(https?:|data:|\/\/)/i.test(href)) return match;
42
+ if (match.includes("<style>")) return match; // already inlined
43
+ const css = _tryRead(path.resolve(dir, href), "utf-8");
44
+ return css !== null ? `<style>/* ${href} */${css}</style>` : match;
45
+ }
46
+ );
47
+ html = html.replace(
48
+ /<link\s+[^>]*stylesheet[^>]*href\s*=\s*["']([^"']+)["'][^>]*\/?>/gi,
49
+ (match, href) => {
50
+ if (/^(https?:|data:|\/\/)/i.test(href)) return match;
51
+ if (match.includes("<style>")) return match;
52
+ const css = _tryRead(path.resolve(dir, href), "utf-8");
53
+ return css !== null ? `<style>/* ${href} */${css}</style>` : match;
54
+ }
55
+ );
56
+
57
+ // Inline <script src="..."></script>
58
+ html = html.replace(
59
+ /<script\s+[^>]*src\s*=\s*["']([^"']+)["'][^>]*>\s*<\/script>/gi,
60
+ (match, src) => {
61
+ if (/^(https?:|data:|\/\/)/i.test(src)) return match;
62
+ const js = _tryRead(path.resolve(dir, src), "utf-8");
63
+ return js !== null ? `<script>/* ${src} */${js}</script>` : match;
64
+ }
65
+ );
66
+
67
+ // Inline images as base64 data URIs
68
+ html = html.replace(
69
+ /(<img\s+[^>]*src\s*=\s*["'])([^"']+)(["'][^>]*>)/gi,
70
+ (match, pre, src, suf) => {
71
+ if (/^(https?:|data:|about:|\/\/)/i.test(src)) return match;
72
+ const p = path.resolve(dir, src);
73
+ const buf = _tryRead(p);
74
+ if (!buf || buf.length > MAX_IMG_SIZE) return match;
75
+ const ext = path.extname(p).toLowerCase();
76
+ const mime = MIME[ext] || "application/octet-stream";
77
+ return `${pre}data:${mime};base64,${buf.toString("base64")}${suf}`;
78
+ }
79
+ );
80
+
81
+ return html;
82
+ }
83
+
84
+ /**
85
+ * Build a complete data: URL from an HTML file, with assets inlined.
86
+ */
87
+ function buildDataUrl(htmlPath) {
88
+ const html = inlineAssets(htmlPath);
89
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
90
+ }
91
+
92
+ module.exports = { inlineAssets, buildDataUrl };
package/lib/mime.js ADDED
@@ -0,0 +1,32 @@
1
+ /** MIME type map for the static file server */
2
+ module.exports = {
3
+ ".html": "text/html",
4
+ ".htm": "text/html",
5
+ ".css": "text/css",
6
+ ".js": "application/javascript",
7
+ ".mjs": "application/javascript",
8
+ ".json": "application/json",
9
+ ".png": "image/png",
10
+ ".jpg": "image/jpeg",
11
+ ".jpeg": "image/jpeg",
12
+ ".gif": "image/gif",
13
+ ".svg": "image/svg+xml",
14
+ ".ico": "image/x-icon",
15
+ ".webp": "image/webp",
16
+ ".avif": "image/avif",
17
+ ".woff": "font/woff",
18
+ ".woff2": "font/woff2",
19
+ ".ttf": "font/ttf",
20
+ ".otf": "font/otf",
21
+ ".eot": "application/vnd.ms-fontobject",
22
+ ".mp4": "video/mp4",
23
+ ".webm": "video/webm",
24
+ ".ogg": "audio/ogg",
25
+ ".mp3": "audio/mpeg",
26
+ ".wav": "audio/wav",
27
+ ".pdf": "application/pdf",
28
+ ".xml": "application/xml",
29
+ ".txt": "text/plain",
30
+ ".wasm": "application/wasm",
31
+ ".map": "application/json",
32
+ };
package/lib/patch.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Monkey-patch Playwright browser types so every page.goto()
3
+ * automatically converts file:// URLs.
4
+ */
5
+
6
+ const { convertUrl } = require("./convert");
7
+ const { stopAll } = require("./server");
8
+
9
+ let _opts = {};
10
+ let _patched = new WeakSet();
11
+
12
+ function _patchPage(page) {
13
+ if (_patched.has(page)) return;
14
+ _patched.add(page);
15
+
16
+ const origGoto = page.goto.bind(page);
17
+ page.goto = async function (url, options) {
18
+ const converted = await convertUrl(url, _opts);
19
+ return origGoto(converted, options);
20
+ };
21
+ }
22
+
23
+ function _patchContext(ctx) {
24
+ if (_patched.has(ctx)) return;
25
+ _patched.add(ctx);
26
+
27
+ const origNewPage = ctx.newPage.bind(ctx);
28
+ ctx.newPage = async function (...args) {
29
+ const page = await origNewPage(...args);
30
+ _patchPage(page);
31
+ return page;
32
+ };
33
+ }
34
+
35
+ function _patchBrowser(browser) {
36
+ if (_patched.has(browser)) return;
37
+ _patched.add(browser);
38
+
39
+ const origNewContext = browser.newContext.bind(browser);
40
+ browser.newContext = async function (...args) {
41
+ const ctx = await origNewContext(...args);
42
+ _patchContext(ctx);
43
+ return ctx;
44
+ };
45
+
46
+ const origNewPage = browser.newPage.bind(browser);
47
+ browser.newPage = async function (...args) {
48
+ const page = await origNewPage(...args);
49
+ _patchPage(page);
50
+ return page;
51
+ };
52
+ }
53
+
54
+ function _patchBrowserType(bt) {
55
+ if (_patched.has(bt)) return;
56
+ _patched.add(bt);
57
+
58
+ const origLaunch = bt.launch.bind(bt);
59
+ bt.launch = async function (...args) {
60
+ const browser = await origLaunch(...args);
61
+ _patchBrowser(browser);
62
+ return browser;
63
+ };
64
+
65
+ if (bt.launchPersistentContext) {
66
+ const origLPC = bt.launchPersistentContext.bind(bt);
67
+ bt.launchPersistentContext = async function (...args) {
68
+ const ctx = await origLPC(...args);
69
+ _patchContext(ctx);
70
+ return ctx;
71
+ };
72
+ }
73
+
74
+ if (bt.connect) {
75
+ const origConnect = bt.connect.bind(bt);
76
+ bt.connect = async function (...args) {
77
+ const browser = await origConnect(...args);
78
+ _patchBrowser(browser);
79
+ return browser;
80
+ };
81
+ }
82
+
83
+ if (bt.connectOverCDP) {
84
+ const origCDP = bt.connectOverCDP.bind(bt);
85
+ bt.connectOverCDP = async function (...args) {
86
+ const browser = await origCDP(...args);
87
+ _patchBrowser(browser);
88
+ return browser;
89
+ };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Apply the deblock patch to Playwright.
95
+ *
96
+ * @param {object} [opts]
97
+ * @param {"auto"|"http"|"data"} [opts.strategy="auto"]
98
+ * @param {boolean} [opts.silent=false]
99
+ */
100
+ function apply(opts = {}) {
101
+ _opts = opts;
102
+
103
+ // Try both playwright and playwright-core
104
+ const modules = [];
105
+ try { modules.push(require("playwright")); } catch {}
106
+ try { modules.push(require("playwright-core")); } catch {}
107
+
108
+ if (modules.length === 0) {
109
+ throw new Error(
110
+ "[playwright-deblock] Neither 'playwright' nor 'playwright-core' found. Install one first."
111
+ );
112
+ }
113
+
114
+ for (const pw of modules) {
115
+ for (const name of ["chromium", "firefox", "webkit"]) {
116
+ if (pw[name]) _patchBrowserType(pw[name]);
117
+ }
118
+ }
119
+
120
+ // Cleanup on exit
121
+ process.on("exit", stopAll);
122
+
123
+ if (!opts.silent) {
124
+ console.log("[playwright-deblock] Active — file:// URLs will be auto-redirected");
125
+ }
126
+ }
127
+
128
+ module.exports = { apply };
package/lib/server.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Zero-dependency static file server.
3
+ * One server per root directory, reused across calls.
4
+ */
5
+
6
+ const http = require("http");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const MIME = require("./mime");
10
+
11
+ const _servers = new Map();
12
+ const STARTUP_TIMEOUT = 5000;
13
+
14
+ function _createHandler(rootDir) {
15
+ const root = path.resolve(rootDir);
16
+ return (req, res) => {
17
+ const urlPath = decodeURIComponent(req.url.split("?")[0]);
18
+ let fp = path.join(root, urlPath);
19
+
20
+ // Block path traversal
21
+ if (!path.resolve(fp).startsWith(root)) {
22
+ res.writeHead(403);
23
+ return res.end("Forbidden");
24
+ }
25
+
26
+ if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) {
27
+ fp = path.join(fp, "index.html");
28
+ }
29
+
30
+ if (!fs.existsSync(fp)) {
31
+ res.writeHead(404);
32
+ return res.end("Not Found");
33
+ }
34
+
35
+ const ext = path.extname(fp).toLowerCase();
36
+ const buf = fs.readFileSync(fp);
37
+ res.writeHead(200, {
38
+ "Content-Type": MIME[ext] || "application/octet-stream",
39
+ "Content-Length": buf.length,
40
+ "Cache-Control": "no-store",
41
+ });
42
+ res.end(buf);
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Get or start a static server for the given root directory.
48
+ * Returns { server, port }.
49
+ */
50
+ async function getServer(rootDir) {
51
+ const key = path.resolve(rootDir);
52
+ if (_servers.has(key)) return _servers.get(key);
53
+
54
+ return new Promise((resolve, reject) => {
55
+ const srv = http.createServer(_createHandler(key));
56
+ const t = setTimeout(
57
+ () => reject(new Error(`Server start timeout for ${key}`)),
58
+ STARTUP_TIMEOUT
59
+ );
60
+
61
+ srv.on("error", (e) => {
62
+ clearTimeout(t);
63
+ reject(e);
64
+ });
65
+
66
+ srv.listen(0, "127.0.0.1", () => {
67
+ clearTimeout(t);
68
+ const info = { server: srv, port: srv.address().port };
69
+ _servers.set(key, info);
70
+ resolve(info);
71
+ });
72
+ });
73
+ }
74
+
75
+ /** Stop all running servers */
76
+ function stopAll() {
77
+ for (const [, { server }] of _servers) {
78
+ try { server.close(); } catch {}
79
+ }
80
+ _servers.clear();
81
+ }
82
+
83
+ /** Number of active servers */
84
+ function count() {
85
+ return _servers.size;
86
+ }
87
+
88
+ module.exports = { getServer, stopAll, count };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "playwright-deblock",
3
+ "version": "1.0.0",
4
+ "description": "Automatic fix for Playwright file:// navigation blocks. One require(), works forever.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./register": "./register.js",
10
+ "./helper": "./helper.js"
11
+ },
12
+ "keywords": [
13
+ "playwright",
14
+ "file-protocol",
15
+ "deblock",
16
+ "localhost",
17
+ "data-url",
18
+ "navigation",
19
+ "fix",
20
+ "patch",
21
+ "ci",
22
+ "sandbox"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "playwright": "^1.58.2",
28
+ "playwright-core": ">=1.20.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "playwright": {
32
+ "optional": true
33
+ },
34
+ "playwright-core": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "files": [
39
+ "index.js",
40
+ "index.d.ts",
41
+ "register.js",
42
+ "helper.js",
43
+ "lib/",
44
+ "LICENSE",
45
+ "README.md"
46
+ ],
47
+ "scripts": {
48
+ "test": "node test/run.js"
49
+ }
50
+ }
package/register.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Side-effect-only import. Just require this file to activate the patch.
3
+ *
4
+ * require("playwright-deblock/register");
5
+ *
6
+ * Useful for --require in Playwright config:
7
+ * npx playwright test --require playwright-deblock/register
8
+ */
9
+ require("./index");