@lownoise-studio/rendershield 1.0.0 → 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 +15 -0
- package/README.md +32 -14
- package/dist/cli.js +28 -23
- 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 +2 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +2 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/verify.d.ts +22 -8
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +158 -66
- 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/generateWorker.js +1 -1
- 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 +3 -1
- package/dist/core/loadConfig.d.ts.map +1 -1
- package/dist/core/loadConfig.js +45 -38
- package/dist/core/loadConfig.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/CONFIG.md +70 -0
- package/package.json +2 -1
- package/rendershield.config.schema.json +100 -0
- package/src/cli.ts +36 -26
- package/src/cliArgs.ts +71 -0
- package/src/commands/build.ts +3 -2
- package/src/commands/init.ts +8 -8
- package/src/commands/verify.ts +264 -79
- package/src/configPath.ts +17 -0
- package/src/core/generateWorker.ts +1 -1
- package/src/core/listOutputRoutes.ts +51 -0
- package/src/core/loadConfig.ts +59 -38
- package/src/index.ts +4 -1
- package/src/types.ts +1 -1
package/src/core/loadConfig.ts
CHANGED
|
@@ -2,8 +2,11 @@ import fs from "fs-extra";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RenderShieldConfig, SCHEMA_TYPES, type SchemaType } from "../types.js";
|
|
4
4
|
import { renderShieldError } from "../errors.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
resolveConfigFile,
|
|
7
|
+
DEFAULT_CONFIG_NAME,
|
|
8
|
+
type CommandOptions,
|
|
9
|
+
} from "../configPath.js";
|
|
7
10
|
|
|
8
11
|
const DEFAULT_SITEMAP_PATH = "/sitemap.xml";
|
|
9
12
|
const DEFAULT_ROBOTS_PATH = "/robots.txt";
|
|
@@ -30,10 +33,8 @@ function coerceBoolFlag(
|
|
|
30
33
|
key: "sitemap" | "robots" | "worker",
|
|
31
34
|
defaultEnabled: boolean
|
|
32
35
|
): BoolFlag {
|
|
33
|
-
// If missing, provide defaults (keeps older configs from exploding)
|
|
34
36
|
if (parsed?.[key] == null) return { enabled: defaultEnabled };
|
|
35
37
|
|
|
36
|
-
// If present, validate shape
|
|
37
38
|
const v = parsed[key];
|
|
38
39
|
if (!isObject(v)) {
|
|
39
40
|
throw renderShieldError(
|
|
@@ -51,40 +52,44 @@ function coerceBoolFlag(
|
|
|
51
52
|
return { enabled };
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function readSpaOrigin(worker: Record<string, unknown>): string | undefined {
|
|
56
|
+
const spa = worker.spaOrigin ?? worker.lovableOrigin;
|
|
57
|
+
return typeof spa === "string" && spa.trim() !== "" ? spa.trim() : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
|
-
* Worker config policy: strict mode.
|
|
56
|
-
*
|
|
57
|
-
* botUserAgentPatterns are required and must be non-empty. No defaults are
|
|
58
|
-
* supplied — build fails with a crisp error.
|
|
59
|
-
* lovableOrigin: must be http: or https: only (no file:, javascript:, protocol-relative).
|
|
60
|
-
* botUserAgentPatterns: substring match only (not regex); empty strings rejected to avoid overmatching.
|
|
61
|
+
* Worker config policy: strict mode when enabled.
|
|
62
|
+
* spaOrigin (or deprecated lovableOrigin) must be http(s).
|
|
61
63
|
*/
|
|
62
64
|
function validateWorkerWhenEnabled(parsed: Record<string, unknown>): void {
|
|
63
65
|
const worker = parsed.worker as Record<string, unknown> | undefined;
|
|
64
|
-
|
|
66
|
+
const spaOrigin = worker ? readSpaOrigin(worker) : undefined;
|
|
67
|
+
|
|
68
|
+
if (!spaOrigin) {
|
|
65
69
|
throw renderShieldError(
|
|
66
70
|
"CONFIG_INVALID",
|
|
67
|
-
`worker.
|
|
71
|
+
`worker.spaOrigin is required when worker.enabled is true. Example: "https://app.example.com" (lovableOrigin is accepted as a deprecated alias).`
|
|
68
72
|
);
|
|
69
73
|
}
|
|
74
|
+
|
|
70
75
|
let url: URL;
|
|
71
76
|
try {
|
|
72
|
-
url = new URL(
|
|
77
|
+
url = new URL(spaOrigin);
|
|
73
78
|
} catch {
|
|
74
79
|
throw renderShieldError(
|
|
75
80
|
"CONFIG_INVALID",
|
|
76
|
-
`worker.
|
|
81
|
+
`worker.spaOrigin must be a valid URL. Got: "${spaOrigin}"`
|
|
77
82
|
);
|
|
78
83
|
}
|
|
79
84
|
const scheme = url.protocol.toLowerCase();
|
|
80
85
|
if (scheme !== "http:" && scheme !== "https:") {
|
|
81
86
|
throw renderShieldError(
|
|
82
87
|
"CONFIG_INVALID",
|
|
83
|
-
`worker.
|
|
88
|
+
`worker.spaOrigin must use http or https. Got: "${url.protocol}"`
|
|
84
89
|
);
|
|
85
90
|
}
|
|
86
91
|
if (
|
|
87
|
-
!Array.isArray(worker
|
|
92
|
+
!Array.isArray(worker?.rewriteRouteBases) ||
|
|
88
93
|
worker.rewriteRouteBases.length === 0
|
|
89
94
|
) {
|
|
90
95
|
throw renderShieldError(
|
|
@@ -93,7 +98,7 @@ function validateWorkerWhenEnabled(parsed: Record<string, unknown>): void {
|
|
|
93
98
|
);
|
|
94
99
|
}
|
|
95
100
|
if (
|
|
96
|
-
!Array.isArray(worker
|
|
101
|
+
!Array.isArray(worker?.botUserAgentPatterns) ||
|
|
97
102
|
worker.botUserAgentPatterns.length === 0
|
|
98
103
|
) {
|
|
99
104
|
throw renderShieldError(
|
|
@@ -101,7 +106,6 @@ function validateWorkerWhenEnabled(parsed: Record<string, unknown>): void {
|
|
|
101
106
|
`worker.botUserAgentPatterns must be a non-empty array when worker.enabled is true. Example: ["googlebot", "bingbot"]`
|
|
102
107
|
);
|
|
103
108
|
}
|
|
104
|
-
// Substring match only; empty string would match every User-Agent
|
|
105
109
|
for (let i = 0; i < worker.botUserAgentPatterns.length; i++) {
|
|
106
110
|
const p = worker.botUserAgentPatterns[i];
|
|
107
111
|
if (typeof p !== "string" || p.trim() === "") {
|
|
@@ -113,27 +117,54 @@ function validateWorkerWhenEnabled(parsed: Record<string, unknown>): void {
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
function normalizeWorker(
|
|
121
|
+
parsed: Record<string, unknown>,
|
|
122
|
+
enabled: boolean
|
|
123
|
+
): void {
|
|
124
|
+
if (!enabled) {
|
|
125
|
+
parsed.worker = {
|
|
126
|
+
enabled: false,
|
|
127
|
+
spaOrigin: "",
|
|
128
|
+
rewriteRouteBases: [],
|
|
129
|
+
botUserAgentPatterns: [],
|
|
130
|
+
debugHeaders: false,
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
validateWorkerWhenEnabled(parsed);
|
|
136
|
+
const worker = parsed.worker as Record<string, unknown>;
|
|
137
|
+
parsed.worker = {
|
|
138
|
+
enabled: true,
|
|
139
|
+
spaOrigin: readSpaOrigin(worker) as string,
|
|
140
|
+
rewriteRouteBases: worker.rewriteRouteBases,
|
|
141
|
+
botUserAgentPatterns: worker.botUserAgentPatterns,
|
|
142
|
+
debugHeaders: worker.debugHeaders === true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
export async function loadConfig(
|
|
117
|
-
cwd = process.cwd()
|
|
147
|
+
cwd = process.cwd(),
|
|
148
|
+
options?: CommandOptions
|
|
118
149
|
): Promise<RenderShieldConfig> {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
150
|
+
const configFile = resolveConfigFile(cwd, options?.configPath);
|
|
151
|
+
const configName = path.basename(configFile);
|
|
152
|
+
const exists = await fs.pathExists(configFile);
|
|
121
153
|
if (!exists) {
|
|
122
154
|
throw renderShieldError(
|
|
123
155
|
"CONFIG_MISSING",
|
|
124
|
-
`Missing ${
|
|
156
|
+
`Missing ${configName}. Run: rendershield init`
|
|
125
157
|
);
|
|
126
158
|
}
|
|
127
159
|
|
|
128
|
-
const raw = await fs.readFile(
|
|
160
|
+
const raw = await fs.readFile(configFile, "utf8");
|
|
129
161
|
let parsed: ParsedInput & Record<string, unknown>;
|
|
130
162
|
try {
|
|
131
163
|
parsed = JSON.parse(raw) as ParsedInput & Record<string, unknown>;
|
|
132
164
|
} catch {
|
|
133
|
-
throw renderShieldError("CONFIG_INVALID", `${
|
|
165
|
+
throw renderShieldError("CONFIG_INVALID", `${configName} is not valid JSON`);
|
|
134
166
|
}
|
|
135
167
|
|
|
136
|
-
// Minimal validation (v0)
|
|
137
168
|
if (parsed?.version !== 1) {
|
|
138
169
|
throw renderShieldError("CONFIG_INVALID", "Config version must be 1");
|
|
139
170
|
}
|
|
@@ -169,7 +200,6 @@ export async function loadConfig(
|
|
|
169
200
|
parsed.content.markdown.collections
|
|
170
201
|
);
|
|
171
202
|
|
|
172
|
-
// Validate + default these optional sections (preserve path so it is not lost)
|
|
173
203
|
const sitemapFlag = coerceBoolFlag(parsed, "sitemap", true);
|
|
174
204
|
const sitemapObj = parsed.sitemap as Record<string, unknown> | undefined;
|
|
175
205
|
const sitemapPathVal = sitemapObj?.path;
|
|
@@ -189,22 +219,13 @@ export async function loadConfig(
|
|
|
189
219
|
parsed.robots = { enabled: robotsFlag.enabled, path: robotsPath };
|
|
190
220
|
|
|
191
221
|
const workerFlag = coerceBoolFlag(parsed, "worker", true);
|
|
192
|
-
|
|
193
|
-
validateWorkerWhenEnabled(parsed);
|
|
194
|
-
// Keep parsed.worker as the full object from JSON (already validated)
|
|
195
|
-
} else {
|
|
196
|
-
parsed.worker = {
|
|
197
|
-
enabled: false,
|
|
198
|
-
lovableOrigin: "",
|
|
199
|
-
rewriteRouteBases: [],
|
|
200
|
-
botUserAgentPatterns: [],
|
|
201
|
-
debugHeaders: false,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
222
|
+
normalizeWorker(parsed, workerFlag.enabled);
|
|
204
223
|
|
|
205
224
|
return parsed as RenderShieldConfig;
|
|
206
225
|
}
|
|
207
226
|
|
|
227
|
+
export { DEFAULT_CONFIG_NAME };
|
|
228
|
+
|
|
208
229
|
function normalizeCollections(collections: unknown[]): RenderShieldConfig["content"]["markdown"]["collections"] {
|
|
209
230
|
return collections.map((raw, index) => {
|
|
210
231
|
if (!isObject(raw)) {
|
package/src/index.ts
CHANGED
|
@@ -9,10 +9,11 @@ export { cmdInit } from "./commands/init.js";
|
|
|
9
9
|
export { cmdBuild } from "./commands/build.js";
|
|
10
10
|
export {
|
|
11
11
|
cmdVerify,
|
|
12
|
-
type
|
|
12
|
+
type VerifyOptions,
|
|
13
13
|
type VerifyLocalResult,
|
|
14
14
|
type VerifyProdResult,
|
|
15
15
|
type VerifyResult,
|
|
16
|
+
type VerifyPageResult,
|
|
16
17
|
} from "./commands/verify.js";
|
|
17
18
|
|
|
18
19
|
export { loadConfig } from "./core/loadConfig.js";
|
|
@@ -28,6 +29,8 @@ export { generateSitemapXml } from "./core/generateSitemap.js";
|
|
|
28
29
|
export { generateRobotsTxt } from "./core/generateRobots.js";
|
|
29
30
|
export { generateWorkerJs } from "./core/generateWorker.js";
|
|
30
31
|
|
|
32
|
+
export type { CommandOptions } from "./configPath.js";
|
|
33
|
+
export { DEFAULT_CONFIG_NAME, resolveConfigFile } from "./configPath.js";
|
|
31
34
|
export type { RenderShieldConfig, MarkdownDoc, SchemaType } from "./types.js";
|
|
32
35
|
export { SCHEMA_TYPES } from "./types.js";
|
|
33
36
|
|
package/src/types.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type RenderShieldConfig = {
|
|
|
34
34
|
};
|
|
35
35
|
worker: {
|
|
36
36
|
enabled: boolean;
|
|
37
|
-
|
|
37
|
+
spaOrigin: string; // https://app.example.com — human SPA / hosting origin
|
|
38
38
|
rewriteRouteBases: string[]; // ["/blog/"]
|
|
39
39
|
botUserAgentPatterns: string[];
|
|
40
40
|
debugHeaders: boolean;
|