@ohos-ports/lwrjs-static 0.24.0-beta.1

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 (29) hide show
  1. package/LICENSE +10 -0
  2. package/build/cjs/index.cjs +22 -0
  3. package/build/cjs/providers/static-asset-provider.cjs +103 -0
  4. package/build/cjs/providers/static-bundle-provider.cjs +246 -0
  5. package/build/cjs/providers/static-module-provider.cjs +138 -0
  6. package/build/cjs/providers/static-resource-provider.cjs +73 -0
  7. package/build/cjs/site-metadata.cjs +215 -0
  8. package/build/cjs/tools/dedupe-bundles.cjs +108 -0
  9. package/build/cjs/transformers/mrt-static-uri-transformer.cjs +62 -0
  10. package/build/cjs/utils/decision-tree.cjs +209 -0
  11. package/build/es/index.d.ts +2 -0
  12. package/build/es/index.js +2 -0
  13. package/build/es/providers/static-asset-provider.d.ts +16 -0
  14. package/build/es/providers/static-asset-provider.js +90 -0
  15. package/build/es/providers/static-bundle-provider.d.ts +59 -0
  16. package/build/es/providers/static-bundle-provider.js +257 -0
  17. package/build/es/providers/static-module-provider.d.ts +15 -0
  18. package/build/es/providers/static-module-provider.js +118 -0
  19. package/build/es/providers/static-resource-provider.d.ts +10 -0
  20. package/build/es/providers/static-resource-provider.js +53 -0
  21. package/build/es/site-metadata.d.ts +80 -0
  22. package/build/es/site-metadata.js +243 -0
  23. package/build/es/tools/dedupe-bundles.d.ts +3 -0
  24. package/build/es/tools/dedupe-bundles.js +89 -0
  25. package/build/es/transformers/mrt-static-uri-transformer.d.ts +3 -0
  26. package/build/es/transformers/mrt-static-uri-transformer.js +40 -0
  27. package/build/es/utils/decision-tree.d.ts +35 -0
  28. package/build/es/utils/decision-tree.js +287 -0
  29. package/package.json +77 -0
