@lownoise-studio/rendershield 0.3.1 → 1.1.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/CHANGELOG.md +73 -32
- package/CONTRIBUTING.md +41 -0
- package/README.md +227 -144
- package/SECURITY.md +25 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -26
- package/dist/cli.js.map +1 -1
- package/dist/cliArgs.d.ts +8 -0
- package/dist/cliArgs.d.ts.map +1 -0
- package/dist/cliArgs.js +56 -0
- package/dist/cliArgs.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +12 -11
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +8 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/verify.d.ts +33 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +202 -117
- package/dist/commands/verify.js.map +1 -1
- package/dist/configPath.d.ts +7 -0
- package/dist/configPath.d.ts.map +1 -0
- package/dist/configPath.js +8 -0
- package/dist/configPath.js.map +1 -0
- package/dist/core/generateRobots.d.ts +3 -0
- package/dist/core/generateRobots.d.ts.map +1 -0
- package/dist/core/generateSitemap.d.ts +3 -0
- package/dist/core/generateSitemap.d.ts.map +1 -0
- package/dist/core/generateWorker.d.ts +3 -0
- package/dist/core/generateWorker.d.ts.map +1 -0
- package/dist/core/generateWorker.js +75 -75
- package/dist/core/generateWorker.js.map +1 -1
- package/dist/core/listOutputRoutes.d.ts +4 -0
- package/dist/core/listOutputRoutes.d.ts.map +1 -0
- package/dist/core/listOutputRoutes.js +40 -0
- package/dist/core/listOutputRoutes.js.map +1 -0
- package/dist/core/loadConfig.d.ts +5 -0
- package/dist/core/loadConfig.d.ts.map +1 -0
- package/dist/core/loadConfig.js +108 -58
- package/dist/core/loadConfig.js.map +1 -1
- package/dist/core/loadMarkdown.d.ts +3 -0
- package/dist/core/loadMarkdown.d.ts.map +1 -0
- package/dist/core/loadMarkdown.js +4 -3
- package/dist/core/loadMarkdown.js.map +1 -1
- package/dist/core/renderHtml.d.ts +3 -0
- package/dist/core/renderHtml.d.ts.map +1 -0
- package/dist/core/renderHtml.js +25 -10
- package/dist/core/renderHtml.js.map +1 -1
- package/dist/core/validateOutput.d.ts +28 -0
- package/dist/core/validateOutput.d.ts.map +1 -0
- package/dist/core/validateOutput.js +7 -1
- package/dist/core/validateOutput.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/docs/CONFIG.md +70 -0
- package/docs/deploy-cloudflare.md +40 -14
- package/package.json +25 -2
- package/rendershield.config.schema.json +100 -0
- package/src/cli.ts +96 -75
- package/src/cliArgs.ts +71 -0
- package/src/commands/build.ts +200 -185
- package/src/commands/init.ts +8 -8
- package/src/commands/verify.ts +451 -236
- package/src/configPath.ts +17 -0
- package/src/core/generateWorker.ts +97 -97
- package/src/core/listOutputRoutes.ts +51 -0
- package/src/core/loadConfig.ts +282 -173
- package/src/core/loadMarkdown.ts +9 -3
- package/src/core/renderHtml.ts +36 -12
- package/src/core/validateOutput.ts +335 -328
- package/src/errors.ts +48 -0
- package/src/index.ts +43 -0
- package/src/types.ts +5 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CONFIG_NAME = "rendershield.config.json";
|
|
4
|
+
|
|
5
|
+
export type CommandOptions = {
|
|
6
|
+
/** Relative to cwd, or absolute. Defaults to rendershield.config.json in cwd. */
|
|
7
|
+
configPath?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveConfigFile(
|
|
11
|
+
cwd: string,
|
|
12
|
+
configPath = DEFAULT_CONFIG_NAME
|
|
13
|
+
): string {
|
|
14
|
+
return path.isAbsolute(configPath)
|
|
15
|
+
? configPath
|
|
16
|
+
: path.join(cwd, configPath);
|
|
17
|
+
}
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { RenderShieldConfig } from "../types.js";
|
|
3
|
-
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
-
let VERSION = "0.0.0";
|
|
6
|
-
try {
|
|
7
|
-
const pkg = require("../../package.json") as { version?: string };
|
|
8
|
-
VERSION = pkg.version ?? "0.0.0";
|
|
9
|
-
} catch {
|
|
10
|
-
VERSION = "unknown";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function jsStringArray(arr: string[]): string {
|
|
14
|
-
return `[${arr.map((s) => JSON.stringify(s)).join(", ")}]`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function generateWorkerJs(cfg: RenderShieldConfig): string {
|
|
18
|
-
const patterns = cfg.worker.botUserAgentPatterns.map((p) => p.toLowerCase());
|
|
19
|
-
const rewriteBases = cfg.worker.rewriteRouteBases;
|
|
20
|
-
|
|
21
|
-
return `/**
|
|
22
|
-
* RenderShield Worker (generated) v${VERSION}
|
|
23
|
-
* Serves prerendered /index.html to bots on selected route bases.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const BOT_SUBSTRINGS = ${jsStringArray(patterns)};
|
|
27
|
-
const REWRITE_BASES = ${jsStringArray(rewriteBases)};
|
|
28
|
-
|
|
29
|
-
function isBot(ua) {
|
|
30
|
-
const s = (ua || "").toLowerCase();
|
|
31
|
-
return BOT_SUBSTRINGS.some((sub) => s.includes(sub));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function shouldRewrite(pathname) {
|
|
35
|
-
return REWRITE_BASES.some((base) => pathname.startsWith(base));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function toIndexHtml(pathname) {
|
|
39
|
-
let p = pathname;
|
|
40
|
-
if (!p.endsWith("/")) p += "/";
|
|
41
|
-
return p + "index.html";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default {
|
|
45
|
-
async fetch(request, env, ctx) {
|
|
46
|
-
const url = new URL(request.url);
|
|
47
|
-
const ua = request.headers.get("User-Agent") || "";
|
|
48
|
-
const bot = isBot(ua);
|
|
49
|
-
|
|
50
|
-
const isGetLike = request.method === "GET" || request.method === "HEAD";
|
|
51
|
-
const rewrite = bot && isGetLike && shouldRewrite(url.pathname);
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
if (!rewrite) {
|
|
55
|
-
const passResp = await fetch(request);
|
|
56
|
-
const out = new Response(passResp.body, passResp);
|
|
57
|
-
out.headers.set("x-rendershield", "pass-through");
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const origin = ${JSON.stringify(cfg.worker.
|
|
62
|
-
const finalPath = toIndexHtml(url.pathname);
|
|
63
|
-
const originUrl = origin + finalPath + url.search;
|
|
64
|
-
|
|
65
|
-
const headers = new Headers();
|
|
66
|
-
headers.set("Accept", "text/html");
|
|
67
|
-
headers.set("User-Agent", ua);
|
|
68
|
-
|
|
69
|
-
const resp = await fetch(originUrl, { method: "GET", headers });
|
|
70
|
-
|
|
71
|
-
if (!resp.ok) {
|
|
72
|
-
const fallbackUrl = origin + url.pathname + url.search;
|
|
73
|
-
const fb = await fetch(fallbackUrl, { method: "GET", headers });
|
|
74
|
-
const out = new Response(fb.body, fb);
|
|
75
|
-
out.headers.set("x-rendershield", "bot-fallback");
|
|
76
|
-
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
77
|
-
out.headers.set("X-Prerender-Fallback", "true");
|
|
78
|
-
out.headers.set("X-Requested-Path", url.pathname);` : ""}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const out = new Response(resp.body, resp);
|
|
83
|
-
out.headers.set("x-rendershield", "bot-hit");
|
|
84
|
-
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
85
|
-
out.headers.set("X-Prerender", "true");
|
|
86
|
-
out.headers.set("X-Final-Path", finalPath);` : ""}
|
|
87
|
-
return out;
|
|
88
|
-
} catch (err) {
|
|
89
|
-
return new Response("Service temporarily unavailable.", {
|
|
90
|
-
status: 500,
|
|
91
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
`;
|
|
97
|
-
}
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { RenderShieldConfig } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
let VERSION = "0.0.0";
|
|
6
|
+
try {
|
|
7
|
+
const pkg = require("../../package.json") as { version?: string };
|
|
8
|
+
VERSION = pkg.version ?? "0.0.0";
|
|
9
|
+
} catch {
|
|
10
|
+
VERSION = "unknown";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function jsStringArray(arr: string[]): string {
|
|
14
|
+
return `[${arr.map((s) => JSON.stringify(s)).join(", ")}]`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function generateWorkerJs(cfg: RenderShieldConfig): string {
|
|
18
|
+
const patterns = cfg.worker.botUserAgentPatterns.map((p) => p.toLowerCase());
|
|
19
|
+
const rewriteBases = cfg.worker.rewriteRouteBases;
|
|
20
|
+
|
|
21
|
+
return `/**
|
|
22
|
+
* RenderShield Worker (generated) v${VERSION}
|
|
23
|
+
* Serves prerendered /index.html to bots on selected route bases.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const BOT_SUBSTRINGS = ${jsStringArray(patterns)};
|
|
27
|
+
const REWRITE_BASES = ${jsStringArray(rewriteBases)};
|
|
28
|
+
|
|
29
|
+
function isBot(ua) {
|
|
30
|
+
const s = (ua || "").toLowerCase();
|
|
31
|
+
return BOT_SUBSTRINGS.some((sub) => s.includes(sub));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldRewrite(pathname) {
|
|
35
|
+
return REWRITE_BASES.some((base) => pathname.startsWith(base));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toIndexHtml(pathname) {
|
|
39
|
+
let p = pathname;
|
|
40
|
+
if (!p.endsWith("/")) p += "/";
|
|
41
|
+
return p + "index.html";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
async fetch(request, env, ctx) {
|
|
46
|
+
const url = new URL(request.url);
|
|
47
|
+
const ua = request.headers.get("User-Agent") || "";
|
|
48
|
+
const bot = isBot(ua);
|
|
49
|
+
|
|
50
|
+
const isGetLike = request.method === "GET" || request.method === "HEAD";
|
|
51
|
+
const rewrite = bot && isGetLike && shouldRewrite(url.pathname);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (!rewrite) {
|
|
55
|
+
const passResp = await fetch(request);
|
|
56
|
+
const out = new Response(passResp.body, passResp);
|
|
57
|
+
out.headers.set("x-rendershield", "pass-through");
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const origin = ${JSON.stringify(cfg.worker.spaOrigin)};
|
|
62
|
+
const finalPath = toIndexHtml(url.pathname);
|
|
63
|
+
const originUrl = origin + finalPath + url.search;
|
|
64
|
+
|
|
65
|
+
const headers = new Headers();
|
|
66
|
+
headers.set("Accept", "text/html");
|
|
67
|
+
headers.set("User-Agent", ua);
|
|
68
|
+
|
|
69
|
+
const resp = await fetch(originUrl, { method: "GET", headers });
|
|
70
|
+
|
|
71
|
+
if (!resp.ok) {
|
|
72
|
+
const fallbackUrl = origin + url.pathname + url.search;
|
|
73
|
+
const fb = await fetch(fallbackUrl, { method: "GET", headers });
|
|
74
|
+
const out = new Response(fb.body, fb);
|
|
75
|
+
out.headers.set("x-rendershield", "bot-fallback");
|
|
76
|
+
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
77
|
+
out.headers.set("X-Prerender-Fallback", "true");
|
|
78
|
+
out.headers.set("X-Requested-Path", url.pathname);` : ""}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const out = new Response(resp.body, resp);
|
|
83
|
+
out.headers.set("x-rendershield", "bot-hit");
|
|
84
|
+
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
85
|
+
out.headers.set("X-Prerender", "true");
|
|
86
|
+
out.headers.set("X-Final-Path", finalPath);` : ""}
|
|
87
|
+
return out;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return new Response("Service temporarily unavailable.", {
|
|
90
|
+
status: 500,
|
|
91
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/** All routed index.html files under outDir (excludes bare outDir/index.html). */
|
|
5
|
+
export async function listPrerenderIndexFiles(
|
|
6
|
+
outDirAbs: string
|
|
7
|
+
): Promise<string[]> {
|
|
8
|
+
const results: string[] = [];
|
|
9
|
+
const stack: string[] = [outDirAbs];
|
|
10
|
+
|
|
11
|
+
while (stack.length > 0) {
|
|
12
|
+
const current = stack.pop() as string;
|
|
13
|
+
|
|
14
|
+
let entries: fs.Dirent[];
|
|
15
|
+
try {
|
|
16
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
17
|
+
} catch {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const full = path.join(current, entry.name);
|
|
25
|
+
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
stack.push(full);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (entry.isFile() && entry.name.toLowerCase() === "index.html") {
|
|
32
|
+
const rel = path.relative(outDirAbs, full);
|
|
33
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
34
|
+
if (parts.length >= 2) results.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
results.sort((a, b) => a.localeCompare(b));
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function indexHtmlPathToRoute(
|
|
44
|
+
outDirAbs: string,
|
|
45
|
+
indexPathAbs: string
|
|
46
|
+
): string {
|
|
47
|
+
const rel = path.relative(outDirAbs, indexPathAbs);
|
|
48
|
+
const noFile = rel.replace(/index\.html$/i, "");
|
|
49
|
+
const normalized = noFile.split(path.sep).join("/").replace(/\/+$/, "");
|
|
50
|
+
return "/" + normalized.replace(/^\/+/, "");
|
|
51
|
+
}
|