@lwrjs/core 0.12.0-alpha.2 → 0.12.0-alpha.21

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/build/es/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getFeatureFlags, DEFAULT_LWR_BOOTSTRAP_CONFIG } from '@lwrjs/shared-utils';
2
2
  import { createInternalServer } from '@lwrjs/server';
3
3
  import { LwrServerError, createSingleDiagnosticError, descriptions, logger } from '@lwrjs/diagnostics';
4
- import { loadConfig, executeConfigHooks, executeStartHooks, executeInstrumentationHooks, } from '@lwrjs/config';
4
+ import { loadConfig, executeConfigHooks, executeStartHooks, executeInstrumentationHooks, executeContextHooks, } from '@lwrjs/config';
5
5
  import { loadHooks, loadServices, loadRouteHandlers } from '@lwrjs/config/modules';
6
6
  import SiteGenerator from './tools/static-generation.js';
7
7
  import { warmupServer } from './tools/server-warmup.js';
@@ -47,12 +47,18 @@ async function initContext(appConfig, runtimeEnvironment, globalData) {
47
47
  const skipValidation = true; // skip for config hook, since `executeStartHooks` hook will validate
48
48
  await executeConfigHooks(hooks, appConfig, runtimeEnvironment, globalData, skipValidation);
49
49
  executeInstrumentationHooks(hooks);
50
- executeStartHooks(hooks, appConfig, runtimeEnvironment);
51
50
  }
52
51
  // load all configurable modules
53
52
  const services = await loadServices(appConfig);
54
53
  // create all framework components(ie. registries)
55
54
  const serverContext = getTracer().trace({ name: CoreSpan.CreateServerContext }, () => createServerContext(appConfig, runtimeEnvironment, globalData));
55
+ // set routes on server context
56
+ const routeHandlers = await loadRouteHandlers(appConfig);
57
+ serverContext.routeHandlers = routeHandlers;
58
+ if (hooks.length) {
59
+ await executeContextHooks(hooks, serverContext);
60
+ executeStartHooks(hooks, appConfig, runtimeEnvironment);
61
+ }
56
62
  // create public subset of configurations
57
63
  const providerContext = createProviderContext(serverContext);
58
64
  const { moduleRegistry, assetRegistry, resourceRegistry, viewRegistry, moduleBundler } = serverContext;
@@ -82,30 +88,26 @@ async function initContext(appConfig, runtimeEnvironment, globalData) {
82
88
  viewRegistry.addViewTransformers(viewTransformers);
83
89
  // invoke async initialization
84
90
  await serverContext.viewRegistry.initializeViewProviders();
85
- // set routes on server context
86
- const routeHandlers = await loadRouteHandlers(appConfig);
87
- serverContext.routeHandlers = routeHandlers;
88
91
  return serverContext;
89
92
  }
90
93
  export class LwrApp {
91
- constructor(config = {}) {
94
+ constructor(configs) {
92
95
  this.initialized = false;
93
96
  const span = getTracer().startSpan({ name: CoreSpan.CreateServer });
94
- const { appConfig, runtimeEnvironment, globalData } = loadConfig(config);
97
+ const { appConfig, runtimeEnvironment, globalData } = configs;
95
98
  this.config = appConfig;
96
99
  this.runtimeEnvironment = runtimeEnvironment;
97
100
  this.globalData = globalData;
98
- const { basePath } = this.config;
99
- this.app = createInternalServer(this.config.serverType, { basePath });
101
+ const { basePath, serverType } = this.config;
102
+ this.serverType = serverType;
103
+ this.app = createInternalServer(serverType, { basePath });
100
104
  this.server = this.app.createHttpServer();
105
+ this.use = this.app.use.bind(this.app);
106
+ this.all = this.app.all.bind(this.app);
107
+ this.get = this.app.get.bind(this.app);
108
+ this.post = this.app.post.bind(this.app);
101
109
  span.end();
102
110
  }
103
- setConfig(config) {
104
- const { appConfig, runtimeEnvironment, globalData } = loadConfig(config);
105
- this.config = appConfig;
106
- this.runtimeEnvironment = runtimeEnvironment;
107
- this.globalData = globalData;
108
- }
109
111
  getConfig() {
110
112
  return this.config;
111
113
  }
@@ -163,26 +165,17 @@ export class LwrApp {
163
165
  });
164
166
  });
