@se-studio/site-check 2.6.3 → 2.7.1

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @se-studio/contentful-rest-api@1.12.0
9
+
10
+ ## 2.7.0
11
+
12
+ ### Minor Changes
13
+
14
+ - Add build route policy validation (`parseNextBuildRouteTable`, `validateBuildRoutePolicy`, `validate-build-routes` CLI) and SSG guardrail templates for marketing sites.
15
+
3
16
  ## 2.6.3
4
17
 
5
18
  ### Patch Changes
@@ -0,0 +1,39 @@
1
+ /** Next.js build route table symbols (app router). */
2
+ export type NextBuildRouteSymbol = 'static' | 'ssg' | 'dynamic';
3
+ export interface ParsedBuildRoute {
4
+ symbol: NextBuildRouteSymbol;
5
+ route: string;
6
+ }
7
+ /** Per-app policy committed beside smoke.cases.json. */
8
+ export interface BuildRoutePolicy {
9
+ /** Routes that may be ƒ (dynamic) — e.g. CMS index pages, preview, download handlers. */
10
+ allowedDynamic: string[];
11
+ /** Segment routes that must be ○ or ●, never ƒ. */
12
+ mustBeSsgOrStatic: string[];
13
+ }
14
+ export interface BuildRoutePolicyViolation {
15
+ kind: 'must_be_ssg_or_static' | 'unexpected_dynamic' | 'parse_failure';
16
+ route: string;
17
+ symbol: NextBuildRouteSymbol;
18
+ message: string;
19
+ }
20
+ export interface BuildRoutePolicyResult {
21
+ routes: ParsedBuildRoute[];
22
+ violations: BuildRoutePolicyViolation[];
23
+ }
24
+ /** Parse the Route (app) table from `next build` stdout. */
25
+ export declare function parseNextBuildRouteTable(buildOutput: string): ParsedBuildRoute[];
26
+ /** Validate parsed build routes against a committed policy file. */
27
+ export declare function validateBuildRoutePolicy(routes: ParsedBuildRoute[], policy: BuildRoutePolicy): BuildRoutePolicyResult;
28
+ export declare function formatBuildRoutePolicyReport(result: BuildRoutePolicyResult): string;
29
+ export declare function getBuildRoutePolicyExitCode(result: BuildRoutePolicyResult): number;
30
+ export interface LoadBuildRoutePolicyOptions {
31
+ cwd?: string;
32
+ }
33
+ /** Load committed route-build-policy.json from an app directory. Returns null if missing. */
34
+ export declare function loadBuildRoutePolicy(filename?: string, options?: LoadBuildRoutePolicyOptions): BuildRoutePolicy | null;
35
+ /** Parse and validate `next build` stdout against a policy file in cwd. */
36
+ export declare function validateBuildRoutesFromBuildOutput(buildOutput: string, options?: LoadBuildRoutePolicyOptions & {
37
+ policyFile?: string;
38
+ }): BuildRoutePolicyResult | null;
39
+ //# sourceMappingURL=build-route-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-route-policy.d.ts","sourceRoot":"","sources":["../../src/smoke-test/build-route-policy.ts"],"names":[],"mappings":"AAGA,sDAAsD;AACtD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,uBAAuB,GAAG,oBAAoB,GAAG,eAAe,CAAC;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,UAAU,EAAE,yBAAyB,EAAE,CAAC;CACzC;AAQD,4DAA4D;AAC5D,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAsChF;AAQD,oEAAoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,MAAM,EAAE,gBAAgB,GACvB,sBAAsB,CA0BxB;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAenF;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAElF;AAED,MAAM,WAAW,2BAA2B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,wBAAgB,oBAAoB,CAClC,QAAQ,SAA4B,EACpC,OAAO,GAAE,2BAAgC,GACxC,gBAAgB,GAAG,IAAI,CAezB;AAED,2EAA2E;AAC3E,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,2BAA2B,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,sBAAsB,GAAG,IAAI,CAoB/B"}
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const SYMBOL_MAP = {
4
+ '○': 'static',
5
+ '●': 'ssg',
6
+ ƒ: 'dynamic',
7
+ };
8
+ /** Parse the Route (app) table from `next build` stdout. */
9
+ export function parseNextBuildRouteTable(buildOutput) {
10
+ const routes = [];
11
+ let inRouteTable = false;
12
+ for (const line of buildOutput.split('\n')) {
13
+ if (line.includes('Route (app)')) {
14
+ inRouteTable = true;
15
+ continue;
16
+ }
17
+ if (!inRouteTable) {
18
+ continue;
19
+ }
20
+ if (line.includes('First Load JS shared by all') || line.includes('Middleware')) {
21
+ break;
22
+ }
23
+ const match = line.match(/^[┌├└]\s+([○●ƒ])\s+(\S+)/);
24
+ if (!match) {
25
+ continue;
26
+ }
27
+ const symbolChar = match[1];
28
+ const route = match[2];
29
+ if (!symbolChar || !route) {
30
+ continue;
31
+ }
32
+ const symbol = SYMBOL_MAP[symbolChar];
33
+ if (!symbol) {
34
+ continue;
35
+ }
36
+ routes.push({ symbol, route });
37
+ }
38
+ return routes;
39
+ }
40
+ function symbolLabel(symbol) {
41
+ if (symbol === 'static')
42
+ return '○ static';
43
+ if (symbol === 'ssg')
44
+ return '● SSG';
45
+ return 'ƒ dynamic';
46
+ }
47
+ /** Validate parsed build routes against a committed policy file. */
48
+ export function validateBuildRoutePolicy(routes, policy) {
49
+ const violations = [];
50
+ const allowedDynamic = new Set(policy.allowedDynamic);
51
+ const mustBeSsgOrStatic = new Set(policy.mustBeSsgOrStatic);
52
+ for (const { route, symbol } of routes) {
53
+ if (mustBeSsgOrStatic.has(route) && symbol === 'dynamic') {
54
+ violations.push({
55
+ kind: 'must_be_ssg_or_static',
56
+ route,
57
+ symbol,
58
+ message: `${route} must be SSG/static (● or ○) but build reported ${symbolLabel(symbol)}`,
59
+ });
60
+ }
61
+ if (symbol === 'dynamic' && !allowedDynamic.has(route) && !mustBeSsgOrStatic.has(route)) {
62
+ violations.push({
63
+ kind: 'unexpected_dynamic',
64
+ route,
65
+ symbol,
66
+ message: `${route} is ${symbolLabel(symbol)} but not listed in allowedDynamic — add to route-build-policy.json if intentional`,
67
+ });
68
+ }
69
+ }
70
+ return { routes, violations };
71
+ }
72
+ export function formatBuildRoutePolicyReport(result) {
73
+ const lines = [];
74
+ lines.push('Build route policy');
75
+ lines.push(` routes parsed: ${result.routes.length}`);
76
+ if (result.violations.length === 0) {
77
+ lines.push(' status: ok');
78
+ return lines.join('\n');
79
+ }
80
+ lines.push(` violations: ${result.violations.length}`);
81
+ for (const v of result.violations) {
82
+ lines.push(` - ${v.message}`);
83
+ }
84
+ return lines.join('\n');
85
+ }
86
+ export function getBuildRoutePolicyExitCode(result) {
87
+ return result.violations.length > 0 ? 1 : 0;
88
+ }
89
+ /** Load committed route-build-policy.json from an app directory. Returns null if missing. */
90
+ export function loadBuildRoutePolicy(filename = 'route-build-policy.json', options = {}) {
91
+ const cwd = options.cwd ?? process.cwd();
92
+ const filePath = path.join(cwd, filename);
93
+ if (!fs.existsSync(filePath)) {
94
+ return null;
95
+ }
96
+ const raw = fs.readFileSync(filePath, 'utf8');
97
+ const parsed = JSON.parse(raw);
98
+ if (!Array.isArray(parsed.allowedDynamic) || !Array.isArray(parsed.mustBeSsgOrStatic)) {
99
+ throw new Error(`${filePath} must include allowedDynamic and mustBeSsgOrStatic arrays`);
100
+ }
101
+ return parsed;
102
+ }
103
+ /** Parse and validate `next build` stdout against a policy file in cwd. */
104
+ export function validateBuildRoutesFromBuildOutput(buildOutput, options = {}) {
105
+ const policy = loadBuildRoutePolicy(options.policyFile, options);
106
+ if (!policy) {
107
+ return null;
108
+ }
109
+ const routes = parseNextBuildRouteTable(buildOutput);
110
+ const result = validateBuildRoutePolicy(routes, policy);
111
+ if (routes.length === 0) {
112
+ result.violations.push({
113
+ kind: 'parse_failure',
114
+ route: '',
115
+ symbol: 'dynamic',
116
+ message: 'No routes parsed from build output — capture next build stdout or check Route (app) table format',
117
+ });
118
+ }
119
+ return result;
120
+ }
121
+ //# sourceMappingURL=build-route-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-route-policy.js","sourceRoot":"","sources":["../../src/smoke-test/build-route-policy.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  export { createProtectedFetch, requireDeploymentBaseUrl, requirePreviewBaseUrl, requireProtectionBypass, requireSitemapProdUrl, resolveDeploymentBaseUrl, resolvePreviewBaseUrl, resolveProductionSiteUrl, resolveProtectionBypass, resolveSitemapProdUrl, VERCEL_PROTECTION_BYPASS_HEADER, } from '../vercel-env.js';
2
+ export type { BuildRoutePolicy, BuildRoutePolicyResult, BuildRoutePolicyViolation, NextBuildRouteSymbol, ParsedBuildRoute, } from './build-route-policy.js';
3
+ export { formatBuildRoutePolicyReport, getBuildRoutePolicyExitCode, loadBuildRoutePolicy, parseNextBuildRouteTable, validateBuildRoutePolicy, validateBuildRoutesFromBuildOutput, } from './build-route-policy.js';
2
4
  export type { CacheLogAuditMatch, CacheLogAuditOptions, CacheLogAuditResult, CacheLogAuditSeverity, } from './cache-log-audit.js';
