@webstir-io/webstir-frontend 0.1.40 → 0.1.41
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/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- package/src/watch/watchReporter.ts +0 -98
package/src/modes/ssg/seo.ts
CHANGED
|
@@ -1,261 +1,269 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
2
|
import { load } from 'cheerio';
|
|
4
3
|
import { ensureDir, pathExists, readFile, writeFile } from '../../utils/fs.js';
|
|
5
4
|
import { FILES } from '../../core/constants.js';
|
|
5
|
+
import { scanGlob } from '../../utils/glob.js';
|
|
6
6
|
|
|
7
7
|
interface HtmlPage {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
readonly filePath: string;
|
|
9
|
+
readonly urlPath: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface SsgSeoOptions {
|
|
13
|
-
|
|
13
|
+
readonly siteUrl?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export async function runSsgSeo(distRoot: string, options: SsgSeoOptions = {}): Promise<void> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const pages = await discoverHtmlPages(distRoot);
|
|
18
|
+
await validateInternalLinks(pages, distRoot);
|
|
19
|
+
await writeSitemap(distRoot, pages, options.siteUrl);
|
|
20
|
+
await writeRobots(distRoot, options.siteUrl);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async function discoverHtmlPages(distRoot: string): Promise<HtmlPage[]> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
if (!(await pathExists(distRoot))) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const files = (await scanGlob('**/index.html', { cwd: distRoot })).filter(
|
|
29
|
+
(relative) => !relative.split(path.sep).join('/').startsWith('pages/'),
|
|
30
|
+
);
|
|
31
|
+
const pages = files
|
|
32
|
+
.map((relative) => {
|
|
33
|
+
const normalized = relative.split(path.sep).join('/');
|
|
34
|
+
const urlPath = toUrlPath(normalized);
|
|
35
|
+
return {
|
|
36
|
+
filePath: path.join(distRoot, relative),
|
|
37
|
+
urlPath,
|
|
38
|
+
};
|
|
39
|
+
})
|
|
40
|
+
.filter((page) => Boolean(page.urlPath));
|
|
41
|
+
|
|
42
|
+
pages.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
43
|
+
return pages;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function toUrlPath(relativeIndex: string): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (relativeIndex === FILES.indexHtml) {
|
|
48
|
+
return '/';
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (relativeIndex.endsWith(`/${FILES.indexHtml}`)) {
|
|
52
|
+
return `/${relativeIndex.slice(0, -FILES.indexHtml.length)}`;
|
|
53
|
+
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
return '';
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
async function validateInternalLinks(pages: readonly HtmlPage[], distRoot: string): Promise<void> {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
errors.push(`${page.filePath}: broken link '${rawHref}'`);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const hash = resolved.hash.startsWith('#') ? resolved.hash.slice(1) : resolved.hash;
|
|
97
|
-
if (!hash) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const ids = idsByPath.get(normalizedPath) ?? new Set<string>();
|
|
102
|
-
if (!ids.has(hash)) {
|
|
103
|
-
errors.push(`${page.filePath}: broken anchor '${rawHref}' (missing '#${hash}' on ${normalizedPath})`);
|
|
104
|
-
}
|
|
59
|
+
if (pages.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const knownPages = new Set<string>(pages.map((page) => page.urlPath));
|
|
64
|
+
const idsByPath = await collectIdsByPath(pages);
|
|
65
|
+
const errors: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (const page of pages) {
|
|
68
|
+
const html = await readFile(page.filePath);
|
|
69
|
+
const doc = load(html);
|
|
70
|
+
const anchors = doc('a[href]').toArray();
|
|
71
|
+
|
|
72
|
+
for (const element of anchors) {
|
|
73
|
+
const rawHref = doc(element).attr('href')?.trim() ?? '';
|
|
74
|
+
if (!rawHref) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isExternalHref(rawHref)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolved = resolveHref(page.urlPath, rawHref);
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizedPath = normalizePagePath(resolved.pathname, knownPages);
|
|
88
|
+
if (!normalizedPath) {
|
|
89
|
+
// Still allow links to known static assets (best-effort).
|
|
90
|
+
if (await targetExistsInDist(distRoot, resolved.pathname)) {
|
|
91
|
+
continue;
|
|
105
92
|
}
|
|
106
|
-
}
|
|
107
93
|
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
errors.push(`${page.filePath}: broken link '${rawHref}'`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const hash = resolved.hash.startsWith('#') ? resolved.hash.slice(1) : resolved.hash;
|
|
99
|
+
if (!hash) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ids = idsByPath.get(normalizedPath) ?? new Set<string>();
|
|
104
|
+
if (!ids.has(hash)) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`${page.filePath}: broken anchor '${rawHref}' (missing '#${hash}' on ${normalizedPath})`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
110
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (errors.length === 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
const preview = errors.slice(0, 16).join('\n');
|
|
117
|
+
const suffix = errors.length > 16 ? `\n… and ${errors.length - 16} more.` : '';
|
|
118
|
+
throw new Error(`Broken links found in publish output:\n${preview}${suffix}`);
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
async function collectIdsByPath(pages: readonly HtmlPage[]): Promise<Map<string, Set<string>>> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
const idsByPath = new Map<string, Set<string>>();
|
|
123
|
+
|
|
124
|
+
for (const page of pages) {
|
|
125
|
+
const html = await readFile(page.filePath);
|
|
126
|
+
const doc = load(html);
|
|
127
|
+
const ids = new Set<string>();
|
|
128
|
+
|
|
129
|
+
doc('[id]').each((_, element) => {
|
|
130
|
+
const raw = doc(element).attr('id');
|
|
131
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
132
|
+
if (value) {
|
|
133
|
+
ids.add(value);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
idsByPath.set(page.urlPath, ids);
|
|
138
|
+
}
|
|
135
139
|
|
|
136
|
-
|
|
140
|
+
return idsByPath;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
function isExternalHref(href: string): boolean {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
if (href.startsWith('//')) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
function resolveHref(basePath: string, href: string): URL | null {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
try {
|
|
152
|
+
const base = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
|
153
|
+
return new URL(href, `http://webstir.local${base}`);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
function normalizePagePath(pathname: string, knownPages: ReadonlySet<string>): string | null {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
if (pathname === '/' || pathname === '') {
|
|
161
|
+
return '/';
|
|
162
|
+
}
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
if (pathname.endsWith('/index.html')) {
|
|
165
|
+
const asDir = pathname.slice(0, -'index.html'.length);
|
|
166
|
+
return knownPages.has(asDir) ? asDir : null;
|
|
167
|
+
}
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
if (pathname.endsWith('.html')) {
|
|
170
|
+
return knownPages.has(pathname) ? pathname : null;
|
|
171
|
+
}
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
if (pathname.endsWith('/')) {
|
|
174
|
+
return knownPages.has(pathname) ? pathname : null;
|
|
175
|
+
}
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
const withSlash = `${pathname}/`;
|
|
178
|
+
if (knownPages.has(withSlash)) {
|
|
179
|
+
return withSlash;
|
|
180
|
+
}
|
|
177
181
|
|
|
178
|
-
|
|
182
|
+
return knownPages.has(pathname) ? pathname : null;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
async function targetExistsInDist(distRoot: string, pathname: string): Promise<boolean> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
186
|
+
if (!pathname.startsWith('/')) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const relative = pathname.replace(/^\/+/, '');
|
|
191
|
+
if (!relative) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const full = path.join(distRoot, relative);
|
|
196
|
+
if (await pathExists(full)) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const asIndex = path.join(distRoot, relative, FILES.indexHtml);
|
|
201
|
+
return pathExists(asIndex);
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
async function writeSitemap(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
204
|
+
async function writeSitemap(
|
|
205
|
+
distRoot: string,
|
|
206
|
+
pages: readonly HtmlPage[],
|
|
207
|
+
siteUrl?: string,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const urls = pages.map((page) => page.urlPath).filter((url) => url.startsWith('/'));
|
|
210
|
+
const unique = Array.from(new Set(urls)).sort((a, b) => a.localeCompare(b));
|
|
211
|
+
|
|
212
|
+
const baseUrl = normalizeSiteUrl(siteUrl);
|
|
213
|
+
const comment = baseUrl ? '' : '<!-- Set WEBSTIR_SITE_URL to emit absolute <loc> entries. -->\n';
|
|
214
|
+
|
|
215
|
+
const entries = unique
|
|
216
|
+
.map((pathname) => {
|
|
217
|
+
const loc = baseUrl ? new URL(pathname, baseUrl).href : pathname;
|
|
218
|
+
return ` <url><loc>${escapeXml(loc)}</loc></url>`;
|
|
219
|
+
})
|
|
220
|
+
.join('\n');
|
|
221
|
+
|
|
222
|
+
const xml = [
|
|
223
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
224
|
+
`${comment}<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
225
|
+
entries,
|
|
226
|
+
'</urlset>',
|
|
227
|
+
'',
|
|
228
|
+
].join('\n');
|
|
229
|
+
|
|
230
|
+
const outputPath = path.join(distRoot, 'sitemap.xml');
|
|
231
|
+
await ensureDir(path.dirname(outputPath));
|
|
232
|
+
await writeFile(outputPath, xml);
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
async function writeRobots(distRoot: string, siteUrl?: string): Promise<void> {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
const baseUrl = normalizeSiteUrl(siteUrl);
|
|
237
|
+
const sitemapLine = baseUrl ? `\nSitemap: ${new URL('/sitemap.xml', baseUrl).href}` : '';
|
|
238
|
+
const content = `User-agent: *\nAllow: /${sitemapLine}\n`;
|
|
231
239
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
const outputPath = path.join(distRoot, FILES.robotsTxt);
|
|
241
|
+
await ensureDir(path.dirname(outputPath));
|
|
242
|
+
await writeFile(outputPath, content);
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
function normalizeSiteUrl(value: string | undefined): string | undefined {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
246
|
+
const trimmed = (value ?? '').trim();
|
|
247
|
+
if (!trimmed) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const url = new URL(trimmed);
|
|
253
|
+
url.pathname = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
|
254
|
+
url.hash = '';
|
|
255
|
+
url.search = '';
|
|
256
|
+
return url.toString();
|
|
257
|
+
} catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
function escapeXml(value: string): string {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
return value
|
|
264
|
+
.replace(/&/g, '&')
|
|
265
|
+
.replace(/</g, '<')
|
|
266
|
+
.replace(/>/g, '>')
|
|
267
|
+
.replace(/"/g, '"')
|
|
268
|
+
.replace(/'/g, ''');
|
|
261
269
|
}
|
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readJson } from '../../utils/fs.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
WorkspaceModuleConfig,
|
|
5
|
+
WorkspacePackageJson,
|
|
6
|
+
} from '../../config/workspaceManifest.js';
|
|
4
7
|
|
|
5
|
-
export function assertNoSsgRoutesInModuleConfig(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export function assertNoSsgRoutesInModuleConfig(
|
|
9
|
+
moduleConfig: WorkspaceModuleConfig | undefined,
|
|
10
|
+
): void {
|
|
11
|
+
const routes = moduleConfig?.routes ?? [];
|
|
12
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const hasSsgRoute = routes.some((route) => {
|
|
17
|
+
if (!route || typeof route !== 'object') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const renderMode =
|
|
22
|
+
typeof route.renderMode === 'string' ? route.renderMode.toLowerCase() : undefined;
|
|
23
|
+
const hasStaticPaths = Array.isArray(route.staticPaths) && route.staticPaths.length > 0;
|
|
24
|
+
const hasSsgBlock = route.ssg !== undefined;
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
return renderMode === 'ssg' || hasStaticPaths || hasSsgBlock;
|
|
27
|
+
});
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
if (!hasSsgRoute) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
throw new Error(
|
|
34
|
+
"SSG publish expects SSG metadata under `webstir.moduleManifest.views`, not `webstir.moduleManifest.routes`. Move `renderMode: 'ssg'`, `staticPaths`, and/or `ssg` onto the corresponding view definition.",
|
|
35
|
+
);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export async function assertNoSsgRoutes(workspaceRoot: string): Promise<void> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const pkgPath = path.join(workspaceRoot, 'package.json');
|
|
40
|
+
const pkg = await readJson<WorkspacePackageJson>(pkgPath);
|
|
41
|
+
const moduleConfig = pkg?.webstir?.moduleManifest;
|
|
42
|
+
assertNoSsgRoutesInModuleConfig(moduleConfig);
|
|
37
43
|
}
|