@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +32 -14
  3. package/dist/cli.js +28 -23
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cliArgs.d.ts +8 -0
  6. package/dist/cliArgs.d.ts.map +1 -0
  7. package/dist/cliArgs.js +56 -0
  8. package/dist/cliArgs.js.map +1 -0
  9. package/dist/commands/build.d.ts +2 -1
  10. package/dist/commands/build.d.ts.map +1 -1
  11. package/dist/commands/build.js +2 -2
  12. package/dist/commands/build.js.map +1 -1
  13. package/dist/commands/init.d.ts +2 -1
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +8 -7
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/verify.d.ts +22 -8
  18. package/dist/commands/verify.d.ts.map +1 -1
  19. package/dist/commands/verify.js +158 -66
  20. package/dist/commands/verify.js.map +1 -1
  21. package/dist/configPath.d.ts +7 -0
  22. package/dist/configPath.d.ts.map +1 -0
  23. package/dist/configPath.js +8 -0
  24. package/dist/configPath.js.map +1 -0
  25. package/dist/core/generateWorker.js +1 -1
  26. package/dist/core/generateWorker.js.map +1 -1
  27. package/dist/core/listOutputRoutes.d.ts +4 -0
  28. package/dist/core/listOutputRoutes.d.ts.map +1 -0
  29. package/dist/core/listOutputRoutes.js +40 -0
  30. package/dist/core/listOutputRoutes.js.map +1 -0
  31. package/dist/core/loadConfig.d.ts +3 -1
  32. package/dist/core/loadConfig.d.ts.map +1 -1
  33. package/dist/core/loadConfig.js +45 -38
  34. package/dist/core/loadConfig.js.map +1 -1
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/types.d.ts +1 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/docs/CONFIG.md +70 -0
  42. package/package.json +2 -1
  43. package/rendershield.config.schema.json +100 -0
  44. package/src/cli.ts +36 -26
  45. package/src/cliArgs.ts +71 -0
  46. package/src/commands/build.ts +3 -2
  47. package/src/commands/init.ts +8 -8
  48. package/src/commands/verify.ts +264 -79
  49. package/src/configPath.ts +17 -0
  50. package/src/core/generateWorker.ts +1 -1
  51. package/src/core/listOutputRoutes.ts +51 -0
  52. package/src/core/loadConfig.ts +59 -38
  53. package/src/index.ts +4 -1
  54. package/src/types.ts +1 -1
@@ -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
- const CONFIG_NAME = "rendershield.config.json";
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
- * When worker.enabled is true, all of lovableOrigin, rewriteRouteBases, and
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
- if (!worker?.lovableOrigin || typeof worker.lovableOrigin !== "string") {
66
+ const spaOrigin = worker ? readSpaOrigin(worker) : undefined;
67
+
68
+ if (!spaOrigin) {
65
69
  throw renderShieldError(
66
70
  "CONFIG_INVALID",
67
- `worker.lovableOrigin is required when worker.enabled is true. Example: "https://your-site.lovable.app"`
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(worker.lovableOrigin);
77
+ url = new URL(spaOrigin);
73
78
  } catch {
74
79
  throw renderShieldError(
75
80
  "CONFIG_INVALID",
76
- `worker.lovableOrigin must be a valid URL. Got: "${worker.lovableOrigin}"`
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.lovableOrigin must use http or https. Got: "${url.protocol}"`
88
+ `worker.spaOrigin must use http or https. Got: "${url.protocol}"`
84
89
  );
85
90
  }
86
91
  if (
87
- !Array.isArray(worker.rewriteRouteBases) ||
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.botUserAgentPatterns) ||
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 p = path.join(cwd, CONFIG_NAME);
120
- const exists = await fs.pathExists(p);
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 ${CONFIG_NAME}. Run: rendershield init`
156
+ `Missing ${configName}. Run: rendershield init`
125
157
  );
126
158
  }
127
159
 
128
- const raw = await fs.readFile(p, "utf8");
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", `${CONFIG_NAME} is not valid JSON`);
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
- if (workerFlag.enabled) {
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 VerifyProdOptions,
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
- lovableOrigin: string; // https://YOUR_SITE.lovable.app
37
+ spaOrigin: string; // https://app.example.com — human SPA / hosting origin
38
38
  rewriteRouteBases: string[]; // ["/blog/"]
39
39
  botUserAgentPatterns: string[];
40
40
  debugHeaders: boolean;