3
5
  export { auditCacheLogs, formatCacheLogAuditReport, getCacheLogAuditExitCode, } from './cache-log-audit.js';
4
6
  export { isCmsIntegrityEnabled, loadSmokeConfigFile } from './config.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,YAAY,EACZ,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,aAAa,CAAC;AAC/F,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,EAC1B,sCAAsC,EACtC,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,+BAA+B,GAChC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,4BAA4B,EAC5B,iBAAiB,EACjB,gCAAgC,EAChC,yBAAyB,EACzB,uBAAuB,EACvB,kBAAkB,EAClB,0BAA0B,EAC1B,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,kCAAkC,GACnC,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,YAAY,EACZ,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,aAAa,CAAC;AAC/F,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,EAC1B,sCAAsC,EACtC,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,+BAA+B,GAChC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,4BAA4B,EAC5B,iBAAiB,EACjB,gCAAgC,EAChC,yBAAyB,EACzB,uBAAuB,EACvB,kBAAkB,EAClB,0BAA0B,EAC1B,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { createProtectedFetch, requireDeploymentBaseUrl, requirePreviewBaseUrl, requireProtectionBypass, requireSitemapProdUrl, resolveDeploymentBaseUrl, resolvePreviewBaseUrl, resolveProductionSiteUrl, resolveProtectionBypass, resolveSitemapProdUrl, VERCEL_PROTECTION_BYPASS_HEADER, } from '../vercel-env.js';
