@lwrjs/static 0.13.0-alpha.2 → 0.13.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/cjs/{utils/static-utils.cjs → index.cjs} +2 -19
- package/build/cjs/providers/static-bundle-provider.cjs +34 -29
- package/build/cjs/providers/static-module-provider.cjs +32 -13
- package/build/cjs/providers/static-resource-provider.cjs +13 -18
- package/build/cjs/site-metadata.cjs +65 -1
- package/build/cjs/tools/dedupe-bundles.cjs +108 -0
- package/build/cjs/utils/decision-tree.cjs +201 -0
- package/build/es/index.d.ts +2 -0
- package/build/es/index.js +2 -0
- package/build/es/providers/static-bundle-provider.d.ts +8 -4
- package/build/es/providers/static-bundle-provider.js +40 -31
- package/build/es/providers/static-module-provider.d.ts +4 -2
- package/build/es/providers/static-module-provider.js +32 -13
- package/build/es/providers/static-resource-provider.d.ts +3 -4
- package/build/es/providers/static-resource-provider.js +20 -26
- package/build/es/site-metadata.d.ts +48 -1
- package/build/es/site-metadata.js +101 -0
- package/build/es/tools/dedupe-bundles.d.ts +3 -0
- package/build/es/tools/dedupe-bundles.js +89 -0
- package/build/es/utils/decision-tree.d.ts +29 -0
- package/build/es/utils/decision-tree.js +267 -0
- package/package.json +9 -5
- package/build/es/utils/static-utils.d.ts +0 -12
- package/build/es/utils/static-utils.js +0 -23
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { logger } from '@lwrjs/diagnostics';
|
|
4
|
+
import DecisionTree, { createFallbackMap } from './utils/decision-tree.js';
|
|
5
|
+
import { LOCALE_SIGIL, VERSION_NOT_PROVIDED, VERSION_SIGIL, normalizeVersionToUri, } from '@lwrjs/shared-utils';
|
|
4
6
|
const SITE_METADATA_PATH = '.metadata';
|
|
5
7
|
const STATIC_BUNDLE_METADATA_PATH = path.join(SITE_METADATA_PATH, '/bundle-metadata.json');
|
|
6
8
|
const DEBUG_STATIC_BUNDLE_METADATA_PATH = path.join(SITE_METADATA_PATH, '/bundle-metadata-debug.json');
|
|
7
9
|
const STATIC_RESOURCE_METADATA_PATH = path.join(SITE_METADATA_PATH, '/resource-metadata.json');
|
|
8
10
|
const DEBUG_STATIC_RESOURCE_METADATA_PATH = path.join(SITE_METADATA_PATH, '/resource-metadata-debug.json');
|
|
9
11
|
const STATIC_ASSET_METADATA_PATH = path.join(SITE_METADATA_PATH, '/asset-metadata.json');
|
|
12
|
+
export const SITE_VERSION_PREFIX = `|${VERSION_SIGIL}/`;
|
|
13
|
+
export const SITE_LOCALE_PREFIX = `|${LOCALE_SIGIL}/`;
|
|
10
14
|
export class SiteMetadataImpl {
|
|
11
15
|
constructor(options) {
|
|
12
16
|
this.options = options;
|
|
@@ -34,6 +38,44 @@ export class SiteMetadataImpl {
|
|
|
34
38
|
getSiteAssets() {
|
|
35
39
|
return this.siteAssets;
|
|
36
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns a decision tree for site bundles in the form [debug, specifier, version, locale].
|
|
43
|
+
* It is assumed this is static after creation subsequent calls will return the same instance.
|
|
44
|
+
*/
|
|
45
|
+
getSiteBundlesDecisionTree() {
|
|
46
|
+
if (!this.bundleDecisionTree) {
|
|
47
|
+
this.bundleDecisionTree = new DecisionTree();
|
|
48
|
+
// Normalize i18NConfig fallback paths
|
|
49
|
+
const localeFallbacks = createFallbackMap(this.options.i18n);
|
|
50
|
+
// Add All the Bundles path keys [specifier][prod][version? (version || '') : ('' || '*')][localeId || fallbacks]
|
|
51
|
+
for (const [key, bundle] of Object.entries(this.siteBundles.bundles)) {
|
|
52
|
+
this.bundleDecisionTree.insert(key, bundle, false, localeFallbacks);
|
|
53
|
+
}
|
|
54
|
+
// Add All the Bundles path keys [specifier][debug][[version? (version || '') : ('' || '*')][localeId || fallbacks]
|
|
55
|
+
for (const [key, bundle] of Object.entries(this.debugSiteBundles.bundles)) {
|
|
56
|
+
this.bundleDecisionTree.insert(key, bundle, true, localeFallbacks);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return this.bundleDecisionTree;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns a decision tree for site resources.
|
|
63
|
+
* It is assumed this is static after creation subsequent calls will return the same instance.
|
|
64
|
+
*/
|
|
65
|
+
getSiteResourcesDecisionTree() {
|
|
66
|
+
if (!this.resourceDecisionTree) {
|
|
67
|
+
this.resourceDecisionTree = new DecisionTree();
|
|
68
|
+
// Add All the prod resources path keys [specifier][prod][version? (version || '') : ('' || '*')][*]
|
|
69
|
+
for (const [key, resource] of Object.entries(this.siteResources.resources)) {
|
|
70
|
+
this.resourceDecisionTree.insert(key, resource, false);
|
|
71
|
+
}
|
|
72
|
+
// Add All the debug resources path keys [specifier][debug][version? (version || '') : ('' || '*')][*]
|
|
73
|
+
for (const [key, resource] of Object.entries(this.debugSiteResources.resources)) {
|
|
74
|
+
this.resourceDecisionTree.insert(key, resource, true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return this.resourceDecisionTree;
|
|
78
|
+
}
|
|
37
79
|
async persistSiteMetadata() {
|
|
38
80
|
// Create the metadata directory if if does not exist
|
|
39
81
|
const siteMetadataPath = path.join(this.options.rootDir, SITE_METADATA_PATH);
|
|
@@ -135,4 +177,63 @@ export class SiteMetadataImpl {
|
|
|
135
177
|
return siteAssets;
|
|
136
178
|
}
|
|
137
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Return the version for a static module bundle.
|
|
182
|
+
*
|
|
183
|
+
* Version defined in the metadata > Requested Version > 'version-not-provided'
|
|
184
|
+
*/
|
|
185
|
+
export function resolveStaticBundleVersion(metadataVersion, requestedVersion) {
|
|
186
|
+
return metadataVersion || requestedVersion || VERSION_NOT_PROVIDED;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Parse a site artifact ids string in the form specifier(|sigil(/value)?)*
|
|
190
|
+
*/
|
|
191
|
+
export function parseSiteId(input) {
|
|
192
|
+
const parts = input.split('|');
|
|
193
|
+
const specifier = parts[0];
|
|
194
|
+
const variants = {};
|
|
195
|
+
// Process each variant part after the first element
|
|
196
|
+
for (let i = 1; i < parts.length; i++) {
|
|
197
|
+
const [sigil, value] = parts[i].split('/');
|
|
198
|
+
if (sigil && value) {
|
|
199
|
+
variants[sigil] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
specifier: specifier,
|
|
204
|
+
variants: variants,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get a Site Bundle Identifier from a Root Module
|
|
209
|
+
*
|
|
210
|
+
* @param moduleId - Root Module Id
|
|
211
|
+
* @param locale - Current locale
|
|
212
|
+
* @returns Site Bundle Identifier
|
|
213
|
+
*/
|
|
214
|
+
export function getSiteBundleId({ specifier, namespace, name = '', version }, locale, i18n) {
|
|
215
|
+
if (!specifier) {
|
|
216
|
+
specifier = namespace ? `${namespace}/${name}` : name;
|
|
217
|
+
}
|
|
218
|
+
// If a module has an explicit 'version-not-provided' version this will not be reflected in the specifier
|
|
219
|
+
const versionedSpecifier = version && version !== VERSION_NOT_PROVIDED
|
|
220
|
+
? `${specifier}${SITE_VERSION_PREFIX}${normalizeVersionToUri(version)}`
|
|
221
|
+
: specifier;
|
|
222
|
+
return i18n?.defaultLocale === locale
|
|
223
|
+
? versionedSpecifier
|
|
224
|
+
: `${versionedSpecifier}${SITE_LOCALE_PREFIX}${locale}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get a Site Resource Identifier from a Resource Identifier
|
|
228
|
+
*
|
|
229
|
+
* @param resourceID -Resource Identifier
|
|
230
|
+
* @returns Site Bundle Identifier
|
|
231
|
+
*/
|
|
232
|
+
export function getSiteResourceId({ specifier, version }) {
|
|
233
|
+
// If a module has an explicit 'version-not-provided' version this will not be reflected in the specifier
|
|
234
|
+
const versionedSpecifier = version && version !== VERSION_NOT_PROVIDED
|
|
235
|
+
? `${specifier}${SITE_VERSION_PREFIX}${normalizeVersionToUri(version)}`
|
|
236
|
+
: specifier;
|
|
237
|
+
return versionedSpecifier;
|
|
238
|
+
}
|
|
138
239
|
//# sourceMappingURL=site-metadata.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from '@lwrjs/diagnostics';
|
|
4
|
+
import { SiteMetadataImpl, parseSiteId } from '../site-metadata.js';
|
|
5
|
+
import { LOCALE_SIGIL, hashContent } from '@lwrjs/shared-utils';
|
|
6
|
+
export async function dedupeBundles(rootDir, i18n) {
|
|
7
|
+
const siteMetadata = new SiteMetadataImpl({
|
|
8
|
+
rootDir,
|
|
9
|
+
i18n,
|
|
10
|
+
});
|
|
11
|
+
const siteBundles = siteMetadata.getSiteBundles();
|
|
12
|
+
const decisionTree = siteMetadata.getSiteBundlesDecisionTree();
|
|
13
|
+
logger.info({
|
|
14
|
+
label: `dedupeBundles`,
|
|
15
|
+
message: `Deduplicating ${Object.keys(siteBundles.bundles).length} bundles`,
|
|
16
|
+
});
|
|
17
|
+
for (const [siteIdStr, metadata] of Object.entries(siteBundles.bundles)) {
|
|
18
|
+
const siteId = parseSiteId(siteIdStr);
|
|
19
|
+
const localeId = siteId.variants[LOCALE_SIGIL];
|
|
20
|
+
// If this is already the default locale has no fall backs skip
|
|
21
|
+
if (!localeId || localeId === i18n.defaultLocale) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Read the content from the current metadata
|
|
25
|
+
const currentPath = path.join(rootDir, metadata.path);
|
|
26
|
+
const currentSrc = fs.readFileSync(currentPath);
|
|
27
|
+
logger.debug({
|
|
28
|
+
label: `dedupeBundles`,
|
|
29
|
+
message: `${siteIdStr} -> ${hashContent(currentSrc)}`,
|
|
30
|
+
});
|
|
31
|
+
// Find the current locale
|
|
32
|
+
const locale = i18n.locales.find((l) => l.id === localeId);
|
|
33
|
+
const fallBackLocale = locale?.fallback ?? i18n.defaultLocale;
|
|
34
|
+
const fallbackMetadata = decisionTree.find(siteIdStr, false, fallBackLocale);
|
|
35
|
+
if (fallbackMetadata) {
|
|
36
|
+
// Read the content of the fallback metadata
|
|
37
|
+
const fallbackSrc = fs.readFileSync(path.join(rootDir, fallbackMetadata.path));
|
|
38
|
+
logger.debug({
|
|
39
|
+
label: `dedupeBundles`,
|
|
40
|
+
message: `fallback ${siteIdStr},${fallBackLocale} -> ${hashContent(fallbackSrc)}`,
|
|
41
|
+
});
|
|
42
|
+
if (currentSrc.equals(fallbackSrc)) {
|
|
43
|
+
logger.debug({
|
|
44
|
+
label: `dedupeBundles`,
|
|
45
|
+
message: `Remove duplicate variant ${siteIdStr}`,
|
|
46
|
+
});
|
|
47
|
+
delete siteBundles.bundles[siteIdStr];
|
|
48
|
+
// Do not remove the file if it is the same path as the fallback
|
|
49
|
+
if (metadata.path != fallbackMetadata.path) {
|
|
50
|
+
fs.removeSync(currentPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
logger.info({
|
|
56
|
+
label: `dedupeBundles`,
|
|
57
|
+
message: `Deduplicated down to ${Object.keys(siteBundles.bundles).length} bundles`,
|
|
58
|
+
});
|
|
59
|
+
// Save the updated bundle metadata
|
|
60
|
+
await siteMetadata.persistSiteMetadata();
|
|
61
|
+
// Clean up empty folders
|
|
62
|
+
deleteEmptyFolders(rootDir);
|
|
63
|
+
}
|
|
64
|
+
function deleteEmptyFolders(directory) {
|
|
65
|
+
if (!fs.existsSync(directory)) {
|
|
66
|
+
logger.warn({ label: `dedupeBundles`, message: `Directory does not exist: ${directory}` });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const files = fs.readdirSync(directory);
|
|
70
|
+
if (files.length === 0) {
|
|
71
|
+
fs.rmdirSync(directory);
|
|
72
|
+
logger.debug({ label: `dedupeBundles`, message: `Deleted empty folder: ${directory}` });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
files.forEach((file) => {
|
|
76
|
+
const filePath = path.join(directory, file);
|
|
77
|
+
const isDirectory = fs.statSync(filePath).isDirectory();
|
|
78
|
+
if (isDirectory) {
|
|
79
|
+
deleteEmptyFolders(filePath);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Check if the directory is empty after deleting its subdirectories
|
|
83
|
+
const updatedFiles = fs.readdirSync(directory);
|
|
84
|
+
if (updatedFiles.length === 0) {
|
|
85
|
+
fs.rmdirSync(directory);
|
|
86
|
+
logger.debug({ label: `dedupeBundles`, message: `Deleted empty folder: ${directory}` });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=dedupe-bundles.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DecisionTree, I18NConfig, SiteArtifact } from '@lwrjs/types';
|
|
2
|
+
export interface ArtifactVariantId {
|
|
3
|
+
specifier: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
localeId?: string;
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export default class DecisionTreeImpl<Artifact extends SiteArtifact> implements DecisionTree<Artifact> {
|
|
9
|
+
private root;
|
|
10
|
+
insert(siteArtifactId: string, artifact: Artifact, debug?: boolean, localeFallbacks?: Record<string, string[]>): void;
|
|
11
|
+
/**
|
|
12
|
+
* A method to handle deeper insertions, preserving the unique paths.
|
|
13
|
+
* This will be called for each node in the decision path.
|
|
14
|
+
*/
|
|
15
|
+
private deepInsert;
|
|
16
|
+
find(siteArtifactId: string, debug?: boolean, localeId?: string): Artifact | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Create a decision tree patch to look up the most appropriate bundle
|
|
19
|
+
*
|
|
20
|
+
* @param specifier Bundle specifier
|
|
21
|
+
* @param version known version or will add the choice ''
|
|
22
|
+
* @param localeId preferred bundle locale or will add '' for default locale
|
|
23
|
+
* @param debug flag if debug bundle is preferred
|
|
24
|
+
*/
|
|
25
|
+
private createArtifactChoices;
|
|
26
|
+
private createPossibleArtifactChoices;
|
|
27
|
+
}
|
|
28
|
+
export declare function createFallbackMap(config: I18NConfig): Record<string, string[]>;
|
|
29
|
+
//# sourceMappingURL=decision-tree.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decision tree is used to determine the best static site artifact metadata based on criteria such as specifier, version, isDebug, locale, and additional variants.
|
|
3
|
+
*
|
|
4
|
+
* This tree processes all the metadata for a specific type of artifact (bundle, asset, or resource), finding the best match based on the provided input.
|
|
5
|
+
*
|
|
6
|
+
* It operates on general decision tree principles, using tree nodes to navigate individual choices and identify the most suitable metadata.
|
|
7
|
+
*
|
|
8
|
+
* The tree is populated once by providing a set of possible matching conditions.
|
|
9
|
+
*
|
|
10
|
+
* tree.insert('bundle/foo|v/7_0|l/en', {metadata}, isDebug, {en-MX: [en-MX, en-US, en], en: [en], ...})
|
|
11
|
+
*
|
|
12
|
+
* The tree currently makes decisions in the following order: specifier, isDebug, version, and then locale.
|
|
13
|
+
*
|
|
14
|
+
* To find matching metadata, use the following commands:
|
|
15
|
+
*
|
|
16
|
+
* tree.find('bundle/foo|v/7_0|l/en-MX')
|
|
17
|
+
*
|
|
18
|
+
* tree.find('bundle/foo', isDebug, 'en-US')
|
|
19
|
+
*/
|
|
20
|
+
import { logger } from '@lwrjs/diagnostics';
|
|
21
|
+
import { LOCALE_SIGIL, VERSION_NOT_PROVIDED, VERSION_SIGIL, normalizeVersionToUri, } from '@lwrjs/shared-utils';
|
|
22
|
+
import { parseSiteId } from '../site-metadata.js';
|
|
23
|
+
// Choice wildcard means I want to match anything
|
|
24
|
+
// Examples are any locale and match the default locale
|
|
25
|
+
const CHOICE_WILDCARD = '*';
|
|
26
|
+
// Choice empty is explicitly an empty choice
|
|
27
|
+
// This is useful for scenarios like an empty version can match any version
|
|
28
|
+
// This cannot be wildcard since we want an explicit version to also not match this choice.
|
|
29
|
+
const CHOICE_EMPTY = '';
|
|
30
|
+
const CHOICE_PROD = 'prod';
|
|
31
|
+
const CHOICE_DEBUG = 'debug';
|
|
32
|
+
// Tree of decisions to lead you to the right artifact
|
|
33
|
+
export default class DecisionTreeImpl {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.root = new TreeNode(); // Root node does not hold any decision value
|
|
36
|
+
}
|
|
37
|
+
// Insert an artifact into the tree based on a path of decisions
|
|
38
|
+
insert(siteArtifactId, artifact, debug, localeFallbacks) {
|
|
39
|
+
// The decision path is the set of choices needed to get to the right metadata
|
|
40
|
+
// Currently this is hard coded to [specifier, isDebug, version, locale]
|
|
41
|
+
const decisionPath = this.createPossibleArtifactChoices({
|
|
42
|
+
id: siteArtifactId,
|
|
43
|
+
localeFallbacks,
|
|
44
|
+
debug,
|
|
45
|
+
});
|
|
46
|
+
// The set of choices in the root decision (specifier)
|
|
47
|
+
const choices = decisionPath[0];
|
|
48
|
+
// This would only be true if we ever had a decision tree with one choice (for now we expect this to always be false)
|
|
49
|
+
const isLeaf = decisionPath.length == 1;
|
|
50
|
+
// Set of valid choices in the root decision (specifier) only expected choice here is the exact specifier
|
|
51
|
+
for (const [index, key] of choices.entries()) {
|
|
52
|
+
// We will chose the ranked choice on every node along the decision to keep track of the preferred choice at each node
|
|
53
|
+
const rank = [index];
|
|
54
|
+
// If we have note made a node for the root choice (specifier) create one
|
|
55
|
+
if (!this.root.getChild(key, false)) {
|
|
56
|
+
this.root.addChild(key, new TreeNode(key, ''));
|
|
57
|
+
}
|
|
58
|
+
const nextNode = this.root.getChild(key, false);
|
|
59
|
+
// Not expected this would only be a leaf if there we no other decisions to be made
|
|
60
|
+
if (isLeaf) {
|
|
61
|
+
nextNode.setArtifact(artifact, rank);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// If it's not a leaf, prepare for the next iteration
|
|
65
|
+
// We need to iterate over each choice separately to maintain distinct paths
|
|
66
|
+
this.deepInsert(1, decisionPath, key, nextNode, artifact, rank);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A method to handle deeper insertions, preserving the unique paths.
|
|
72
|
+
* This will be called for each node in the decision path.
|
|
73
|
+
*/
|
|
74
|
+
deepInsert(level, decisionPath, currentPath, currentNode, artifact, rank) {
|
|
75
|
+
// No more choices for you
|
|
76
|
+
if (level >= decisionPath.length)
|
|
77
|
+
return;
|
|
78
|
+
// Get the set of choice for this node
|
|
79
|
+
const choices = decisionPath[level];
|
|
80
|
+
// Is this the last node in the decision path?
|
|
81
|
+
const isLeaf = level === decisionPath.length - 1;
|
|
82
|
+
for (const [index, key] of choices.entries()) {
|
|
83
|
+
// If this is a wild card mark it as WILD_CARD_RANK so we force it to be the last choice
|
|
84
|
+
const nextRank = [...rank, index];
|
|
85
|
+
if (!currentNode.getChild(key, false)) {
|
|
86
|
+
currentNode.addChild(key, new TreeNode(key, currentPath));
|
|
87
|
+
}
|
|
88
|
+
const nextNode = currentNode.getChild(key, false);
|
|
89
|
+
if (isLeaf) {
|
|
90
|
+
nextNode.setArtifact(artifact, nextRank);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.deepInsert(level + 1, decisionPath, `${currentPath}/${key}`, nextNode, artifact, nextRank);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Retrieve an artifact from the tree based on a path of decisions
|
|
98
|
+
find(siteArtifactId, debug, localeId) {
|
|
99
|
+
const parsedArtifactId = parseSiteId(siteArtifactId);
|
|
100
|
+
const decisionPath = this.createArtifactChoices({
|
|
101
|
+
specifier: parsedArtifactId.specifier,
|
|
102
|
+
version: parsedArtifactId.variants[VERSION_SIGIL],
|
|
103
|
+
localeId: localeId ?? parsedArtifactId.variants[LOCALE_SIGIL],
|
|
104
|
+
debug,
|
|
105
|
+
});
|
|
106
|
+
let currentNode = this.root;
|
|
107
|
+
for (const key of decisionPath) {
|
|
108
|
+
const lastPath = currentNode.getPath();
|
|
109
|
+
currentNode = currentNode.getChild(key);
|
|
110
|
+
if (!currentNode) {
|
|
111
|
+
logger.debug(`Module ${key} not found at ${lastPath}`);
|
|
112
|
+
return undefined; // Decision path does not lead to an artifact
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!currentNode.artifact) {
|
|
116
|
+
logger.debug(`Artifact not found at ${currentNode.getPath()}`);
|
|
117
|
+
}
|
|
118
|
+
return currentNode.artifact;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a decision tree patch to look up the most appropriate bundle
|
|
122
|
+
*
|
|
123
|
+
* @param specifier Bundle specifier
|
|
124
|
+
* @param version known version or will add the choice ''
|
|
125
|
+
* @param localeId preferred bundle locale or will add '' for default locale
|
|
126
|
+
* @param debug flag if debug bundle is preferred
|
|
127
|
+
*/
|
|
128
|
+
createArtifactChoices({ specifier, version, localeId, debug }) {
|
|
129
|
+
const envChoice = debug ? CHOICE_DEBUG : CHOICE_PROD;
|
|
130
|
+
// Versions are stored in the bundle id in URL normalized form
|
|
131
|
+
const versionChoice = getVersionChoice(version);
|
|
132
|
+
const uriVersion = normalizeVersionToUri(versionChoice);
|
|
133
|
+
return [specifier, envChoice, uriVersion, localeId || CHOICE_WILDCARD];
|
|
134
|
+
}
|
|
135
|
+
createPossibleArtifactChoices({ id, localeFallbacks, debug, }) {
|
|
136
|
+
const match = parseSiteId(id);
|
|
137
|
+
const specifier = match.specifier;
|
|
138
|
+
if (!specifier) {
|
|
139
|
+
// TODO make diagnostic error
|
|
140
|
+
throw new Error(`Unable to parse${debug ? ' debug' : ''} static bundle specifier: ${id}`);
|
|
141
|
+
}
|
|
142
|
+
// Try to parse a version out of the specifier
|
|
143
|
+
const versionChoice = getVersionChoice(match.variants[VERSION_SIGIL]);
|
|
144
|
+
// To make it so that if you ask for a version it will only match an explicit version from the metadata.
|
|
145
|
+
// I think this will cause a breaking change?
|
|
146
|
+
// Un comment to all versioned requests to fall back to *
|
|
147
|
+
// const versions = [...new Set([versionChoice, CHOICE_WILDCARD])];
|
|
148
|
+
const versions = versionChoice === CHOICE_EMPTY
|
|
149
|
+
? [...new Set([CHOICE_EMPTY, CHOICE_WILDCARD])]
|
|
150
|
+
: [...new Set([versionChoice, CHOICE_EMPTY])];
|
|
151
|
+
const envChoice = debug ? [CHOICE_DEBUG] : [CHOICE_PROD];
|
|
152
|
+
const localeChoice = match.variants[LOCALE_SIGIL];
|
|
153
|
+
const localeId = localeChoice && localeFallbacks ? localeFallbacks[localeChoice] : [CHOICE_WILDCARD];
|
|
154
|
+
return [[specifier], envChoice, versions, localeId];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* This represents a single node on the decision path, corresponding to a specific value for a decision criterion (e.g., specifier, isDebug, version, locale).
|
|
159
|
+
* If it's a leaf node, it points to an artifact's metadata.
|
|
160
|
+
* It maintains the rank of all choices at this node, allowing a later node with a higher rank to replace the current one as the best option.
|
|
161
|
+
*/
|
|
162
|
+
class TreeNode {
|
|
163
|
+
constructor(value = '', parentPath = '') {
|
|
164
|
+
this.children = new Map(); // Maps a decision key to the next TreeNode
|
|
165
|
+
this.artifact = undefined; // Final artifact at a leaf node
|
|
166
|
+
this.decisionValue = value;
|
|
167
|
+
this.parentPath = parentPath;
|
|
168
|
+
}
|
|
169
|
+
// Adds a child node based on a decision key
|
|
170
|
+
addChild(value, node) {
|
|
171
|
+
this.children.set(value, node);
|
|
172
|
+
}
|
|
173
|
+
// Sets the artifact at a leaf node
|
|
174
|
+
setArtifact(artifact, rank) {
|
|
175
|
+
if (this.artifact && isLowerOrEqualRank(rank, this.rank)) {
|
|
176
|
+
logger.debug({
|
|
177
|
+
label: 'DecisionTree',
|
|
178
|
+
message: `Ignored Artifact ${this.getPath()} ${this.rank} <= ${rank}`,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
logger.debug({
|
|
183
|
+
label: 'DecisionTree',
|
|
184
|
+
message: `Added artifact at ${this.getPath()}`,
|
|
185
|
+
});
|
|
186
|
+
this.rank = rank;
|
|
187
|
+
this.artifact = artifact;
|
|
188
|
+
}
|
|
189
|
+
// Retrieves a child node based on a decision key
|
|
190
|
+
getChild(key, allowWildcard = true) {
|
|
191
|
+
return allowWildcard
|
|
192
|
+
? this.children.get(key) || this.children.get(CHOICE_WILDCARD)
|
|
193
|
+
: this.children.get(key);
|
|
194
|
+
}
|
|
195
|
+
getPath() {
|
|
196
|
+
return this.parentPath ? this.parentPath + '|' + this.decisionValue : this.decisionValue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// If any choice was lower ranked choose this artifact
|
|
200
|
+
function isLowerOrEqualRank(contender, existing) {
|
|
201
|
+
// If existing path is undefined, we can consider the contender as lower ranked
|
|
202
|
+
// because there's nothing to compare against.
|
|
203
|
+
if (!existing) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Should not happen placed here to be sure
|
|
207
|
+
if (existing.length !== contender.length) {
|
|
208
|
+
throw new Error(`Paths must be of the same length ${existing} not found at ${contender}`);
|
|
209
|
+
}
|
|
210
|
+
// Iterate over each decision point to compare choices
|
|
211
|
+
for (let i = 0; i < existing.length; i++) {
|
|
212
|
+
// If the contender has made a choice with a higher index at any decision point,
|
|
213
|
+
// it means the contender is of a lower rank.
|
|
214
|
+
if (contender[i] > existing[i]) {
|
|
215
|
+
return true; // Contender is of a lower rank
|
|
216
|
+
}
|
|
217
|
+
else if (contender[i] < existing[i]) {
|
|
218
|
+
// If the contender has a choice with a lower index at any point, it's not of a lower rank.
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
// If the choices are the same, continue to the next decision point.
|
|
222
|
+
}
|
|
223
|
+
// If all choices are the same, the contender is equal rank.
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Returns the version or if empty or undefined it will return a wild card and match
|
|
228
|
+
* an explicity un-versioned or the first match from the bundle metadata
|
|
229
|
+
*/
|
|
230
|
+
function getVersionChoice(version) {
|
|
231
|
+
// If the version if empty or explicity version-not-provided
|
|
232
|
+
// return an empty choice to indicate that this an a value not provided
|
|
233
|
+
// so that it can match an explicit empty or wild card choice.
|
|
234
|
+
if (!version || version === VERSION_NOT_PROVIDED) {
|
|
235
|
+
return CHOICE_EMPTY;
|
|
236
|
+
}
|
|
237
|
+
// If there is a version use normalizeVersionToUri to convert it to a how it will be requested
|
|
238
|
+
return normalizeVersionToUri(version);
|
|
239
|
+
}
|
|
240
|
+
export function createFallbackMap(config) {
|
|
241
|
+
const map = {};
|
|
242
|
+
// Helper function to recursively find fallbacks
|
|
243
|
+
function findFallbacks(localeId, visited = new Set()) {
|
|
244
|
+
// Prevent cycles by checking if we've already visited this locale
|
|
245
|
+
if (visited.has(localeId) || localeId === config.defaultLocale) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
visited.add(localeId);
|
|
249
|
+
const locale = config.locales.find((l) => l.id === localeId);
|
|
250
|
+
if (!locale || !locale.fallback) {
|
|
251
|
+
return [localeId];
|
|
252
|
+
}
|
|
253
|
+
// Recursively find fallbacks, adding the current localeId to the start
|
|
254
|
+
return [localeId, ...findFallbacks(locale.fallback, visited)];
|
|
255
|
+
}
|
|
256
|
+
config.locales.forEach((locale) => {
|
|
257
|
+
// default will be wild carded
|
|
258
|
+
if (locale.id !== config.defaultLocale) {
|
|
259
|
+
// Initialize the fallbacks array for each locale, including the default as an implied fallback
|
|
260
|
+
map[locale.id] = [...new Set([...findFallbacks(locale.id)])];
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// Setup a default locale under key '*'
|
|
264
|
+
map[CHOICE_WILDCARD] = [CHOICE_WILDCARD];
|
|
265
|
+
return map;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=decision-tree.js.map
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.13.0-alpha.
|
|
7
|
+
"version": "0.13.0-alpha.21",
|
|
8
8
|
"homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
18
|
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./build/es/index.js",
|
|
21
|
+
"require": "./build/cjs/index.cjs"
|
|
22
|
+
},
|
|
19
23
|
"./site-metadata": {
|
|
20
24
|
"import": "./build/es/site-metadata.js",
|
|
21
25
|
"require": "./build/cjs/site-metadata.cjs"
|
|
@@ -48,11 +52,11 @@
|
|
|
48
52
|
"build/**/*.d.ts"
|
|
49
53
|
],
|
|
50
54
|
"dependencies": {
|
|
51
|
-
"@lwrjs/diagnostics": "0.13.0-alpha.
|
|
52
|
-
"@lwrjs/shared-utils": "0.13.0-alpha.
|
|
55
|
+
"@lwrjs/diagnostics": "0.13.0-alpha.21",
|
|
56
|
+
"@lwrjs/shared-utils": "0.13.0-alpha.21"
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
|
-
"@lwrjs/types": "0.13.0-alpha.
|
|
59
|
+
"@lwrjs/types": "0.13.0-alpha.21",
|
|
56
60
|
"@types/express": "^4.17.21",
|
|
57
61
|
"jest": "^26.6.3",
|
|
58
62
|
"jest-express": "^1.12.0",
|
|
@@ -65,5 +69,5 @@
|
|
|
65
69
|
"volta": {
|
|
66
70
|
"extends": "../../../package.json"
|
|
67
71
|
},
|
|
68
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "d272dcd07881fa469ee73bd28d2f30f99957e122"
|
|
69
73
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { I18NConfig, SiteBundle, SiteBundles } from '@lwrjs/types';
|
|
2
|
-
/**
|
|
3
|
-
* Return the version for a static module bundle.
|
|
4
|
-
*
|
|
5
|
-
* Version defined in the metadata > Requested Version > 'version-not-provided'
|
|
6
|
-
*/
|
|
7
|
-
export declare function resolveStaticBundleVersion(metadataVersion?: string, requestedVersion?: string): string;
|
|
8
|
-
/**
|
|
9
|
-
* Get the most best match for a localized bundle
|
|
10
|
-
*/
|
|
11
|
-
export declare function getLocalizedBundle(specifier: string, siteBundles: SiteBundles, initialLocaleId: string, i18n: I18NConfig): Promise<SiteBundle | undefined>;
|
|
12
|
-
//# sourceMappingURL=static-utils.d.ts.map
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Try to add any code that is mrt specific here so that we can isolate details that would have to be refactored to run lambdas
|
|
3
|
-
* on a different platform
|
|
4
|
-
*/
|
|
5
|
-
import { VERSION_NOT_PROVIDED, walkLocaleFallbacks } from '@lwrjs/shared-utils';
|
|
6
|
-
/**
|
|
7
|
-
* Return the version for a static module bundle.
|
|
8
|
-
*
|
|
9
|
-
* Version defined in the metadata > Requested Version > 'version-not-provided'
|
|
10
|
-
*/
|
|
11
|
-
export function resolveStaticBundleVersion(metadataVersion, requestedVersion) {
|
|
12
|
-
return metadataVersion || requestedVersion || VERSION_NOT_PROVIDED;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Get the most best match for a localized bundle
|
|
16
|
-
*/
|
|
17
|
-
export async function getLocalizedBundle(specifier, siteBundles, initialLocaleId, i18n) {
|
|
18
|
-
return walkLocaleFallbacks(initialLocaleId, i18n, async (localeId) => {
|
|
19
|
-
const localizedSpecifier = localeId === i18n.defaultLocale ? specifier : `${specifier}|l/${localeId}`;
|
|
20
|
-
return siteBundles.bundles[localizedSpecifier];
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=static-utils.js.map
|