@lwrjs/core 0.5.11-alpha.1 → 0.6.0-alpha.10

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.
@@ -1,172 +1,16 @@
1
+ import { getSpecifier, getExperimentalFeatures } from '@lwrjs/shared-utils';
1
2
  import { join, dirname, extname } from 'path';
2
3
  import fs from 'fs-extra';
3
4
  import { writeResponse } from './utils/stream.js';
4
5
  import { createDir, createResourceDir } from './utils/dir.js';
5
6
  export default class SiteGenerator {
6
- async dispatchResourceRecursive(url, dispatcher, resourceOpts, siteConfig) {
7
- const { outputDir, visitedUrls } = siteConfig;
8
- if (!visitedUrls.has(url)) {
9
- visitedUrls.add(url); // Maintain a list of visited urls here to avoid potential infinite loops
10
- // Skip urls with path segment variables (i.e. '/custom/:bar')
11
- // Users can specify specific urls via the '_additionalRoutePaths' config property
12
- if (url.indexOf('/:') !== -1) {
13
- console.error('Skipped url with variable path segment: ' + url);
14
- return;
15
- }
16
- // Generate resource
17
- const context = await dispatcher.dispatchUrl(url, 'GET', siteConfig.locale);
18
- const { resourceType } = resourceOpts;
19
- // -- Routes (root resources) -------
20
- if (resourceType === 'route') {
21
- const dir = createResourceDir(url, outputDir);
22
- const localeDir = createResourceDir(url, join(outputDir, siteConfig.locale));
23
- // TODO: We assume HTML, revisit this in the future
24
- // This fails for routes configured with non-html file extensions.
25
- // Example Route: "path": "/mixed_templates.md", (Configured as '/mixed_templates' would work)
26
- // "contentTemplate": "$contentDir/composed_markdown.md"
27
- const filePath = join(dir, 'index.html');
28
- const fileLocalePath = join(localeDir, 'index.html');
29
- // Default Path (only write once)
30
- // The default locale is english
31
- if (siteConfig.locale.toLowerCase().startsWith('en')) {
32
- await writeResponse(context, filePath);
33
- }
34
- // Language path (always write out)
35
- createDir(localeDir);
36
- await writeResponse(context, fileLocalePath);
37
- // Get the metadata
38
- const viewDefinition = context.fs?.metadata?.viewDefinition;
39
- if (viewDefinition) {
40
- // -- Dispatch dependencies
41
- const assets = viewDefinition.viewRecord.assetReferences || [];
42
- for (const asset of assets) {
43
- const assetUrl = asset.override?.uri || asset.url;
44
- // skip empty asset urls / data urls (i.e. <img src="" /> <img src="data:image/png;base64, iVBORw0..." />)
45
- if (assetUrl && !assetUrl.startsWith('data:')) {
46
- // eslint-disable-next-line no-await-in-loop
47
- await this.dispatchResourceRecursive(assetUrl, dispatcher, { resourceType: 'asset', asset }, siteConfig);
48
- }
49
- }
50
- // Custom Elements
51
- const customElements = viewDefinition.viewRecord.customElements || [];
52
- for (const customElement of customElements) {
53
- const jsUris = Object.values(customElement.flatGraph.uriMap);
54
- for (const jsUri of jsUris) {
55
- // eslint-disable-next-line no-await-in-loop
56
- await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
57
- }
58
- }
59
- // Bootstrap modules
60
- if (viewDefinition.viewRecord.bootstrapModule) {
61
- const boot = viewDefinition.viewRecord.bootstrapModule;
62
- const jsUris = Object.values(boot.flatGraph.uriMap);
63
- for (const jsUri of jsUris) {
64
- // eslint-disable-next-line no-await-in-loop
65
- await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
66
- }
67
- }
68
- // Bootstrap Resources
69
- const bootstrapResources = viewDefinition.viewRecord.bootstrapModule?.resources || [];
70
- for (const resource of bootstrapResources) {
71
- // If resource is not inline download it for reference
72
- if (!resource.inline) {
73
- const resourceUri = resource.src || resource.specifier;
74
- if (resourceUri) {
75
- // eslint-disable-next-line no-await-in-loop
76
- await this.dispatchResourceRecursive(resourceUri, dispatcher, { resourceType: 'resource' }, siteConfig);
77
- }
78
- else {
79
- console.warn('Skipped inline bootstrap resource: %j', resource);
80
- }
81
- }
82
- }
83
- // Resources
84
- const resources = viewDefinition.viewRecord.resources || [];
85
- for (const resource of resources) {
86
- const resourceUri = resource.src || resource.specifier || '';
87
- if (resourceUri.startsWith('/')) {
88
- // eslint-disable-next-line no-await-in-loop
89
- await this.dispatchResourceRecursive(resourceUri, dispatcher, { resourceType: 'resource' }, siteConfig);
90
- }
91
- else {
92
- console.warn('Skipped resource: %j', resource);
93
- }
94
- }
95
- }
96
- // -- Process Assets (css, images, ...) && Resources (lwr-loader-shim ...)
97
- }
98
- else if (resourceType === 'asset' || resourceType === 'resource') {
99
- const normalizedUrl = decodeURIComponent(url);
100
- createResourceDir(dirname(normalizedUrl), outputDir);
101
- const fullPath = join(outputDir, normalizedUrl);
102
- await writeResponse(context, fullPath);
103
- // -- Process JS files
104
- }
105
- else if (resourceType === 'js') {
106
- const normalizedUrl = decodeURIComponent(url);
107
- createResourceDir(dirname(normalizedUrl), outputDir);
108
- const ext = extname(normalizedUrl);
109
- const fullPath = join(outputDir, `${normalizedUrl}${ext ? '' : '.js'}`);
110
- await writeResponse(context, fullPath);
111
- // Add URL re-writes for module redirects
112
- if (normalizedUrl.indexOf('/s/') !== -1) {
113
- const rewriteUrl = normalizedUrl.substring(0, normalizedUrl.indexOf('/s/'));
114
- // Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6 -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
115
- siteConfig.urlRewriteMap.set(rewriteUrl, normalizedUrl);
116
- // Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr%2Fnavigation%2Fv%2F0_1_6 -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
117
- siteConfig.urlRewriteMap.set(url.substring(0, url.indexOf('/s/')), normalizedUrl);
118
- // TODO Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
119
- // Dynamic import example /1/bundle/amd/l/en-US/bi/0/module/mi/dynamic%2Fmodule?importer=examples%2Fhome%2Fv%2F0_1_6
120
- }
121
- // Recursively traverse dependencies
122
- const moduleDefinition = context.fs?.metadata.moduleDefinition; // LinkedModuleDefinition | BundleDefinition
123
- if (moduleDefinition) {
124
- // Imports
125
- const imports = moduleDefinition.linkedModuleRecord?.imports ||
126
- moduleDefinition.bundleRecord?.imports ||
127
- [];
128
- // /1/module/esm/0/l/en-US/mi/lwc
129
- for (const importModule of imports) {
130
- const jsUri = importModule.specifier;
131
- if (jsUri.startsWith('/')) {
132
- // eslint-disable-next-line no-await-in-loop
133
- await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
134
- }
135
- else {
136
- if (jsUri !== 'lwc')
137
- console.warn('Skipped import: ' + jsUri);
138
- }
139
- }
140
- // Dynamic imports
141
- const dynamicImports = moduleDefinition.linkedModuleRecord?.dynamicImports ||
142
- moduleDefinition.bundleRecord?.dynamicImports ||
143
- [];
144
- for (const importModule of dynamicImports) {
145
- const jsUri = importModule.specifier;
146
- if (jsUri.startsWith('/')) {
147
- // eslint-disable-next-line no-await-in-loop
148
- await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
149
- }
150
- else {
151
- console.warn('Skipped dynamic import: ' + jsUri);
152
- }
153
- }
154
- }
155
- // Bundles with unresolved module uris
156
- const uris = context.fs?.metadata.resolvedUris || [];
157
- for (const jsUri of uris) {
158
- // eslint-disable-next-line no-await-in-loop
159
- await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
160
- }
161
- }
162
- }
163
- }
164
7
  /**
165
8
  * Build a static site in the configured directory
166
9
  * - Generate all routes / modules
167
10
  * - copy assets / resources
168
11
  *
169
- * @param lwrApp
12
+ * @param config - LWR config for the site
13
+ * @param dispatcher - Facilitate server requests
170
14
  */
171
15
  async buildStaticApplication(config, dispatcher) {
172
16
  console.log('[Static Generation] starting');
@@ -175,34 +19,331 @@ export default class SiteGenerator {
175
19
  staticSiteGenerator.outputDir = '__generated_site__';
176
20
  }
177
21
  const outputDir = join(rootDir, staticSiteGenerator.outputDir);
22
+ console.log(`[INFO] Clear Output Location: ${outputDir}`);
23
+ fs.rmSync(outputDir, { recursive: true, force: true });
178
24
  const urlRewriteMap = new Map();
179
25
  // For each locale, generate all the modules
180
26
  await this.generateRoutes(staticSiteGenerator, routes, dispatcher, outputDir, urlRewriteMap);
181
27
  // Write redirect files
182
- this.writeUrlMappings(outputDir, urlRewriteMap);
28
+ this.writeNetlifyRedirectConfig(outputDir, urlRewriteMap);
183
29
  // Copy over assets
184
30
  this.copyAssets(assets, outputDir);
185
31
  console.log('[Static Generation] complete');
186
32
  }
33
+ /**
34
+ * Crawl all view routes for a site
35
+ */
187
36
  async generateRoutes(staticSiteGenerator, routes, dispatcher, outputDir, urlRewriteMap = new Map()) {
188
37
  if (!staticSiteGenerator.locales) {
189
38
  staticSiteGenerator.locales = ['en-US'];
190
39
  }
40
+ // Build up a list of dispatch requests to kick off in parallel
41
+ const dispatchRequests = [];
42
+ const generateUrl = this.createGenerateURLFunction(dispatcher);
191
43
  for (const locale of staticSiteGenerator.locales) {
192
- const visitedUrls = new Set();
193
44
  // Generate all the routes
194
45
  for (const route of routes) {
195
- // eslint-disable-next-line no-await-in-loop
196
- await this.dispatchResourceRecursive(route.path, dispatcher, { resourceType: 'route' }, { outputDir, visitedUrls, locale, urlRewriteMap });
46
+ const siteConfig = this.createSiteConfig(outputDir, locale, urlRewriteMap);
47
+ dispatchRequests.push(generateUrl(route.path, siteConfig));
197
48
  }
198
49
  // Generate any additional urls
199
50
  if (staticSiteGenerator._additionalRoutePaths) {
200
51
  for (const uri of staticSiteGenerator._additionalRoutePaths) {
201
- // eslint-disable-next-line no-await-in-loop
202
- await this.dispatchResourceRecursive(uri, dispatcher, { resourceType: 'route' }, { outputDir, visitedUrls, locale, urlRewriteMap });
52
+ const siteConfig = this.createSiteConfig(outputDir, locale, urlRewriteMap);
53
+ dispatchRequests.push(generateUrl(uri, siteConfig));
203
54
  }
204
55
  }
205
56
  }
57
+ // -- Dispatch routes
58
+ await Promise.all(dispatchRequests);
59
+ }
60
+ /**
61
+ * Creates a function to dispatch the root requests for a given view url
62
+ */
63
+ createGenerateURLFunction(dispatcher) {
64
+ const generateRoute = async (uri, siteConfig) => {
65
+ const locale = siteConfig.locale;
66
+ console.log(`[INFO] Start Generate: ${locale} ${uri}`);
67
+ // Kick off site generation for the current route
68
+ await this.dispatchResourceRecursive(uri, dispatcher, { resourceType: 'route' }, siteConfig);
69
+ // If there is a view config add any extra collected import metadata to the config
70
+ this.addAdditionalImportMetadataToViewConfig(siteConfig);
71
+ console.log(`[INFO] End Generate ${locale} ${uri}`);
72
+ };
73
+ return generateRoute.bind(this);
74
+ }
75
+ /**
76
+ * Primary recursive dispatch function. Executes a URL and crawls its dependents.
77
+ *
78
+ * @param url - URL to execute
79
+ * @param dispatcher - Facilitates GET requests
80
+ * @param resourceOpts - Data about the URL to execute
81
+ * @param siteConfig - Running config for the current view
82
+ */
83
+ async dispatchResourceRecursive(url, dispatcher, resourceOpts, siteConfig) {
84
+ const { visitedUrls } = siteConfig;
85
+ if (!visitedUrls.has(url)) {
86
+ visitedUrls.add(url); // Maintain a list of visited urls here to avoid potential infinite loops
87
+ // Skip urls with path segment variables (i.e. '/custom/:bar')
88
+ // Users can specify specific urls via the '_additionalRoutePaths' config property
89
+ if (url.indexOf('/:') !== -1) {
90
+ console.error('Skipped url with variable path segment: ' + url);
91
+ return;
92
+ }
93
+ // Generate resource
94
+ const context = await dispatcher.dispatchUrl(url, 'GET', siteConfig.locale);
95
+ const { resourceType } = resourceOpts;
96
+ // -- Routes (root resources) -------
97
+ if (resourceType === 'route') {
98
+ await this.handleHtmlResource(url, context, siteConfig, dispatcher);
99
+ // -- Process Assets (css, images, ...) && Resources (lwr-loader-shim ...)
100
+ }
101
+ else if (resourceType === 'asset' || resourceType === 'resource') {
102
+ await this.handleAssetOrResource(url, context, siteConfig);
103
+ // -- Import Metadata Mappings
104
+ }
105
+ else if (resourceType == 'mapping') {
106
+ await this.handleMappingResource(url, context, siteConfig, dispatcher);
107
+ // -- Process JS files
108
+ }
109
+ else if (resourceType === 'js') {
110
+ await this.handleJavascriptResource(url, context, siteConfig, dispatcher);
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Handle processing a returned javascript module or bundle and follow all returned references
116
+ * @param url - URL of the request javascript resource
117
+ * @param context - Response Context
118
+ * @param siteConfig - Global metadata about the site
119
+ * @param dispatcher - Network dispatcher
120
+ */
121
+ async handleJavascriptResource(url, context, siteConfig, dispatcher) {
122
+ const { outputDir } = siteConfig;
123
+ const normalizedUrl = decodeURIComponent(url);
124
+ createResourceDir(dirname(normalizedUrl), outputDir);
125
+ const ext = extname(normalizedUrl);
126
+ const fullPath = join(outputDir, `${normalizedUrl}${ext ? '' : '.js'}`);
127
+ await writeResponse(context, fullPath);
128
+ // Build up a list of dispatch requests to kick off in parallel
129
+ const dispatchRequests = [];
130
+ // Add URL re-writes for module redirects
131
+ if (normalizedUrl.indexOf('/s/') !== -1) {
132
+ const rewriteUrl = normalizedUrl.substring(0, normalizedUrl.indexOf('/s/'));
133
+ // Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6 -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
134
+ siteConfig.urlRewriteMap.set(rewriteUrl, normalizedUrl);
135
+ // Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr%2Fnavigation%2Fv%2F0_1_6 -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
136
+ siteConfig.urlRewriteMap.set(url.substring(0, url.indexOf('/s/')), normalizedUrl);
137
+ // TODO Redirect /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation -> /1/bundle/amd/l/en-US/bi/0/module/mi/lwr/navigation/v/0_1_6/s/{signature}
138
+ // Dynamic import example /1/bundle/amd/l/en-US/bi/0/module/mi/dynamic%2Fmodule?importer=examples%2Fhome%2Fv%2F0_1_6
139
+ }
140
+ // Recursively traverse dependencies
141
+ const moduleDefinition = context.fs?.metadata?.moduleDefinition; // LinkedModuleDefinition | BundleDefinition
142
+ if (moduleDefinition) {
143
+ // Imports
144
+ const imports = moduleDefinition.linkedModuleRecord?.imports || moduleDefinition.bundleRecord?.imports || [];
145
+ // /1/module/esm/0/l/en-US/mi/lwc
146
+ for (const importModule of imports) {
147
+ const jsUri = getSpecifier(importModule);
148
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
149
+ }
150
+ // Dynamic imports
151
+ const dynamicImports = moduleDefinition.linkedModuleRecord?.dynamicImports ||
152
+ moduleDefinition.bundleRecord?.dynamicImports ||
153
+ [];
154
+ for (const importModule of dynamicImports) {
155
+ const jsUri = getSpecifier(importModule);
156
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
157
+ }
158
+ }
159
+ // Bundles with unresolved module uris
160
+ const uris = context.fs?.metadata?.resolvedUris || [];
161
+ for (const jsUri of uris) {
162
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
163
+ }
164
+ // -- Dispatch dependencies
165
+ await Promise.all(dispatchRequests);
166
+ }
167
+ /**
168
+ * Handle processing a returned module URI mapping resource and follow all returned references
169
+ * @param url - URL of the request mapping resource
170
+ * @param context - Response Context
171
+ * @param siteConfig - Global metadata about the site
172
+ * @param dispatcher - Network dispatcher
173
+ */
174
+ async handleMappingResource(url, context, siteConfig, dispatcher) {
175
+ const { importMetadata: importMetatdata } = siteConfig;
176
+ const statusCode = context.response?.status;
177
+ // Received a server error
178
+ if (statusCode === 200) {
179
+ // Read JSON
180
+ const newImportMetadata = context.fs?.body;
181
+ if (!importMetatdata) {
182
+ console.warn('[WARN] Import metadata collector was never initialized');
183
+ return;
184
+ }
185
+ // Filter out and import metadata already included with the view
186
+ const filteredImportMetadata = importMetatdata.addAdditionalMetadata(newImportMetadata);
187
+ // Build up a list of dispatch requests to kick off in parallel
188
+ const dispatchRequests = [];
189
+ // Iterate through the import mappings and return request uris
190
+ for (const uri of Object.keys(filteredImportMetadata.imports)) {
191
+ dispatchRequests.push(this.dispatchResourceRecursive(uri, dispatcher, { resourceType: 'js' }, siteConfig));
192
+ }
193
+ // Wait for all requests to comeback
194
+ await Promise.all(dispatchRequests);
195
+ }
196
+ else {
197
+ const body = context.fs?.body;
198
+ console.warn(`[WARN] Failed to fetch ${url}: (${statusCode}) ${body}`);
199
+ }
200
+ }
201
+ /**
202
+ * Handle processing a returned HTML resource and process any provided view metadata
203
+ *
204
+ * @param url - URL of the requested HTML resource
205
+ * @param context - Response Context
206
+ * @param siteConfig - Global metadata about the site
207
+ * @param dispatcher - Network Dispatcher
208
+ */
209
+ async handleHtmlResource(url, context, siteConfig, dispatcher) {
210
+ const { outputDir } = siteConfig;
211
+ const dir = createResourceDir(url, outputDir);
212
+ const localeDir = createResourceDir(url, join(outputDir, siteConfig.locale));
213
+ // TODO: We assume HTML, revisit this in the future
214
+ // This fails for routes configured with non-html file extensions.
215
+ // Example Route: "path": "/mixed_templates.md", (Configured as '/mixed_templates' would work)
216
+ // "contentTemplate": "$contentDir/composed_markdown.md"
217
+ const filePath = join(dir, 'index.html');
218
+ const fileLocalePath = join(localeDir, 'index.html');
219
+ // Default Path (only write once)
220
+ // The default locale is english
221
+ if (siteConfig.locale.toLowerCase().startsWith('en')) {
222
+ await writeResponse(context, filePath);
223
+ }
224
+ // Language path (always write out)
225
+ createDir(localeDir);
226
+ await writeResponse(context, fileLocalePath);
227
+ // Get the metadata
228
+ const viewDefinition = context.fs?.metadata?.viewDefinition;
229
+ if (viewDefinition) {
230
+ // Process all references in the view
231
+ await this.handleViewDefinition(viewDefinition, siteConfig, dispatcher);
232
+ }
233
+ }
234
+ /**
235
+ * Handle processing a returned view definition by following all returned references
236
+ * @param viewDefinition - Metadata about everything directly referenced form the view
237
+ * @param siteConfig - Global metadata about the site
238
+ * @param dispatcher - Network dispatcher
239
+ */
240
+ async handleViewDefinition(viewDefinition, siteConfig, dispatcher) {
241
+ // Save mapping endpoint for future
242
+ siteConfig.endpoints = viewDefinition.viewRecord.endpoints;
243
+ // Save existing import metadata
244
+ if (viewDefinition.viewRecord.importMetadata) {
245
+ // Initialize import metadata collector
246
+ siteConfig.importMetadata = new ViewImportMetadataImpl(viewDefinition.viewRecord.importMetadata);
247
+ }
248
+ // Build up a list of dispatch requests to kick off in parallel
249
+ const dispatchRequests = [];
250
+ // Assets
251
+ const assets = viewDefinition.viewRecord.assetReferences || [];
252
+ for (const asset of assets) {
253
+ const assetUrl = asset.override?.uri || asset.url;
254
+ // skip empty asset urls / data urls (i.e. <img src="" /> <img src="data:image/png;base64, iVBORw0..." />)
255
+ if (assetUrl && !assetUrl.startsWith('data:')) {
256
+ dispatchRequests.push(this.dispatchResourceRecursive(assetUrl, dispatcher, { resourceType: 'asset', asset }, siteConfig));
257
+ }
258
+ }
259
+ // Custom Elements
260
+ const customElements = viewDefinition.viewRecord.customElements || [];
261
+ for (const customElement of customElements) {
262
+ const jsUris = Object.values(customElement.flatGraph.uriMap);
263
+ for (const jsUri of jsUris) {
264
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
265
+ }
266
+ }
267
+ // Bootstrap modules
268
+ if (viewDefinition.viewRecord.bootstrapModule) {
269
+ const boot = viewDefinition.viewRecord.bootstrapModule;
270
+ const jsUris = Object.values(boot.flatGraph.uriMap);
271
+ for (const jsUri of jsUris) {
272
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
273
+ }
274
+ }
275
+ // Bootstrap Resources
276
+ const bootstrapResources = viewDefinition.viewRecord.bootstrapModule?.resources || [];
277
+ for (const resource of bootstrapResources) {
278
+ // If resource is not inline download it for reference
279
+ if (!resource.inline) {
280
+ const resourceUri = resource.src || resource.specifier;
281
+ if (resourceUri) {
282
+ dispatchRequests.push(this.dispatchResourceRecursive(resourceUri, dispatcher, { resourceType: 'resource' }, siteConfig));
283
+ // Rough identify if this is the config Uri and add it to the site config to store additional import metadata
284
+ if (resourceUri.match(/\/application\/.*\/ai\/.*\/configuration/)) {
285
+ siteConfig.viewConfigPath = this.getResourcePathFromUrl(siteConfig, resourceUri);
286
+ }
287
+ }
288
+ else {
289
+ console.warn('Skipped inline bootstrap resource: %j', resource);
290
+ }
291
+ }
292
+ }
293
+ // Resources
294
+ const resources = viewDefinition.viewRecord.resources || [];
295
+ for (const resource of resources) {
296
+ const resourceUri = resource.src || resource.specifier || '';
297
+ if (resourceUri.startsWith('/')) {
298
+ dispatchRequests.push(this.dispatchResourceRecursive(resourceUri, dispatcher, { resourceType: 'resource' }, siteConfig));
299
+ }
300
+ else {
301
+ console.warn('Skipped resource: %j', resource);
302
+ }
303
+ }
304
+ // -- Dispatch dependencies
305
+ await Promise.all(dispatchRequests);
306
+ }
307
+ async dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig) {
308
+ if (jsUri.startsWith('/')) {
309
+ await this.dispatchResourceRecursive(jsUri, dispatcher, { resourceType: 'js' }, siteConfig);
310
+ }
311
+ else {
312
+ const supportsFingerprints = siteConfig.experimentalFeatures?.ENABLE_FINGERPRINTS;
313
+ if (supportsFingerprints) {
314
+ // Download mapping
315
+ const mappingEndpoint = siteConfig.endpoints?.uris?.mapping;
316
+ if (mappingEndpoint) {
317
+ const mappingURL = siteConfig.endpoints?.uris?.mapping + encodeURIComponent(jsUri);
318
+ await this.dispatchResourceRecursive(mappingURL, dispatcher, { resourceType: 'mapping' }, siteConfig);
319
+ }
320
+ else {
321
+ console.warn('[WARN] Unable to fetch mapping for bare specifier or variable dynamic import: "' +
322
+ jsUri +
323
+ '"');
324
+ }
325
+ }
326
+ else {
327
+ console.warn('[WARN] Unable to resolve bare specifier or variable dynamic import: "' + jsUri + '"');
328
+ }
329
+ }
330
+ }
331
+ /**
332
+ * Handle processing a returned asset or resource
333
+ * @param url - URL of the requested asset or resource
334
+ * @param context - Response Context
335
+ * @param siteConfig - Global metadata about the site
336
+ */
337
+ async handleAssetOrResource(url, context, siteConfig) {
338
+ const fullPath = this.getResourcePathFromUrl(siteConfig, url);
339
+ await writeResponse(context, fullPath);
340
+ }
341
+ getResourcePathFromUrl(siteConfig, url) {
342
+ const { outputDir } = siteConfig;
343
+ const normalizedUrl = decodeURIComponent(url);
344
+ createResourceDir(dirname(normalizedUrl), outputDir);
345
+ const fullPath = join(outputDir, normalizedUrl);
346
+ return fullPath;
206
347
  }
207
348
  /**
208
349
  * Write out redirect mapping files for static hosting services like netlify. Examples for why this is needed:
@@ -212,7 +353,7 @@ export default class SiteGenerator {
212
353
  * @param outputDir
213
354
  * @param urlRewriteMap
214
355
  */
215
- writeUrlMappings(outputDir, urlRewriteMap) {
356
+ writeNetlifyRedirectConfig(outputDir, urlRewriteMap) {
216
357
  const serveJsonPath = join(outputDir, 'serve.json');
217
358
  const _redirectsPath = join(outputDir, '_redirects');
218
359
  if (fs.existsSync(_redirectsPath)) {
@@ -259,5 +400,97 @@ export default class SiteGenerator {
259
400
  }
260
401
  }
261
402
  }
403
+ /**
404
+ * Create a new site config for the current view
405
+ */
406
+ createSiteConfig(outputDir, locale, urlRewriteMap) {
407
+ const experimentalFeatures = this.filterExperimentalFeatures();
408
+ return {
409
+ outputDir,
410
+ visitedUrls: new Set(),
411
+ locale,
412
+ urlRewriteMap,
413
+ // Only include ENABLE_FINGERPRINTS if true
414
+ ...experimentalFeatures,
415
+ };
416
+ }
417
+ filterExperimentalFeatures() {
418
+ if (getExperimentalFeatures().ENABLE_FINGERPRINTS) {
419
+ return { experimentalFeatures: { ENABLE_FINGERPRINTS: true } };
420
+ }
421
+ else {
422
+ return undefined;
423
+ }
424
+ }
425
+ /**
426
+ * Add any additional import metadata collected during static site generation to the Client Bootstrap Config for this view.
427
+ */
428
+ addAdditionalImportMetadataToViewConfig(siteConfig) {
429
+ const supportsFingerprints = siteConfig.experimentalFeatures?.ENABLE_FINGERPRINTS;
430
+ const additionalImportMetadata = siteConfig?.importMetadata?.getAdditionalImportMetadata();
431
+ if (supportsFingerprints &&
432
+ siteConfig.viewConfigPath &&
433
+ additionalImportMetadata?.imports &&
434
+ Object.keys(additionalImportMetadata.imports).length > 0) {
435
+ const imports = additionalImportMetadata.imports
436
+ ? JSON.stringify(additionalImportMetadata.imports)
437
+ : '{}';
438
+ const initImports = `if (!globalThis.LWR.imports) { globalThis.LWR.imports = {}; }`;
439
+ const mergeImports = `Object.assign(globalThis.LWR.imports, ${imports})`;
440
+ const index = additionalImportMetadata.index
441
+ ? JSON.stringify(additionalImportMetadata.index)
442
+ : '{}';
443
+ const initIndex = `if (!globalThis.LWR.index) { globalThis.LWR.index = {}; }`;
444
+ const mergeIndex = `Object.assign(globalThis.LWR.index, ${index})`;
445
+ fs.appendFileSync(siteConfig.viewConfigPath, `\n// Appended by Static Site Generator\n${initImports}\n${mergeImports}\n${initIndex}\n${mergeIndex}\n`);
446
+ }
447
+ }
448
+ }
449
+ // Class used to track import metadata for a view
450
+ export class ViewImportMetadataImpl {
451
+ constructor(existingImportMetadata, additionalImportMetadata) {
452
+ this.existing = existingImportMetadata;
453
+ this.additional = additionalImportMetadata || { imports: {}, index: {} };
454
+ }
455
+ /**
456
+ * Get the additional import metadata collected while generating this view
457
+ * @returns
458
+ */
459
+ getAdditionalImportMetadata() {
460
+ return this.additional;
461
+ }
462
+ /**
463
+ * Adds any new imports found to the additional metadata map. Returns a filtered
464
+ * map of imports not in the initial view
465
+ */
466
+ addAdditionalMetadata(newMetadata) {
467
+ const filteredImports = this.filterMetadata(newMetadata);
468
+ this.mergeImportMetadata(this.additional, filteredImports);
469
+ return filteredImports;
470
+ }
471
+ /**
472
+ * Filter out any existing import metadata the would have already been sent back with the view from set of additional metadata detected
473
+ */
474
+ filterMetadata(newMetadata) {
475
+ // Filter Imports
476
+ const importsArray = Object.entries(newMetadata.imports);
477
+ const filteredImports = importsArray.filter(([key]) => !this.existing.imports[key]);
478
+ const imports = Object.fromEntries(filteredImports);
479
+ // Filter Index
480
+ const indexArray = Object.entries(newMetadata.index || {});
481
+ const filteredIndex = indexArray.filter(([key]) => !this.existing.index || !this.existing.index[key]);
482
+ const index = Object.fromEntries(filteredIndex);
483
+ return {
484
+ imports,
485
+ index,
486
+ };
487
+ }
488
+ /**
489
+ * Merge new import metadata into target import metadata
490
+ */
491
+ mergeImportMetadata(targetImportMetdadata, newImportMetadata) {
492
+ Object.assign(targetImportMetdadata.imports, newImportMetadata.imports);
493
+ Object.assign(targetImportMetdadata.index, newImportMetadata.index);
494
+ }
262
495
  }
263
496
  //# sourceMappingURL=static-generation.js.map
@@ -1,6 +1,7 @@
1
+ import { Endpoints, ExperimentalFeatures, ImportMetadata } from '@lwrjs/types';
1
2
  import { RenderedAssetReference } from '@lwrjs/types';
2
3
  export interface BaseResourceContextOpts {
3
- resourceType: 'route' | 'asset' | 'js' | 'resource';
4
+ resourceType: 'route' | 'asset' | 'js' | 'resource' | 'mapping';
4
5
  }
5
6
  export interface RouteResourceOpts extends BaseResourceContextOpts {
6
7
  resourceType: 'route';
@@ -15,11 +16,30 @@ export interface JsResourceOpts extends BaseResourceContextOpts {
15
16
  export interface ResResourceOpts extends BaseResourceContextOpts {
16
17
  resourceType: 'resource';
17
18
  }
18
- export declare type ResourceContextOpts = RouteResourceOpts | AssetResourceOpts | JsResourceOpts | ResResourceOpts;
19
+ export interface MappingResourceOpts extends BaseResourceContextOpts {
20
+ resourceType: 'mapping';
21
+ }
22
+ export declare type ResourceContextOpts = RouteResourceOpts | AssetResourceOpts | JsResourceOpts | ResResourceOpts | MappingResourceOpts;
23
+ export interface ViewImportMetadata {
24
+ /**
25
+ * Get the additional import metadata collected while generating this view
26
+ * @returns
27
+ */
28
+ getAdditionalImportMetadata(): ImportMetadata;
29
+ /**
30
+ * Adds any new imports found to the additional metadata map. Returns a filtered
31
+ * map of imports not in the initial view
32
+ */
33
+ addAdditionalMetadata(newMetadata: ImportMetadata): ImportMetadata;
34
+ }
19
35
  export interface SiteConfig {
20
36
  outputDir: string;
21
37
  visitedUrls: Set<string>;
22
38
  locale: string;
23
39
  urlRewriteMap: Map<string, string>;
40
+ endpoints?: Endpoints;
41
+ importMetadata?: ViewImportMetadata;
42
+ viewConfigPath?: string;
43
+ experimentalFeatures?: ExperimentalFeatures;
24
44
  }
25
45
  //# sourceMappingURL=types.d.ts.map
@@ -1,7 +1,10 @@
1
+ /// <reference types="node" />
2
+ import http from 'http';
1
3
  import { LwrDispatcher, FsContext } from '@lwrjs/types';
2
4
  export default class NetworkDispatcher implements LwrDispatcher {
3
5
  port: number;
4
6
  internalRequestKey: string;
7
+ pool: http.Agent;
5
8
  constructor(port: number, internalRequestKey: string);
6
9
  dispatchUrl(url: string, method: string, lang: string): Promise<FsContext>;
7
10
  }