@@ -0,0 +1,287 @@
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|ssr|l/en', {metadata}, isDebug, forSsr, {en-MX: [en-MX, en-US, en], en: [en], ...})
11
+ *
12
+ * The tree currently makes decisions in the following order: specifier, isDebug, version, forSsr and then locale.
13
+ *
14
+ * To find matching metadata, use the following commands:
15
+ *
16
+ * tree.find('bundle/foo|v/7_0|ssr|l/en-MX')
17
+ *
18
+ * tree.find('bundle/foo', isDebug, forSsr, 'en-US')
19
+ */
20
+ import { logger } from '@lwrjs/diagnostics';
21
+ import { LOCALE_SIGIL, SSR_SIGIL, VERSION_NOT_PROVIDED, VERSION_SIGIL, getFeatureFlags, 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
+ // Boolean choice set available it marked true without requiring a value
31
+ const CHOICE_TRUE = 'true';
32
+ const CHOICE_PROD = 'prod';
33
+ const CHOICE_DEBUG = 'debug';
34
+ // Tree of decisions to lead you to the right artifact
35
+ export default class DecisionTreeImpl {
36
+ constructor() {
37
+ this.root = new TreeNode(); // Root node does not hold any decision value
38
+ }
39
+ // Insert an artifact into the tree based on a path of decisions
40
+ insert(siteArtifactId, artifact, debug, localeFallbacks) {
41
+ // The decision path is the set of choices needed to get to the right metadata
42
+ // Currently this is hard coded to [specifier, isDebug, version, ssr, locale]
43
+ const decisionPath = this.createPossibleArtifactChoices({
44
+ id: siteArtifactId,
45
+ localeFallbacks,
46
+ debug,
47
+ });
48
+ // The set of choices in the root decision (specifier)
49
+ const choices = decisionPath[0];
50
+ // This would only be true if we ever had a decision tree with one choice (for now we expect this to always be false)
51
+ const isLeaf = decisionPath.length == 1;
52
+ // Set of valid choices in the root decision (specifier) only expected choice here is the exact specifier
53
+ for (const [index, key] of choices.entries()) {
54
+ // We will chose the ranked choice on every node along the decision to keep track of the preferred choice at each node
55
+ const rank = [index];
56
+ // If we have note made a node for the root choice (specifier) create one
57
+ if (!this.root.getChild(key, false)) {
58
+ this.root.addChild(key, new TreeNode(key, ''));
59
+ }
60
+ const nextNode = this.root.getChild(key, false);
61
+ // Not expected this would only be a leaf if there we no other decisions to be made
62
+ if (isLeaf) {
63
+ nextNode.setArtifact(artifact, rank);
64
+ }
65
+ else {
66
+ // If it's not a leaf, prepare for the next iteration
67
+ // We need to iterate over each choice separately to maintain distinct paths
68
+ this.deepInsert(1, decisionPath, key, nextNode, artifact, rank);
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * A method to handle deeper insertions, preserving the unique paths.
74
+ * This will be called for each node in the decision path.
75
+ */
76
+ deepInsert(level, decisionPath, currentPath, currentNode, artifact, rank) {
77
+ // No more choices for you
78
+ if (level >= decisionPath.length)
79
+ return;
80
+ // Get the set of choice for this node
81
+ const choices = decisionPath[level];
82
+ // Is this the last node in the decision path?
83
+ const isLeaf = level === decisionPath.length - 1;
84
+ for (const [index, key] of choices.entries()) {
85
+ // If this is a wild card mark it as WILD_CARD_RANK so we force it to be the last choice
86
+ const nextRank = [...rank, index];
87
+ if (!currentNode.getChild(key, false)) {
88
+ currentNode.addChild(key, new TreeNode(key, currentPath));
89
+ }
90
+ const nextNode = currentNode.getChild(key, false);
91
+ if (isLeaf) {
92
+ nextNode.setArtifact(artifact, nextRank);
93
+ }
94
+ else {
95
+ this.deepInsert(level + 1, decisionPath, `${currentPath}/${key}`, nextNode, artifact, nextRank);
96
+ }
97
+ }
98
+ }
99
+ // Retrieve an artifact from the tree based on a path of decisions
100
+ find(siteArtifactId, debug, ssr, localeId) {
101
+ const parsedArtifactId = parseSiteId(siteArtifactId);
102
+ const decisionPath = this.createArtifactChoices({
103
+ specifier: parsedArtifactId.specifier,
104
+ version: parsedArtifactId.variants[VERSION_SIGIL],
105
+ ssr: ssr ?? SSR_SIGIL in parsedArtifactId.variants,
106
+ localeId: localeId ?? parsedArtifactId.variants[LOCALE_SIGIL],
107
+ debug,
108
+ });
109
+ let currentNode = this.root;
110
+ for (const key of decisionPath) {
111
+ const lastPath = currentNode.getPath();
112
+ currentNode = currentNode.getChild(key);
113
+ if (!currentNode) {
114
+ logger.debug(`Module ${key} not found at ${lastPath}`);
115
+ return undefined; // Decision path does not lead to an artifact
116
+ }
117
+ }
118
+ if (!currentNode.artifact) {
119
+ logger.debug(`Artifact not found at ${currentNode.getPath()}`);
120
+ }
121
+ return currentNode.artifact;
122
+ }
123
+ /**
124
+ * Create a decision tree path to look up the most appropriate bundle
125
+ *
126
+ * @param specifier Bundle specifier
127
+ * @param version known version or will add the choice ''
128
+ * @param localeId preferred bundle locale or will add '' for default locale
129
+ * @param debug flag if debug bundle is preferred
130
+ * @param ssr flag if server bundle is requested
131
+ */
132
+ createArtifactChoices({ specifier, version, localeId, debug, ssr }) {
133
+ const envChoice = debug ? CHOICE_DEBUG : CHOICE_PROD;
134
+ // Versions are stored in the bundle id in URL normalized form
135
+ const versionChoice = getVersionChoice(version);
136
+ const uriVersion = normalizeVersionToUri(versionChoice);
137
+ const ssrChoice = ssr ? CHOICE_TRUE : CHOICE_EMPTY;
138
+ return this.getOrderedChoices(specifier, envChoice, ssrChoice, uriVersion, localeId);
139
+ }
140
+ /**
141
+ * Get the choices in a consistent order for possible choices or choices for lookup
142
+ */
143
+ getOrderedChoices(specifier, envChoice, ssrChoice, uriVersion, localeId) {
144
+ return [specifier, envChoice, ssrChoice, uriVersion, localeId || CHOICE_WILDCARD];
145
+ }
146
+ createPossibleArtifactChoices({ id, localeFallbacks, debug, }) {
147
+ const match = parseSiteId(id);
148
+ const specifier = match.specifier;
149
+ if (!specifier) {
150
+ // TODO make diagnostic error
151
+ throw new Error(`Unable to parse${debug ? ' debug' : ''} static bundle specifier: ${id}`);
152
+ }
153
+ // Try to parse a version out of the specifier
154
+ const versionChoice = getVersionChoice(match.variants[VERSION_SIGIL]);
155
+ // To make it so that if you ask for a version it will only match an explicit version from the metadata.
156
+ // I think this will cause a breaking change?
157
+ // Un comment to all versioned requests to fall back to *
158
+ // const versions = [...new Set([versionChoice, CHOICE_WILDCARD])];
159
+ const versions = versionChoice === CHOICE_EMPTY
160
+ ? [...new Set([CHOICE_EMPTY, CHOICE_WILDCARD])]
161
+ : [...new Set([versionChoice, CHOICE_EMPTY])];
162
+ const envChoice = debug ? [CHOICE_DEBUG] : [CHOICE_PROD];
163
+ // If there is an ssr sigil always prefer it during SSR
164
+ const ssrChoice = match.variants[SSR_SIGIL] || CHOICE_EMPTY;
165
+ const ssr = getFeatureFlags().SSR_COMPILER_ENABLED
166
+ ? [ssrChoice]
167
+ : // For SSR V1 |ssr will prefer true non ssr will prefer empty
168
+ match.variants[SSR_SIGIL]
169
+ ? [CHOICE_TRUE, CHOICE_EMPTY]
170
+ : [CHOICE_EMPTY, CHOICE_TRUE];
171
+ const localeChoice = match.variants[LOCALE_SIGIL];
172
+ // If there are no fallbacks, or localeChoice is not in fallbacks use the ['*'] wildcard choice
173
+ const localeId = localeFallbacks?.[localeChoice] ?? [CHOICE_WILDCARD];
174
+ return this.getOrderedChoices([specifier], envChoice, ssr, versions, localeId);
175
+ }
176
+ }
177
+ /**
178
+ * This represents a single node on the decision path, corresponding to a specific value for a decision criterion (e.g., specifier, isDebug, version, locale).
179
+ * If it's a leaf node, it points to an artifact's metadata.
180
+ * 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.
181
+ */
182
+ class TreeNode {
183
+ constructor(value = '', parentPath = '') {
184
+ this.children = new Map(); // Maps a decision key to the next TreeNode
185
+ this.artifact = undefined; // Final artifact at a leaf node
186
+ this.decisionValue = value;
187
+ this.parentPath = parentPath;
188
+ }
189
+ // Adds a child node based on a decision key
190
+ addChild(value, node) {
191
+ this.children.set(value, node);
192
+ }
193
+ // Sets the artifact at a leaf node
194
+ setArtifact(artifact, rank) {
195
+ if (this.artifact && isLowerOrEqualRank(rank, this.rank)) {
196
+ logger.debug({
197
+ label: 'DecisionTree',
198
+ message: `Ignored Artifact ${this.getPath()} ${this.rank} <= ${rank}`,
199
+ });
200
+ return;
201
+ }
202
+ logger.debug({
203
+ label: 'DecisionTree',
204
+ message: `Added artifact at ${this.getPath()}`,
205
+ });
206
+ this.rank = rank;
207
+ this.artifact = artifact;
208
+ }
209
+ // Retrieves a child node based on a decision key
210
+ getChild(key, allowWildcard = true) {
211
+ return allowWildcard
212
+ ? this.children.get(key) || this.children.get(CHOICE_WILDCARD)
213
+ : this.children.get(key);
214
+ }
215
+ getPath() {
216
+ return this.parentPath ? this.parentPath + '|' + this.decisionValue : this.decisionValue;
217
+ }
218
+ }
219
+ // If any choice was lower ranked choose this artifact
220
+ function isLowerOrEqualRank(contender, existing) {
221
+ // If existing path is undefined, we can consider the contender as lower ranked
222
+ // because there's nothing to compare against.
223
+ if (!existing) {
224
+ return true;
225
+ }
226
+ // Should not happen placed here to be sure
227
+ if (existing.length !== contender.length) {
228
+ throw new Error(`Paths must be of the same length ${existing} not found at ${contender}`);
229
+ }
230
+ // Iterate over each decision point to compare choices
231
+ for (let i = 0; i < existing.length; i++) {
232
+ // If the contender has made a choice with a higher index at any decision point,
233
+ // it means the contender is of a lower rank.
234
+ if (contender[i] > existing[i]) {
235
+ return true; // Contender is of a lower rank
236
+ }
237
+ else if (contender[i] < existing[i]) {
238
+ // If the contender has a choice with a lower index at any point, it's not of a lower rank.
239
+ return false;
240
+ }
241
+ // If the choices are the same, continue to the next decision point.
242
+ }
243
+ // If all choices are the same, the contender is equal rank.
244
+ return true;
245
+ }
246
+ /**
247
+ * Returns the version or if empty or undefined it will return a wild card and match
248
+ * an explicity un-versioned or the first match from the bundle metadata
249
+ */
250
+ function getVersionChoice(version) {
251
+ // If the version if empty or explicity version-not-provided
252
+ // return an empty choice to indicate that this an a value not provided
253
+ // so that it can match an explicit empty or wild card choice.
254
+ if (!version || version === VERSION_NOT_PROVIDED) {
255
+ return CHOICE_EMPTY;
256
+ }
257
+ // If there is a version use normalizeVersionToUri to convert it to a how it will be requested
258
+ return normalizeVersionToUri(version);
259
+ }
260
+ export function createFallbackMap(config) {
261
+ const map = {};
262
+ // Helper function to recursively find fallbacks
263
+ function findFallbacks(localeId, visited = new Set()) {
264
+ // Prevent cycles by checking if we've already visited this locale
265
+ if (visited.has(localeId) || localeId === config.defaultLocale) {
266
+ return [];
267
+ }
268
+ visited.add(localeId);
269
+ const locale = config.locales.find((l) => l.id === localeId);
270
+ if (!locale || !locale.fallback) {
271
+ return [localeId];
272
+ }
273
+ // Recursively find fallbacks, adding the current localeId to the start
274
+ return [localeId, ...findFallbacks(locale.fallback, visited)];
275
+ }
276
+ config.locales.forEach((locale) => {
277
+ // default will be wild carded
278
+ if (locale.id !== config.defaultLocale) {
279
+ // Initialize the fallbacks array for each locale, including the default as an implied fallback
280
+ map[locale.id] = [...new Set([...findFallbacks(locale.id)])];
281
+ }
282
+ });
283
+ // Setup a default locale under key '*'
284
+ map[CHOICE_WILDCARD] = [CHOICE_WILDCARD];
285
+ return map;
286
+ }
287
+ //# sourceMappingURL=decision-tree.js.map
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@ohos-ports/lwrjs-static",
3
+ "license": "MIT",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "version": "0.24.0-beta.1",
8
+ "homepage": "https://github.com/ohos-ports/ohos-ports/tree/main/ports/lwrjs-static/0.24.0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ohos-ports/ohos-ports.git",
12
+ "directory": "ports/lwrjs-static/0.24.0"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/ohos-ports/ohos-ports/issues"
16
+ },
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./build/es/index.js",
21
+ "require": "./build/cjs/index.cjs"
22
+ },
23
+ "./site-metadata": {
24
+ "import": "./build/es/site-metadata.js",
25
+ "require": "./build/cjs/site-metadata.cjs"
26
+ },
27
+ "./asset-provider": {
28
+ "import": "./build/es/providers/static-asset-provider.js",
29
+ "require": "./build/cjs/providers/static-asset-provider.cjs"
30
+ },
31
+ "./bundle-provider": {
32
+ "import": "./build/es/providers/static-bundle-provider.js",
33
+ "require": "./build/cjs/providers/static-bundle-provider.cjs"
34
+ },
35
+ "./module-provider": {
36
+ "import": "./build/es/providers/static-module-provider.js",
37
+ "require": "./build/cjs/providers/static-module-provider.cjs"
38
+ },
39
+ "./resource-provider": {
40
+ "import": "./build/es/providers/static-resource-provider.js",
41
+ "require": "./build/cjs/providers/static-resource-provider.cjs"
42
+ },
43
+ "./mrt-static-uri-transformer": {
44
+ "import": "./build/es/transformers/mrt-static-uri-transformer.js",
45
+ "require": "./build/cjs/transformers/mrt-static-uri-transformer.cjs"
46
+ }
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -b",
50
+ "clean": "rimraf build node_modules",
51
+ "test": "jest"
52
+ },
53
+ "files": [
54
+ "build/**/*.js",
55
+ "build/**/*.cjs",
56
+ "build/**/*.d.ts"
57
+ ],
58
+ "dependencies": {
59
+ "@lwrjs/diagnostics": "0.24.0",
60
+ "@lwrjs/instrumentation": "0.24.0",
61
+ "@lwrjs/shared-utils": "0.24.0",
62
+ "fs-extra": "^11.4.0",
63
+ "lru-cache": "^10.4.3"
64
+ },
65
+ "devDependencies": {
66
+ "@lwrjs/types": "0.24.0",
67
+ "@types/express": "^4.17.21",
68
+ "jest": "29.7.0",
69
+ "jest-express": "^1.12.0",
70
+ "memfs": "^4.13.0",
71
+ "mock-res": "^0.6.0",
72
+ "ts-jest": "^29.2.6"
73
+ },
74
+ "engines": {
75
+ "node": ">=22.0.0"
76
+ }
77
+ }