snappy-prerender 2.6.0 → 2.8.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 (49) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +281 -101
  3. package/assets/snappy-logo.png +0 -0
  4. package/docs/limitations.md +94 -0
  5. package/docs/react-snap-migration.md +45 -0
  6. package/docs/report.md +135 -0
  7. package/package.json +4 -1
  8. package/src/cli-init.js +205 -0
  9. package/src/cli-options.js +258 -0
  10. package/src/cli.js +128 -309
  11. package/src/core/artifacts.js +2 -0
  12. package/src/core/audit/index.js +114 -0
  13. package/src/core/audit/links.js +74 -0
  14. package/src/core/audit/metadata.js +197 -0
  15. package/src/core/audit/sitemap.js +39 -0
  16. package/src/core/clock.js +60 -0
  17. package/src/core/config-validation.js +557 -0
  18. package/src/core/config-warnings.js +150 -0
  19. package/src/core/config.js +189 -136
  20. package/src/core/crawl.js +362 -0
  21. package/src/core/csp.js +138 -0
  22. package/src/core/destination.js +23 -7
  23. package/src/core/frameworks.js +94 -0
  24. package/src/core/hydration-diff.js +93 -0
  25. package/src/core/hydration-policy.js +38 -0
  26. package/src/core/init.js +143 -30
  27. package/src/core/inline-css.js +52 -9
  28. package/src/core/locale-context.js +63 -0
  29. package/src/core/locales.js +385 -0
  30. package/src/core/normalize.js +37 -6
  31. package/src/core/page-content.js +57 -7
  32. package/src/core/page-meta.js +122 -0
  33. package/src/core/preload-links.js +133 -0
  34. package/src/core/preload-manifest.js +6 -2
  35. package/src/core/render.js +126 -35
  36. package/src/core/report.js +116 -0
  37. package/src/core/resources.js +5 -1
  38. package/src/core/route-config.js +17 -0
  39. package/src/core/route-sources.js +220 -0
  40. package/src/core/run.js +156 -131
  41. package/src/core/seo-locale.js +234 -0
  42. package/src/core/settle.js +2 -4
  43. package/src/core/sitemap.js +90 -14
  44. package/src/core/text-separators.js +11 -7
  45. package/src/core/verify.js +164 -71
  46. package/src/core/write.js +314 -52
  47. package/src/head/index.js +104 -6
  48. package/src/head/manager.js +304 -48
  49. package/src/head/tags.js +349 -30
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Translates parsed flags into prerender options, only including flags the user actually
3
+ * passed so config-file values are never clobbered by defaults.
4
+ * @param {{ values: object, positionals: string[] }} input
5
+ * @returns {object}
6
+ */
7
+ export function cliOptions({ values, positionals }) {
8
+ const options = {};
9
+ if (positionals[0] !== undefined) options.sourceDir = positionals[0];
10
+ if (values.source !== undefined) options.sourceDir = values.source;
11
+ if (values.base !== undefined) options.base = values.base;
12
+ if (values.include !== undefined) options.include = splitList(values.include);
13
+ if (values.exclude !== undefined) options.exclude = splitList(values.exclude);
14
+ if (values['no-crawl']) options.crawl = false;
15
+ if (values['max-depth'] !== undefined) options.maxDepth = Number(values['max-depth']);
16
+ if (values['max-routes'] !== undefined) options.maxRoutes = Number(values['max-routes']);
17
+ if (values.concurrency !== undefined) options.concurrency = Number(values.concurrency);
18
+ if (values.timeout !== undefined) options.timeout = Number(values.timeout);
19
+ if (values['quiet-period'] !== undefined) options.quietPeriod = Number(values['quiet-period']);
20
+ if (values['scroll-step-delay'] !== undefined) {
21
+ options.scrollStepDelay = Number(values['scroll-step-delay']);
22
+ }
23
+ if (values['max-cached-bytes'] !== undefined) {
24
+ options.maxCachedBytes = Number(values['max-cached-bytes']);
25
+ }
26
+ if (values['serve-cmd-timeout'] !== undefined) {
27
+ options.serveCmdTimeout = Number(values['serve-cmd-timeout']);
28
+ }
29
+ if (values['shutdown-timeout'] !== undefined) {
30
+ options.shutdownTimeout = Number(values['shutdown-timeout']);
31
+ }
32
+ if (values.viewport !== undefined) {
33
+ const match = /^(\d+)x(\d+)$/i.exec(values.viewport);
34
+ if (!match) throw new Error('viewport must look like 1280x720');
35
+ options.viewport = { width: Number(match[1]), height: Number(match[2]) };
36
+ }
37
+ if (values['ready-selector'] !== undefined) options.readySelector = values['ready-selector'];
38
+ if (values['ready-flag'] !== undefined) options.readyFlag = values['ready-flag'];
39
+ if (values.headless !== undefined) options.headless = values.headless;
40
+ if (values['wait-for'] !== undefined) options.waitFor = values['wait-for'];
41
+ if (values.browser !== undefined) options.browser = values.browser;
42
+ if (values['no-browser-download']) options.browserDownload = false;
43
+ if (values['browser-download-hash'] !== undefined) {
44
+ options.browserDownloadHash = values['browser-download-hash'];
45
+ }
46
+ if (values['browser-download-version'] !== undefined) {
47
+ options.browserDownloadVersion = values['browser-download-version'];
48
+ }
49
+ if (values['storage-state'] !== undefined) options.storageState = values['storage-state'];
50
+ if (values['freeze-time'] !== undefined) {
51
+ options.freezeTime = values['freeze-time'] === 'build' ? true : values['freeze-time'];
52
+ }
53
+ if (values.timezone !== undefined) options.timezone = values.timezone;
54
+ if (values['random-seed'] !== undefined) options.randomSeed = Number(values['random-seed']);
55
+ if (values['allow-rerender'] !== undefined || values['skip-hydration'] !== undefined) {
56
+ options.hydration = {
57
+ ...(values['allow-rerender'] !== undefined
58
+ ? { allowRerender: splitList(values['allow-rerender']) }
59
+ : {}),
60
+ ...(values['skip-hydration'] !== undefined
61
+ ? { skip: splitList(values['skip-hydration']) }
62
+ : {}),
63
+ };
64
+ }
65
+ const localeOptions = localeCliOptions(values);
66
+ if (localeOptions) options.locales = localeOptions;
67
+ const seoLocale = seoLocaleCliOptions(values);
68
+ if (seoLocale) options.seoLocale = seoLocale;
69
+ if (values['no-verify']) options.verify = false;
70
+ if (values['verify-changed-only']) options.verifyChangedOnly = true;
71
+ if (values['reuse-verification-context']) options.reuseVerificationContext = true;
72
+ if (values['fail-on-hydration-error']) options.failOnHydrationError = true;
73
+ if (values['fail-on-rerender']) options.failOnRerender = true;
74
+ if (values['fail-on-page-error']) options.failOnPageError = true;
75
+ if (values['fail-on-error'] !== undefined) options.failOnError = values['fail-on-error'];
76
+ if (values['fail-on-empty-page'] !== undefined) {
77
+ options.failOnEmptyPage = values['fail-on-empty-page'];
78
+ }
79
+ if (values['fail-on-console-error']) options.failOnConsoleError = true;
80
+ if (values['fail-on-failed-request']) options.failOnFailedRequest = true;
81
+ if (values['fail-on-duplicate-content']) options.failOnDuplicateContent = true;
82
+ if (values['fail-on-build-origin-metadata']) options.failOnBuildOriginMetadata = true;
83
+ if (values['fail-on-undetected']) options.failOnUndetected = true;
84
+ if (values['fail-on-verification-error']) options.failOnVerificationError = true;
85
+ if (values['no-block-third-party']) options.blockThirdParty = false;
86
+ if (values['block-third-party']) options.blockThirdParty = true;
87
+ if (values['allowed-hosts'] !== undefined)
88
+ options.allowedHosts = splitList(values['allowed-hosts']);
89
+ if (values['no-freeze-animations']) options.freezeAnimations = false;
90
+ if (values['no-capture-runtime-styles']) options.captureRuntimeStyles = false;
91
+ if (values['no-capture-form-state']) options.captureFormState = false;
92
+ if (values.scroll) options.scrollToBottom = true;
93
+ if (values.flat) options.flatOutput = true;
94
+ if (values['not-found'] !== undefined) options.notFoundRoute = values['not-found'];
95
+ if (values['routes-file'] !== undefined) options.routesFile = values['routes-file'];
96
+ if (values['routes-from-sitemap'] !== undefined) {
97
+ options.routesFromSitemap = values['routes-from-sitemap'];
98
+ }
99
+ if (values['route-priority'] !== undefined) {
100
+ options.routePriority = splitList(values['route-priority']);
101
+ }
102
+ if (values.report !== undefined) options.reportPath = values.report;
103
+ if (values.audit !== undefined) options.audit = values.audit;
104
+ else if (values['no-audit']) options.audit = false;
105
+ if (values['fail-on-audit-error']) options.failOnAuditError = true;
106
+ if (values['audit-ignore-links'] !== undefined) {
107
+ options.auditIgnoreLinks = splitList(values['audit-ignore-links']);
108
+ }
109
+ if (values['preload-links']) options.preloadLinks = true;
110
+ if (values['image-sitemap']) options.imageSitemap = true;
111
+ if (values.framework !== undefined) options.framework = values.framework;
112
+ if (values.retries !== undefined) options.retries = Number(values.retries);
113
+ if (values['retry-delay'] !== undefined) options.retryDelay = Number(values['retry-delay']);
114
+ if (values['trace-on-error']) options.traceOnError = true;
115
+ if (values.destination !== undefined) options.destination = values.destination;
116
+ if (values['clean-destination']) options.cleanDestination = true;
117
+ if (values['save-as'] !== undefined) options.saveAs = values['save-as'];
118
+ if (values['user-agent'] !== undefined) options.userAgent = values['user-agent'];
119
+ const metadata = metadataOptions(values);
120
+ if (metadata) options.metadata = metadata;
121
+ if (values['inline-css'] !== undefined) options.inlineCss = values['inline-css'];
122
+ if (values.csp !== undefined) options.csp = values.csp;
123
+ if (values['critical-css-preload'] !== undefined) {
124
+ options.criticalCssPreload =
125
+ values['critical-css-preload'] === 'none' ? false : values['critical-css-preload'];
126
+ }
127
+ if (values['minify-html']) options.minifyHtml = true;
128
+ if (values['minify-css']) options.minifyCss = true;
129
+ if (values['no-preconnect-third-party']) options.preconnectThirdParty = false;
130
+ if (values['preconnect-third-party']) options.preconnectThirdParty = true;
131
+ if (values['preload-images']) options.preloadImages = true;
132
+ if (values['preload-manifest']) options.preloadManifest = true;
133
+ if (values.sitemap) options.sitemap = true;
134
+ if (values['ignore-for-preload'] !== undefined) {
135
+ options.ignoreForPreload = splitList(values['ignore-for-preload']);
136
+ }
137
+ if (values['cache-ajax-requests']) options.cacheAjaxRequests = true;
138
+ if (values['remove-scripts']) options.removeScriptTags = true;
139
+ if (values['remove-styles']) options.removeStyleTags = true;
140
+ if (values['async-scripts']) options.asyncScriptTags = true;
141
+ if (values['external-scripts']) options.externalScripts = true;
142
+ if (values['no-remove-blobs']) options.removeBlobs = false;
143
+ if (values['remove-attributes'] !== undefined) {
144
+ options.removeAttributes = splitList(values['remove-attributes']);
145
+ }
146
+ if (values['no-strip-build-origin-metadata']) options.stripBuildOriginMetadata = false;
147
+ if (values['prune-stale-routes']) options.pruneStaleRoutes = true;
148
+ if (values['ignore-https-errors']) options.ignoreHTTPSErrors = true;
149
+ if (values['browser-args'] !== undefined) options.browserArgs = splitList(values['browser-args']);
150
+ if (values.url !== undefined) options.url = values.url;
151
+ if (values['serve-cmd'] !== undefined) options.serveCmd = values['serve-cmd'];
152
+ if (values['dry-run']) options.dryRun = true;
153
+ if (values['log-level'] !== undefined) options.logLevel = values['log-level'];
154
+ return options;
155
+ }
156
+
157
+ // Splits comma separated flag values and drops empty entries.
158
+ function splitList(entries) {
159
+ return entries.flatMap((entry) => entry.split(',')).filter((entry) => entry.length > 0);
160
+ }
161
+
162
+ // Parses repeatable code=value flags used by the locale matrix.
163
+ function parsePairs(entries) {
164
+ const map = {};
165
+ for (const entry of splitList(entries)) {
166
+ const index = entry.indexOf('=');
167
+ if (index <= 0) throw new Error('locale options must look like code=value');
168
+ map[entry.slice(0, index)] = entry.slice(index + 1);
169
+ }
170
+ return map;
171
+ }
172
+
173
+ // Builds a locale matrix from the simple CLI flags. Full per-locale cookies, storage and
174
+ // translated slugs stay in a config file.
175
+ function localeCliOptions(values) {
176
+ const enabled =
177
+ values.locales !== undefined ||
178
+ values['locale-mode'] !== undefined ||
179
+ values['active-locale'] !== undefined;
180
+ if (!enabled) return null;
181
+ if (values.locales === undefined) throw new Error('--locales is required with locale flags');
182
+ const codes = splitList([values.locales]);
183
+ const defaultCode = values['default-locale'] ?? values['active-locale'] ?? codes[0];
184
+ const mode = values['locale-mode'] ?? (values['active-locale'] ? 'single' : 'path');
185
+ const prefixes = parsePairs(values['locale-prefix'] ?? []);
186
+ const urls = parsePairs(values['locale-url'] ?? []);
187
+ const states = parsePairs(values['locale-storage-state'] ?? []);
188
+ return {
189
+ mode,
190
+ default: defaultCode,
191
+ ...(values['active-locale'] ? { active: values['active-locale'] } : {}),
192
+ hreflang: values['no-hreflang'] ? false : undefined,
193
+ list: codes.map((code) => ({
194
+ code,
195
+ prefix: prefixes[code],
196
+ url: urls[code],
197
+ storageState: states[code],
198
+ })),
199
+ };
200
+ }
201
+
202
+ // Builds the optional SEO locale recipe from the CLI flags.
203
+ function seoLocaleCliOptions(values) {
204
+ const enabled =
205
+ values['seo-locale'] !== undefined ||
206
+ values['seo-locale-source'] !== undefined ||
207
+ values['seo-locale-key'] !== undefined ||
208
+ values['seo-locale-cookie'] !== undefined ||
209
+ values['no-seo-locale-inject'] ||
210
+ values['no-seo-locale-verify'] ||
211
+ values['no-seo-locale-render'] ||
212
+ values['no-seo-locale-overwrite'] ||
213
+ values['seo-locale-navigator'] ||
214
+ values['seo-locale-force-html-lang'];
215
+ if (!enabled) return null;
216
+ return {
217
+ ...(values['seo-locale'] ? { code: values['seo-locale'] } : {}),
218
+ ...(values['seo-locale-source'] ? { source: values['seo-locale-source'] } : {}),
219
+ ...(values['seo-locale-key'] ? { localStorageKey: values['seo-locale-key'] } : {}),
220
+ ...(values['seo-locale-cookie'] ? { cookie: { name: values['seo-locale-cookie'] } } : {}),
221
+ ...(values['no-seo-locale-inject'] ? { inject: false } : {}),
222
+ ...(values['no-seo-locale-verify'] ? { seedVerification: false } : {}),
223
+ ...(values['no-seo-locale-render'] ? { seedRender: false } : {}),
224
+ ...(values['no-seo-locale-overwrite'] ? { overwrite: false } : {}),
225
+ ...(values['seo-locale-navigator'] ? { navigator: true } : {}),
226
+ ...(values['seo-locale-force-html-lang'] ? { forceHtmlLang: true } : {}),
227
+ };
228
+ }
229
+
230
+ // Collects the metadata flags, or null when none was passed so a config file value survives.
231
+ function metadataOptions(values) {
232
+ const metadata = {};
233
+ if (values['metadata-site-url'] !== undefined) metadata.siteUrl = values['metadata-site-url'];
234
+ if (values['metadata-site-name'] !== undefined) {
235
+ metadata.siteName = values['metadata-site-name'];
236
+ }
237
+ if (values['metadata-title-template'] !== undefined) {
238
+ metadata.titleTemplate = values['metadata-title-template'];
239
+ }
240
+ if (values['metadata-default-title'] !== undefined) {
241
+ metadata.defaultTitle = values['metadata-default-title'];
242
+ }
243
+ if (values['metadata-default-image'] !== undefined) {
244
+ metadata.defaultImage = values['metadata-default-image'];
245
+ }
246
+ if (values['metadata-trailing-slash'] !== undefined) {
247
+ metadata.trailingSlash = values['metadata-trailing-slash'];
248
+ }
249
+ if (values['metadata-base'] !== undefined) metadata.base = values['metadata-base'];
250
+ return Object.keys(metadata).length > 0 ? metadata : null;
251
+ }
252
+
253
+ /**
254
+ * Loads a JS config file. An explicitly requested file must exist; the implicit
255
+ * snappy.config.js is optional. The default export is either a plain options object or an
256
+ * async function that returns one, which is how routes are computed from a CMS, a sitemap or
257
+ * a file listing without a separate prebuild script.
258
+ */