2
+ export { formatBuildRoutePolicyReport, getBuildRoutePolicyExitCode, loadBuildRoutePolicy, parseNextBuildRouteTable, validateBuildRoutePolicy, validateBuildRoutesFromBuildOutput, } from './build-route-policy.js';
2
3
  export { auditCacheLogs, formatCacheLogAuditReport, getCacheLogAuditExitCode, } from './cache-log-audit.js';
3
4
  export { isCmsIntegrityEnabled, loadSmokeConfigFile } from './config.js';
4
5
  export { formatSmokeTestReport, getSmokeTestExitCode, isSmokeTestCacheVerifyResult, parseNextJsCacheHeader, runSmokeTest, runSmokeTestWithCacheVerify, } from './runner.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,kBAAkB,CAAC;AAO1B,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,YAAY,EACZ,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,EAC1B,sCAAsC,EACtC,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,+BAA+B,GAChC,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/smoke-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,kCAAkC,GACnC,MAAM,yBAAyB,CAAC;AAOjC,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,YAAY,EACZ,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,EAC1B,sCAAsC,EACtC,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,+BAA+B,GAChC,MAAM,aAAa,CAAC"}
package/docs/llms.md CHANGED
@@ -14,7 +14,7 @@ Site validation CLI, production SEO audits, and curated smoke tests for SE marke
14
14
 