165
167
  }
166
- async close() {
167
- this.server?.close && (await this.server.close());
168
+ close() {
169
+ this.server?.close && this.server.close();
168
170
  }
169
171
  // Get the underlying server (e.g. express, koa...)
170
172
  getInternalServer() {
171
173
  return this.app.getImpl();
172
174
  }
173
- // Return the public server interface which is compatible with all server types
174
- getServer() {
175
- return {
176
- use: this.app.use.bind(this.app),
177
- all: this.app.all.bind(this.app),
178
- get: this.app.get.bind(this.app),
179
- post: this.app.post.bind(this.app),
180
- getRegexWildcard: this.app.getRegexWildcard.bind(this.app),
181
- };
182
- }
183
175
  }
184
176
  export function createServer(config) {
185
- return new LwrApp(config);
177
+ const configs = loadConfig(config);
178
+ return new LwrApp(configs);
186
179
  }
187
180
  export async function generateStaticSite(config) {
188
181
  config = config || {};
@@ -0,0 +1,3 @@
1
+ import type { HandlerContext, ViewRequest, ViewResponse } from '@lwrjs/types';
2
+ export default function siteInfoHandler(_request: ViewRequest, context: HandlerContext): Promise<ViewResponse>;
3
+ //# sourceMappingURL=route-handler.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { LWC_VERSION, LWR_VERSION, PWA_KIT_RUNTIME_VERSION, NODE_VERSION } from '@lwrjs/config';
2
+ import fs from 'fs';
3
+ const baseHtml = `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
8
+ <title>Site Information</title>
9
+ <style>
10
+ body {
11
+ font-family: Arial, sans-serif;
12
+ margin: 0;
13
+ padding: 20px;
14
+ background-color: #f9f9f9;
15
+ color: #333;
16
+ display: flex;
17
+ }
18
+
19
+ .container {
20
+ max-width: 800px;
21
+ width: 100%;
22
+ }
23
+
24
+ h1, h2 {
25
+ color: #555;
26
+ margin-bottom: 16px;
27
+ }
28
+
29
+ ul {
30
+ list-style-type: none;
31
+ padding: 0;
32
+ }
33
+
34
+ li {
35
+ background-color: #fff;
36
+ border: 1px solid #ddd;
37
+ margin-bottom: 10px;
38
+ padding: 10px;
39
+ border-radius: 4px;
40
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
41
+ display: flex;
42
+ }
43
+
44
+ .key {
45
+ margin-right: 10px;
46
+ color: #777;
47
+ font-weight: normal;
48
+ min-width: 200px;
49
+ }
50
+
51
+ .val {
52
+ font-weight: bold;
53
+ color: steelblue;
54
+ font-size: 1.1em;
55
+ flex-grow: 1;
56
+ text-align: left;
57
+ }
58
+
59
+ .data-container {
60
+ padding: 20px;
61
+ border: 1px solid #333;
62
+ border-radius: 5px;
63
+ white-space: pre;
64
+ font-family: monospace;
65
+ text-align: justify;
66
+ };
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div class="container">
71
+ <h1>Site Information</h1>
72
+ <h2>Environment</h2>
73
+ {{ versionInfo }}
74
+ <h2>Metadata</h2>
75
+ {{ metadata }}
76
+ </div>
77
+ </body>
78
+ </html>
79
+ `;
80
+ export default async function siteInfoHandler(_request, context) {
81
+ const versionInfo = {
82
+ 'LWR Version': LWR_VERSION,
83
+ 'LWC Version': LWC_VERSION,
84
+ 'Node Version': NODE_VERSION,
85
+ 'PWA Kit Runtime Version': PWA_KIT_RUNTIME_VERSION,
86
+ };
87
+ let versionInfoString = '<ul>';
88
+ for (const key in versionInfo) {
89
+ if (versionInfo[key] !== 'note-provided') {
90
+ versionInfoString += `<li><span class="key">${key}:</span><span class="val">${versionInfo[key]}</span></li>\n`;
91
+ }
92
+ }
93
+ versionInfoString += '</ul>';
94
+ // Get any provided metadata from site/.metadata/runtime-info.json
95
+ const providedMetadataPath = `${context.rootDir}/site/.metadata/runtime-info.json`;
96
+ let providedMetadata = {};
97
+ if (fs.existsSync(providedMetadataPath)) {
98
+ providedMetadata = JSON.parse(fs.readFileSync(providedMetadataPath, 'utf-8'));
99
+ }
100
+ let providedInfoString = '<ul>';
101
+ for (const key in providedMetadata) {
102
+ if (providedMetadata[key] !== 'note-provided') {
103
+ const value = typeof providedMetadata[key] === 'object'
104
+ ? `<div class="data-container val">${JSON.stringify(providedMetadata[key], null, 2)}</div>`
105
+ : `<span class="val">${providedMetadata[key]}</span>`;
106
+ providedInfoString += `<li><span class="key">${key}:</span>${value}</li>\n`;
107
+ }
108
+ }
109
+ providedInfoString += '</ul>';
110
+ let infoHtml = baseHtml;
111
+ infoHtml = infoHtml.replace('{{ versionInfo }}', versionInfoString);
112
+ infoHtml = infoHtml.replace('{{ metadata }}', providedInfoString);
113
+ return {
114
+ body: infoHtml,
115
+ headers: {
116
+ 'content-type': `text/html; charset=utf-8`,
117
+ },
118
+ cache: {
119
+ ttl: '200s',
120
+ },
121
+ };
122
+ }
123
+ //# sourceMappingURL=route-handler.js.map
@@ -3,6 +3,30 @@ import { DiagnosticsError } from '@lwrjs/diagnostics';
3
3
  import { RequestHandlerSpan, getTracer } from '@lwrjs/instrumentation';
4
4
  import { getAssetIdentity } from './utils/identity.js';
5
5
  import { handleErrors } from './utils/error-handling.js';
6
+ import fs from 'fs-extra';
7
+ export function assetMiddleware(app, context) {
8
+ const paths = context.appConfig.assets.map((config) => {
9
+ const assetDirConfig = config;
10
+ let urlPath = config.urlPath;
11
+ // If this is a root config add a /:filename path to match any file in the root. The middleware will fall through if there is no match.
12
+ if (assetDirConfig.root) {
13
+ urlPath = '/:filename';
14
+ }
15
+ else if (assetDirConfig.dir) {
16
+ urlPath += app.getRegexWildcard();
17
+ }
18
+ return urlPath;
19
+ });
20
+ app.get([
21
+ // De-dupe paths (i.e. root path may have been added more than once)
22
+ ...[...new Set(paths)],
23
+ '/:apiVersion/:assetType(asset|content-asset)/:immutable?/s/:signature/' + app.getRegexWildcard(),
24
+ '/:apiVersion/:assetType(asset|content-asset)/:immutable?/' + app.getRegexWildcard(),
25
+ ], handleErrors(createAssetMiddleware(context)));
26
+ }
27
+ /**
28
+ * Create middleware function to handle get assets requests
29
+ */
6
30
  function createAssetMiddleware(context) {
7
31
  const { assetRegistry, runtimeEnvironment: { basePath }, } = context;
8
32
  return async (req, res, next) => {
@@ -12,24 +36,22 @@ function createAssetMiddleware(context) {
12
36
  assetId.specifier = path.join(basePath, assetId.specifier);
13
37
  }
14
38
  try {
15
- // Redirect if this is an external asset
39
+ let asset;
16
40
  const assetUri = await assetRegistry.resolveAssetUri(assetId, runtimeEnvironment);
41
+ // Check if this asset is available externally
17
42
  if (assetUri.external) {
18
- res.set({
19
- Location: assetUri.uri,
20
- 'cache-control': 'public, max-age=60',
21
- });
22
- res.sendStatus(302);
23
- return;
43
+ // This asset is marked external but hit the middleware anyway
44
+ // Check if we have this file locally, if not, send a 302 to
45
+ // redirect to the external URL
46
+ asset = await getAssetDefinition(assetId, req, assetRegistry, signature, runtimeEnvironment);
47
+ // Verify the content actually exists locally
48
+ if (!fs.existsSync(asset.entry) && assetUri.uri.startsWith('/mobify/bundle')) {
49
+ return sendRedirect(res, assetUri);
50
+ }
51
+ }
52
+ else {
53
+ asset = await getAssetDefinition(assetId, req, assetRegistry, signature, runtimeEnvironment);
24
54
  }
25
- const asset = await getTracer().trace({
26
- name: RequestHandlerSpan.GetAsset,
27
- attributes: {
28
- specifier: assetId.specifier,
29
- },
30
- }, () => {
31
- return assetRegistry.getAsset({ ...assetId, signature }, runtimeEnvironment, req.isSiteGeneration());
32
- });
33
55
  if (req.isSiteGeneration()) {
34
56
  res.setSiteGenerationMetadata({ asset });
35
57
  }
@@ -58,24 +80,29 @@ function createAssetMiddleware(context) {
58
80
  }
59
81
  };
60
82
  }
61
- export function assetMiddleware(app, context) {
62
- const paths = context.appConfig.assets.map((config) => {
63
- const assetDirConfig = config;
64
- let urlPath = config.urlPath;
65
- // If this is a root config add a /:filename path to match any file in the root. The middleware will fall through if there is no match.
66
- if (assetDirConfig.root) {
67
- urlPath = '/:filename';
68
- }
69
- else if (assetDirConfig.dir) {
70
- urlPath += app.getRegexWildcard();
71
- }
72
- return urlPath;
83
+ /**
84
+ * Get the asset definition if asset is local
85
+ */
86
+ async function getAssetDefinition(assetId, req, assetRegistry, signature, runtimeEnvironment) {
87
+ const asset = await getTracer().trace({
88
+ name: RequestHandlerSpan.GetAsset,
89
+ attributes: {
90
+ specifier: assetId.specifier,
91
+ url: req.originalUrl,
92
+ },
93
+ }, () => {
94
+ return assetRegistry.getAsset({ ...assetId, signature }, runtimeEnvironment, req.isSiteGeneration());
73
95
  });
74
- app.get([
75
- // De-dupe paths (i.e. root path may have been added more than once)
76
- ...[...new Set(paths)],
77
- '/:apiVersion/:assetType(asset|content-asset)/:immutable?/s/:signature/' + app.getRegexWildcard(),
78
- '/:apiVersion/:assetType(asset|content-asset)/:immutable?/' + app.getRegexWildcard(),
79
- ], handleErrors(createAssetMiddleware(context)));
96
+ return asset;
97
+ }
98
+ /**
99
+ * Send a redirect (302) response
100
+ */
101
+ function sendRedirect(res, assetUri) {
102
+ res.set({
103
+ Location: assetUri.uri,
104
+ 'cache-control': 'public, max-age=60',
105
+ });
106
+ res.sendStatus(302);
80
107
  }
81
108
  //# sourceMappingURL=asset-middleware.js.map
@@ -20,6 +20,12 @@ function createBundleMiddleware(context) {
20
20
  res.send(descriptions.UNRESOLVABLE.INVALID_JSON().message);
21
21
  return;
22
22
  }
23
+ if (!req.validateApiVersion(appConfig)) {
24
+ res.status(400);
25
+ res.send(descriptions.UNRESOLVABLE.INVALID_API_VERSION(req.params.apiVersion, appConfig.apiVersion)
26
+ .message);
27
+ return;
28
+ }
23
29
  const { runtimeEnvironment, runtimeParams } = req.getRuntimeContext(defaultRuntimeEnvironment);
24
30
  const importer = req.query.importer
25
31
  ? await getRequestImporter(req, moduleRegistry, runtimeParams)
@@ -34,6 +40,7 @@ function createBundleMiddleware(context) {
34
40
  name: RequestHandlerSpan.GetBundle,
35
41
  attributes: {
36
42
  specifier: moduleId.specifier,
43
+ url: req.originalUrl,
37
44
  },
38
45
  }, () => {
39
46
  return moduleBundler.getModuleBundle(moduleId,
@@ -15,6 +15,9 @@ function createMappingMiddleware(context) {
15
15
  const { moduleIds } = getMappingIdentity(req);
16
16
  const importMetadata = await getTracer().trace({
17
17
  name: RequestHandlerSpan.GetMapping,
18
+ attributes: {
19
+ url: req.originalUrl,
20
+ },
18
21
  }, () => {
19
22
  return getImportMetadataMappings(moduleIds, runtimeEnvironment, runtimeParams, moduleRegistry, moduleBundler);
20
23
  });
@@ -19,6 +19,12 @@ function createModuleMiddleware(context) {
19
19
  res.send(descriptions.UNRESOLVABLE.INVALID_JSON().message);
20
20
  return;
21
21
  }
22
+ if (!req.validateApiVersion(appConfig)) {
23
+ res.status(400);
24
+ res.send(descriptions.UNRESOLVABLE.INVALID_API_VERSION(req.params.apiVersion, appConfig.apiVersion)
25
+ .message);
26
+ return;
27
+ }
22
28
  const { runtimeEnvironment, runtimeParams } = req.getRuntimeContext(defaultRuntimeEnvironment);
23
29
  const importer = req.query.importer
24
30
  ? await getRequestImporter(req, moduleRegistry, runtimeParams)
@@ -32,6 +38,7 @@ function createModuleMiddleware(context) {
32
38
  name: RequestHandlerSpan.GetModule,
33
39
  attributes: {
34
40
  specifier: moduleId.specifier,
41
+ url: req.originalUrl,
35
42
  },
36
43
  }, () => {
37
44
  return moduleRegistry.getLinkedModule(moduleId,
@@ -7,15 +7,21 @@
7
7
  *
8
8
  */
9
9
  import { logger } from '@lwrjs/diagnostics';
10
+ import { parseRequestDepthHeader } from '@lwrjs/shared-utils';
10
11
  const MRT_REQUEST_CLASS = 'X-Mobify-Request-Class';
11
12
  const MRT_REQUEST_CLASS_KEY = MRT_REQUEST_CLASS.toLowerCase();
12
13
  export function requestProcessorMiddleware(app, context) {
13
14
  const { basePath } = context.runtimeEnvironment;
14
- app.use(async (req, _res, next) => {
15
+ app.use(async (req, res, next) => {
15
16
  let requestClass;
17
+ let requestDepth;
16
18
  if (req.headers) {
17
19
  // If debug print log all the headers
18
20
  if (logger.isDebugEnabled()) {
21
+ logger.debug({
22
+ label: `request-processor-middleware`,
23
+ message: `Request: ${req.originalUrl}`,
24
+ });
19
25
  // Loop through and print each header
20
26
  for (const headerName in req.headers) {
21
27
  logger.debug({
@@ -25,6 +31,16 @@ export function requestProcessorMiddleware(app, context) {
25
31
  }
26
32
  }
27
33
  requestClass = req.headers[MRT_REQUEST_CLASS_KEY];
34
+ requestDepth = parseRequestDepthHeader(req.headers);
35
+ }
36
+ // TODO Clean up once we have a long term solution
37
+ if (requestDepth && requestDepth > 0) {
38
+ logger.error({
39
+ label: 'request-processor-middleware',
40
+ message: `Lambda SSR request cycle detected: ${req.originalUrl}`,
41
+ });
42
+ // Return 400 Too Many Requests status
43
+ return res.status(400).send('Request depth limit reached');
28
44
  }
29
45
  if (req.headers && typeof requestClass === 'string') {
30
46
  const parsedRequestClass = parseRequestClass(requestClass);
@@ -8,7 +8,7 @@ function createReturnStatus(error, url) {
8
8
  if (error instanceof LwrUnresolvableError) {
9
9
  return { status: 404, message: error.message };
10
10
  }
11
- return { status: 500, message: descriptions.UNRESOLVABLE.SERVER_ERROR(url).message };
11
+ return { status: 500, message: descriptions.SERVER.SERVER_ERROR(url).message };
12
12
  }
13
13
  export function handleErrors(middleware) {
14
14
  return async (req, res, next) => {
@@ -44,7 +44,11 @@ export function getResourceIdentity(req) {
44
44
  }
45
45
  export function getAssetIdentity(req) {
46
46
  const { signature, immutable, assetType: type } = req.params;
47
- const specifier = type ? '/' + req.params[0] : req.originalUrl.split('?')[0];
47
+ const specifier = type
48
+ ? process.platform === 'win32'
49
+ ? req.params[0]
50
+ : '/' + req.params[0]
51
+ : req.originalUrl.split('?')[0];
48
52
  if (validateSpecifier(specifier) === false) {
49
53
  throw createSingleDiagnosticError({
50
54
  description: descriptions.UNRESOLVABLE.INVALID_SPECIFIER(specifier),
@@ -40,13 +40,16 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
40
40
  const resolve = req.isJsonRequest() ? viewHandler.getViewJson : viewHandler.getViewContent;
41
41
  let viewResponse;
42
42
  let resolvedRoute;
43
+ let traceId;
43
44
  try {
44
45
  viewResponse = await getTracer().trace({
45
46
  name: RequestHandlerSpan.GetView,
46
47
  attributes: {
47
48
  view: route.id,
49
+ url: req.originalUrl,
48
50
  },
49
- }, () => {
51
+ }, (span) => {
52
+ traceId = span.traceId;
50
53
  return resolve.call(viewHandler, viewRequest, route, runtimeEnvironment, runtimeParams);
51
54
  });
52
55
  resolvedRoute = route;
@@ -84,6 +87,9 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
84
87
  res.setHeader('cache-control', `public, max-age=${cacheTtl}`);
85
88
  }
86
89
  }
90
+ if (traceId?.length) {
91
+ res.setHeader('x-trace-id', traceId);
92
+ }
87
93
  const status = resolvedRoute.status || viewResponse.status || 200;
88
94
  res.status(status);
89
95
  res.send(viewResponse.body);
@@ -40,6 +40,10 @@ export default class SiteGenerator {
40
40
  * @param dispatcher - Network dispatcher
41
41
  */
42
42
  private handleJavascriptResource;
43
+ /**
44
+ * If this is a file based external copy it to the site folder and add it to the bundle metadata
45
+ */
46
+ private handleExternalBundle;
43
47
  private addBundleToSiteMetadata;
44
48
  private addResourceToSiteMetadata;
45
49
  private addAssetToSiteMetadata;
@@ -1,12 +1,13 @@
1
1
  import { performance } from 'perf_hooks';
2
2
  import { logger } from '@lwrjs/diagnostics';
3
- import { getSpecifier, getFeatureFlags, hashContent, isSelfUrl, getModuleUriPrefix, getMappingUriPrefix, isExternalUrl, mimeLookup, getViewUri, sortLocalesByFallback, } from '@lwrjs/shared-utils';
3
+ import { getSpecifier, getFeatureFlags, hashContent, isSelfUrl, getModuleUriPrefix, getMappingUriPrefix, isExternalUrl, mimeLookup, getViewUri, sortLocalesByFallback, VERSION_NOT_PROVIDED, PROTOCOL_FILE, normalizeFromFileURL, } from '@lwrjs/shared-utils';
4
4
  import { SiteMetadataImpl } from '@lwrjs/static/site-metadata';
5
5
  import { join, dirname, extname, normalize } from 'path';
6
6
  import fs from 'fs-extra';
7
7
  import { writeResponse } from './utils/stream.js';
8
8
  import { createResourceDir } from './utils/dir.js';
9
9
  import { getRuntimeEnvironment } from '@lwrjs/config';
10
+ import { fileURLToPath } from 'url';
10
11
  export default class SiteGenerator {
11
12
  /**
12
13
  * Build a static site in the configured directory
@@ -198,6 +199,14 @@ export default class SiteGenerator {
198
199
  */
199
200
  async handleJavascriptResource(url, context, siteConfig, dispatcher) {
200
201
  const { outputDir } = siteConfig;
202
+ const moduleDefinition = context.fs?.metadata?.moduleDefinition; // LinkedModuleDefinition | BundleDefinition
203
+ const externals = moduleDefinition?.config?.external || {};
204
+ const siteBundles = siteConfig?.siteMetadata?.getSiteBundles()?.bundles;
205
+ const specifier = moduleDefinition?.specifier;
206
+ if (externals && siteBundles && externals[specifier]) {
207
+ this.handleExternalBundle(specifier, siteBundles, externals, outputDir);
208
+ return;
209
+ }
201
210
  const normalizedUrl = decodeURIComponent(url);
202
211
  createResourceDir(dirname(normalizedUrl), outputDir);
203
212
  const ext = extname(normalizedUrl);
@@ -223,26 +232,33 @@ export default class SiteGenerator {
223
232
  siteConfig.urlRewriteMap.set(url.substring(0, url.indexOf('%2Fv%2F')), normalizedUrl);
224
233
  }
225
234
  // Recursively traverse dependencies
226
- const moduleDefinition = context.fs?.metadata?.moduleDefinition; // LinkedModuleDefinition | BundleDefinition
227
235
  if (moduleDefinition) {
228
236
  // Imports
229
237
  const imports = moduleDefinition.linkedModuleRecord?.imports || moduleDefinition.bundleRecord?.imports || [];
230
238
  // /1/module/esm/0/l/en-US/mi/lwc
231
239
  for (const importModule of imports) {
232
- const jsUri = importModule.specifier.startsWith('/')
233
- ? importModule.specifier
234
- : getSpecifier(importModule);
235
- dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
240
+ const specifier = importModule.specifier;
241
+ if (externals[specifier]) {
242
+ this.handleExternalBundle(specifier, siteBundles, externals, outputDir);
243
+ }
244
+ else {
245
+ const jsUri = specifier.startsWith('/') ? specifier : getSpecifier(importModule);
246
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
247
+ }
236
248
  }
237
249
  // Dynamic imports
238
250
  const dynamicImports = moduleDefinition.linkedModuleRecord?.dynamicImports ||
239
251
  moduleDefinition.bundleRecord?.dynamicImports ||
240
252
  [];
241
253
  for (const importModule of dynamicImports) {
242
- const jsUri = importModule.specifier.startsWith('/')
243
- ? importModule.specifier
244
- : getSpecifier(importModule);
245
- dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
254
+ const specifier = importModule.specifier;
255
+ if (externals[specifier]) {
256
+ this.handleExternalBundle(specifier, siteBundles, externals, outputDir);
257
+ }
258
+ else {
259
+ const jsUri = specifier.startsWith('/') ? specifier : getSpecifier(importModule);
260
+ dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
261
+ }
246
262
  }
247
263
  // If this is a bundle add it to the bundle metadata
248
264
  if (moduleDefinition.bundleRecord) {
@@ -257,6 +273,25 @@ export default class SiteGenerator {
257
273
  // -- Dispatch dependencies
258
274
  await Promise.all(dispatchRequests);
259
275
  }
276
+ /**
277
+ * If this is a file based external copy it to the site folder and add it to the bundle metadata
278
+ */
279
+ handleExternalBundle(specifier, siteBundles, externals, outputDir) {
280
+ if (siteBundles && externals[specifier]?.startsWith(PROTOCOL_FILE) && !siteBundles[specifier]) {
281
+ const path = normalizeFromFileURL(externals[specifier], process.cwd());
282
+ const normalizedPath = decodeURIComponent(path);
283
+ createResourceDir(dirname(normalizedPath), outputDir);
284
+ const ext = extname(normalizedPath);
285
+ const fullPath = join(outputDir, `${normalizedPath}${ext ? '' : '.js'}`);
286
+ fs.copyFileSync(fileURLToPath(externals[specifier]), fullPath);
287
+ const bundleMetadata = {
288
+ specifier,
289
+ path: normalizedPath,
290
+ imports: [],
291
+ };
292
+ siteBundles[specifier] = bundleMetadata;
293
+ }
294
+ }
260
295
  addBundleToSiteMetadata(bundleDefinition, url, siteConfig) {
261
296
  if (siteConfig.siteMetadata) {
262
297
  const locale = siteConfig.locale;
@@ -265,8 +300,9 @@ export default class SiteGenerator {
265
300
  : `${bundleDefinition.specifier}|l/${locale}`;
266
301
  const imports = bundleDefinition.bundleRecord.imports?.map((moduleRef) => getSpecifier(moduleRef)) || [];
267
302
  const dynamicImports = bundleDefinition.bundleRecord.dynamicImports?.map((moduleRef) => getSpecifier(moduleRef));
303
+ const version = bundleDefinition.version === VERSION_NOT_PROVIDED ? undefined : bundleDefinition.version;
268
304
  const bundleMetadata = {
269
- version: bundleDefinition.version,
305
+ version,
270
306
  path: decodeURIComponent(url),
271
307
  includedModules: bundleDefinition.bundleRecord.includedModules || [],
272
308
  imports,