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 +21 -0
- package/README.md +101 -0
- package/helper.js +65 -0
- package/index.d.ts +41 -0
- package/index.js +54 -0
- package/lib/convert.js +67 -0
- package/lib/inline.js +92 -0
- package/lib/mime.js +32 -0
- package/lib/patch.js +128 -0
- package/lib/server.js +88 -0
- package/package.json +50 -0
- package/register.js +9 -0
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");
|