15
15
  ## Smoke test (HTTP)
16
16
 
17
- Apps commit `smoke.cases.json`. Requires **^2.0.0** for static smoke; **^2.6.1** when `cmsIntegrity` is enabled.
17
+ Apps commit `smoke.cases.json`. Requires **^2.0.0** for static smoke; **^2.6.1** when `cmsIntegrity` is enabled; **^2.7.0** for build route policy (`validate-build-routes`, `validateBuildRoutesFromBuildOutput`).
18
18
 
19
19
  ```ts
20
20
  import {
@@ -24,9 +24,13 @@ import {
24
24
  formatCombinedSmokeReport,
25
25
  getSmokeTestExitCode,
26
26
  getCombinedSmokeExitCode,
27
+ validateBuildRoutesFromBuildOutput,
28
+ formatBuildRoutePolicyReport,
27
29
  } from '@se-studio/site-check/smoke-test';
28
30
  ```
29
31
 
32
+ **SSG guardrails:** commit `route-build-policy.json`; run `validate-build-routes` or `pnpm validate:routes`. Templates in package `templates/`.
33
+
30
34
  **Per-app scripts:**
31
35
 
32
36
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/site-check",
3
- "version": "2.6.3",
3
+ "version": "2.7.1",
4
4
  "description": "Validate SE marketing sites (sitemap, llms.txt) and download markdown files preserving structure",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,15 +45,18 @@
45
45
  "site-check-screaming-frog": "./dist/screaming-frog-cli.js",
46
46
  "smoke-test-one": "./smoke-test-one.mjs",
47
47
  "smoke-test-deploy-check": "./smoke-test-deploy-check.mjs",
48
- "smoke-test-live": "./smoke-test-live.mjs"
48
+ "smoke-test-live": "./smoke-test-live.mjs",
49
+ "validate-build-routes": "./validate-build-routes.mjs"
49
50
  },
50
51
  "files": [
51
52
  "dist",
52
53
  "docs",
54
+ "templates",
53
55
  "*.md",
54
56
  "smoke-test-one.mjs",
55
57
  "smoke-test-deploy-check.mjs",
56
- "smoke-test-live.mjs"
58
+ "smoke-test-live.mjs",
59
+ "validate-build-routes.mjs"
57
60
  ],
58
61
  "keywords": [
59
62
  "sitemap",
@@ -71,7 +74,7 @@
71
74
  "url": "https://github.com/Something-Else-Studio/se-core-product/issues"
72
75
  },
73
76
  "dependencies": {
74
- "@se-studio/contentful-rest-api": "1.11.0",
77
+ "@se-studio/contentful-rest-api": "1.12.0",
75
78
  "@se-studio/core-data-types": "1.5.1"
76
79
  },
77
80
  "devDependencies": {
@@ -13,6 +13,7 @@
13
13
  * SMOKE_TEST_READY_PATH=/ — path polled until server responds
14
14
  * SMOKE_TEST_USE_RUNNING_SERVER=true — reuse server already on port (default true)
15
15
  * SMOKE_TEST_SKIP_BUILD=true — skip pnpm build when SERVER_SCRIPT=start (post-build deploy check)
16
+ * SMOKE_TEST_VALIDATE_ROUTES=false — skip route-build-policy.json check after build (default on when file exists)
16
17
  * SMOKE_TEST_SKIP_INTEGRITY=true — HTTP smoke only (set by smoke-test-one when cmsIntegrity runs in parallel)
17
18
  * LOG_CMS_FETCH=1 — set when VERIFY_CACHE or AUDIT_CACHE_LOGS is true
18
19
  */
@@ -176,6 +177,11 @@ async function loadCacheLogAudit() {
176
177
  return import(modulePath);
177
178
  }
178
179
 
180
+ async function loadBuildRoutePolicy() {
181
+ const modulePath = path.join(packageDir, 'dist/smoke-test/build-route-policy.js');
182
+ return import(modulePath);
183
+ }
184
+
179
185
  async function loadSmokeIntegrityModules() {
180
186
  const [staticModule, configModule, integrityModule] = await Promise.all([
181
187
  import(path.join(packageDir, 'dist/smoke-test/static.js')),
@@ -229,27 +235,54 @@ const readyPath = env.SMOKE_TEST_READY_PATH ?? '/';
229
235
  const readyUrl = `${baseUrl}${readyPath.startsWith('/') ? readyPath : `/${readyPath}`}`;
230
236
  const isProductionStart = serverScript === 'start';
231
237
  const skipBuild = (env.SMOKE_TEST_SKIP_BUILD ?? '').toLowerCase() === 'true';
238
+ const validateRoutes = (env.SMOKE_TEST_VALIDATE_ROUTES ?? 'true').toLowerCase() !== 'false';
239
+ const routePolicyPath = path.join(appDir, 'route-build-policy.json');
232
240
  const startTimeoutMs = isProductionStart ? 120_000 : 300_000;
233
241
 
234
242
  const logBuffer = { text: '' };
235
243
 
236
244
  if (isProductionStart && !skipBuild) {
245
+ const captureBuildOutput =
246
+ auditCacheLogsEnabled || (validateRoutes && fs.existsSync(routePolicyPath));
247
+
237
248
  console.log('Building app...');
238
249
  const buildResult = spawnSync('pnpm', ['build'], {
239
250
  cwd: appDir,
240
- stdio: auditCacheLogsEnabled ? 'pipe' : 'inherit',
251
+ stdio: captureBuildOutput ? 'pipe' : 'inherit',
241
252
  env,
242
- encoding: auditCacheLogsEnabled ? 'utf8' : undefined,
253
+ encoding: captureBuildOutput ? 'utf8' : undefined,
243
254
  });
244
- if (auditCacheLogsEnabled) {
255
+ if (captureBuildOutput) {
245
256
  logBuffer.text += buildResult.stdout ?? '';
246
257
  logBuffer.text += buildResult.stderr ?? '';
247
258
  if (buildResult.stdout) process.stdout.write(buildResult.stdout);
248
259
  if (buildResult.stderr) process.stderr.write(buildResult.stderr);
260
+
261
+ if (validateRoutes && fs.existsSync(routePolicyPath)) {
262
+ const buildLogPath = path.join(appDir, '.next', 'validate-routes-build.log');
263
+ fs.mkdirSync(path.dirname(buildLogPath), { recursive: true });
264
+ fs.writeFileSync(buildLogPath, logBuffer.text);
265
+ }
249
266
  }
250
267
  if (buildResult.status !== 0) {
251
268
  process.exit(buildResult.status ?? 1);
252
269
  }
270
+
271
+ if (validateRoutes && fs.existsSync(routePolicyPath)) {
272
+ const {
273
+ formatBuildRoutePolicyReport,
274
+ getBuildRoutePolicyExitCode,
275
+ validateBuildRoutesFromBuildOutput,
276
+ } = await loadBuildRoutePolicy();
277
+ const routeResult = validateBuildRoutesFromBuildOutput(logBuffer.text, { cwd: appDir });
278
+ if (routeResult) {
279
+ console.log('');
280
+ console.log(formatBuildRoutePolicyReport(routeResult));
281
+ if (getBuildRoutePolicyExitCode(routeResult) !== 0) {
282
+ process.exit(1);
283
+ }
284
+ }
285
+ }
253
286
  }
254
287
 
255
288
  const enableCmsFetchLog = verifyCache || auditCacheLogsEnabled;
@@ -0,0 +1,27 @@
1
+ {
2
+ "includes": ["**/src/app/layout.tsx", "**/src/app/(cms-routes)/**", "**/src/project/**"],
3
+ "linter": {
4
+ "rules": {
5
+ "style": {
6
+ "noRestrictedImports": {
7
+ "level": "error",
8
+ "options": {
9
+ "paths": {
10
+ "next/headers": {
11
+ "message": "Do not use next/headers in SSG surfaces (headers, cookies, draftMode break on-demand SSG in production). Use client-side window.location or createHubSpotBootstrapScript() with no args instead."
12
+ },
13
+ "next/server": {
14
+ "importNames": ["connection"],
15
+ "message": "Do not use connection() in SSG surfaces — it forces dynamic rendering and breaks on-demand SSG routes in production."
16
+ },
17
+ "next/cache": {
18
+ "importNames": ["unstable_noStore"],
19
+ "message": "Do not use unstable_noStore() in SSG surfaces — it opts out of static generation and breaks on-demand SSG routes in production."
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "allowedDynamic": [
3
+ "/",
4
+ "/articles",
5
+ "/tags",
6
+ "/people",
7
+ "/_not-found",
8
+ "/download/[assetId]/[filename]"
9
+ ],
10
+ "mustBeSsgOrStatic": [
11
+ "/[level1]",
12
+ "/[level1]/[...slugs]",
13
+ "/articles/[articleType]",
14
+ "/articles/[articleType]/[...slugs]",
15
+ "/tags/[tag]",
16
+ "/people/[person]"
17
+ ]
18
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Validate Next.js build route table against route-build-policy.json.
5
+ * Usage: validate-build-routes (from app directory)
6
+ *
7
+ * Env:
8
+ * VALIDATE_ROUTES_SKIP_BUILD=true — assume build already ran; read .next/validate-routes-build.log
9
+ * VALIDATE_ROUTES_IGNORE=true — skip entirely (exit 0)
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { spawnSync } from 'node:child_process';
16
+
17
+ const packageDir = path.dirname(fileURLToPath(import.meta.url));
18
+ const appDir = process.cwd();
19
+ const policyPath = path.join(appDir, 'route-build-policy.json');
20
+ const buildLogPath = path.join(appDir, '.next', 'validate-routes-build.log');
21
+
22
+ if ((process.env.VALIDATE_ROUTES_IGNORE ?? '').toLowerCase() === 'true') {
23
+ console.log('Skipped (VALIDATE_ROUTES_IGNORE=true).');
24
+ process.exit(0);
25
+ }
26
+
27
+ if (!fs.existsSync(policyPath)) {
28
+ console.log('Skipped (no route-build-policy.json).');
29
+ process.exit(0);
30
+ }
31
+
32
+ const {
33
+ formatBuildRoutePolicyReport,
34
+ getBuildRoutePolicyExitCode,
35
+ validateBuildRoutesFromBuildOutput,
36
+ } = await import(path.join(packageDir, 'dist/smoke-test/build-route-policy.js'));
37
+
38
+ let buildOutput = '';
39
+
40
+ if ((process.env.VALIDATE_ROUTES_SKIP_BUILD ?? '').toLowerCase() === 'true') {
41
+ if (!fs.existsSync(buildLogPath)) {
42
+ console.error(
43
+ `Missing ${buildLogPath}. Run pnpm build first or unset VALIDATE_ROUTES_SKIP_BUILD.`,
44
+ );
45
+ process.exit(1);
46
+ }
47
+ buildOutput = fs.readFileSync(buildLogPath, 'utf8');
48
+ } else {
49
+ console.log('Building app for route policy validation...');
50
+ const buildResult = spawnSync('pnpm', ['build'], {
51
+ cwd: appDir,
52
+ env: process.env,
53
+ encoding: 'utf8',
54
+ });
55
+
56
+ buildOutput = `${buildResult.stdout ?? ''}${buildResult.stderr ?? ''}`;
57
+ fs.mkdirSync(path.dirname(buildLogPath), { recursive: true });
58
+ fs.writeFileSync(buildLogPath, buildOutput);
59
+
60
+ if (buildResult.status !== 0) {
61
+ if (buildResult.stdout) process.stdout.write(buildResult.stdout);
62
+ if (buildResult.stderr) process.stderr.write(buildResult.stderr);
63
+ process.exit(buildResult.status ?? 1);
64
+ }
65
+ }
66
+
67
+ const result = validateBuildRoutesFromBuildOutput(buildOutput, { cwd: appDir });
68
+ if (!result) {
69
+ console.log('Skipped (route-build-policy.json could not be loaded).');
70
+ process.exit(0);
71
+ }
72
+
73
+ console.log(formatBuildRoutePolicyReport(result));
74
+ process.exit(getBuildRoutePolicyExitCode(result));