@codecademy/brand 3.36.2-alpha.e9c6fd8276.0 → 3.36.2
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/dist/AppHeader/AppHeaderElements/AppHeaderLinkSections/index.js +0 -0
- package/dist/AppHeader/Search/SearchButton.js +0 -29
- package/dist/AppHeader/Search/SearchPane.js +9 -42
- package/dist/AppHeader/Search/SearchWorker/index.js +7 -55
- package/dist/AppHeader/Search/SearchWorker/src.js +48 -75
- package/dist/AppHeader/Search/SearchWorker/worker.js +23 -70
- package/dist/AppHeader/Search/safelyRedirect.js +11 -50
- package/package.json +1 -1
|
@@ -3,70 +3,22 @@ let workerPromise;
|
|
|
3
3
|
const initErr = 'Search worker not initialized';
|
|
4
4
|
export const searchWorker = {
|
|
5
5
|
init() {
|
|
6
|
-
// eslint-disable-next-line no-console
|
|
7
|
-
console.log('[searchWorker.init] Initializing search worker');
|
|
8
6
|
if (!workerPromise) {
|
|
9
|
-
// eslint-disable-next-line no-console
|
|
10
|
-
console.log('[searchWorker.init] Creating new worker promise');
|
|
11
7
|
workerPromise = (async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
createSearchWorker
|
|
18
|
-
} = await import('./worker');
|
|
19
|
-
// eslint-disable-next-line no-console
|
|
20
|
-
console.log('[searchWorker.init] Worker module imported, creating worker');
|
|
21
|
-
const worker = createSearchWorker();
|
|
22
|
-
// eslint-disable-next-line no-console
|
|
23
|
-
console.log('[searchWorker.init] Worker created successfully');
|
|
24
|
-
return worker;
|
|
25
|
-
} catch (error) {
|
|
26
|
-
// eslint-disable-next-line no-console
|
|
27
|
-
console.error('[searchWorker.init] Error creating worker:', error);
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
8
|
+
// lazy load worker
|
|
9
|
+
const {
|
|
10
|
+
createSearchWorker
|
|
11
|
+
} = await import('./worker');
|
|
12
|
+
return createSearchWorker();
|
|
30
13
|
})();
|
|
31
|
-
} else {
|
|
32
|
-
// eslint-disable-next-line no-console
|
|
33
|
-
console.log('[searchWorker.init] Worker already initialized');
|
|
34
14
|
}
|
|
35
15
|
},
|
|
36
16
|
async autocomplete(query) {
|
|
37
|
-
// eslint-disable-next-line no-console
|
|
38
|
-
console.log('[searchWorker.autocomplete] Query:', query);
|
|
39
17
|
if (!workerPromise) throw new Error(initErr);
|
|
40
|
-
|
|
41
|
-
const worker = await workerPromise;
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.log('[searchWorker.autocomplete] Worker ready, calling with query:', query);
|
|
44
|
-
const result = await worker(SearchAction.Autocomplete, query);
|
|
45
|
-
// eslint-disable-next-line no-console
|
|
46
|
-
console.log('[searchWorker.autocomplete] Result received:', result);
|
|
47
|
-
return result;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
// eslint-disable-next-line no-console
|
|
50
|
-
console.error('[searchWorker.autocomplete] Error:', error);
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
18
|
+
return (await workerPromise)(SearchAction.Autocomplete, query);
|
|
53
19
|
},
|
|
54
20
|
async searchAsYouType(query) {
|
|
55
|
-
// eslint-disable-next-line no-console
|
|
56
|
-
console.log('[searchWorker.searchAsYouType] Query:', query);
|
|
57
21
|
if (!workerPromise) throw new Error(initErr);
|
|
58
|
-
|
|
59
|
-
const worker = await workerPromise;
|
|
60
|
-
// eslint-disable-next-line no-console
|
|
61
|
-
console.log('[searchWorker.searchAsYouType] Worker ready, calling with query:', query);
|
|
62
|
-
const result = await worker(SearchAction.SearchAsYouType, query);
|
|
63
|
-
// eslint-disable-next-line no-console
|
|
64
|
-
console.log('[searchWorker.searchAsYouType] Result received:', result);
|
|
65
|
-
return result;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.error('[searchWorker.searchAsYouType] Error:', error);
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
22
|
+
return (await workerPromise)(SearchAction.SearchAsYouType, query);
|
|
71
23
|
}
|
|
72
24
|
};
|
|
@@ -28,6 +28,9 @@ function getPortalOrigin() {
|
|
|
28
28
|
// for standard envs, use portal app in the same env
|
|
29
29
|
if (envs.some(s => `https://${s}.codecademy.com` === origin)) return origin;
|
|
30
30
|
|
|
31
|
+
// for PR envs (e.g. pr-40229-monolith.dev-eks.codecademy.com)
|
|
32
|
+
if (origin.includes('.dev-eks.codecademy.com')) return origin;
|
|
33
|
+
|
|
31
34
|
// for local, use local portal-app, replace if origin port is monolith or le
|
|
32
35
|
if (origin.includes('localhost')) return origin.replace(/:\d{4}/, ':3100');
|
|
33
36
|
|
|
@@ -35,45 +38,15 @@ function getPortalOrigin() {
|
|
|
35
38
|
return 'https://staging.codecademy.com';
|
|
36
39
|
}
|
|
37
40
|
export function serializeSearchWorkerSrc() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// eslint-disable-next-line no-console
|
|
42
|
-
console.log('[serializeSearchWorkerSrc] Getting portal origin');
|
|
43
|
-
const BASE_URL = `${getPortalOrigin()}/api/portal/search`;
|
|
44
|
-
// eslint-disable-next-line no-console
|
|
45
|
-
console.log('[serializeSearchWorkerSrc] BASE_URL:', BASE_URL);
|
|
46
|
-
|
|
47
|
-
// eslint-disable-next-line no-console
|
|
48
|
-
console.log('[serializeSearchWorkerSrc] Converting worker function to string');
|
|
49
|
-
const fnAsStr = worker.toString().trim();
|
|
50
|
-
// eslint-disable-next-line no-console
|
|
51
|
-
console.log('[serializeSearchWorkerSrc] Worker function string length:', fnAsStr.length);
|
|
52
|
-
// eslint-disable-next-line no-console
|
|
53
|
-
console.log('[serializeSearchWorkerSrc] Full worker function:', fnAsStr);
|
|
41
|
+
const BASE_URL = `${getPortalOrigin()}/api/portal/search`;
|
|
42
|
+
const fnAsStr = worker.toString().trim();
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
// in a prod build, webpack will handle removing comments
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.replaceAll(' ', '') // remove indentation
|
|
62
|
-
.replaceAll('{BASE_URL}', BASE_URL) // interpolate base url
|
|
63
|
-
.replaceAll('{SEARCH}', window.location.search); // interpolate search query
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.log('[serializeSearchWorkerSrc] Serialization complete, result length:', result.length);
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.log('[serializeSearchWorkerSrc] First 500 chars of result:', result.substring(0, 500));
|
|
69
|
-
// eslint-disable-next-line no-console
|
|
70
|
-
console.log('[serializeSearchWorkerSrc] Full result:', result);
|
|
71
|
-
return result;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
// eslint-disable-next-line no-console
|
|
74
|
-
console.error('[serializeSearchWorkerSrc] Error during serialization:', error);
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
46
|
+
return fnAsStr.slice(fnAsStr.indexOf('{') + 1, -1) // remove wrapping function, which may have been renamed by minification
|
|
47
|
+
.replaceAll(' ', '') // remove indentation
|
|
48
|
+
.replaceAll('{BASE_URL}', BASE_URL) // interpolate base url
|
|
49
|
+
.replaceAll('{SEARCH}', window.location.search); // interpolate search query
|
|
77
50
|
}
|
|
78
51
|
|
|
79
52
|
/*
|
|
@@ -82,11 +55,7 @@ export function serializeSearchWorkerSrc() {
|
|
|
82
55
|
* the worker via onmessage and passed back to the main thread via postMessage.
|
|
83
56
|
*/
|
|
84
57
|
function worker() {
|
|
85
|
-
const preloadTitlesPromise = (
|
|
86
|
-
const f = await fetch('{BASE_URL}/autocomplete-preload{SEARCH}');
|
|
87
|
-
const rawTitles = await f.json();
|
|
88
|
-
return rawTitles.map(preparseTitle);
|
|
89
|
-
})();
|
|
58
|
+
const preloadTitlesPromise = fetch('{BASE_URL}/autocomplete-preload{SEARCH}').then(f => f.json()).then(rawTitles => rawTitles.map(preparseTitle));
|
|
90
59
|
function preparseTitle({
|
|
91
60
|
value,
|
|
92
61
|
popularity
|
|
@@ -138,41 +107,43 @@ function worker() {
|
|
|
138
107
|
};
|
|
139
108
|
}
|
|
140
109
|
const maxResults = 5;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
110
|
+
function autocompleteHandler(q) {
|
|
111
|
+
preloadTitlesPromise.then(titles => {
|
|
112
|
+
const scoredTitles = titles.map(t => getScoredTitle(q, t));
|
|
113
|
+
const topAutocompleteTitles = scoredTitles.filter(x => x.score > 0).sort((a, b) => a.score > b.score ? -1 : 1).slice(0, maxResults).map(t => ({
|
|
114
|
+
title: t.title,
|
|
115
|
+
segments: getHighlightSegments(t.title, t.charScores)
|
|
116
|
+
}));
|
|
117
|
+
postMessage({
|
|
118
|
+
query: q.query,
|
|
119
|
+
result: topAutocompleteTitles,
|
|
120
|
+
action: 'autocomplete'
|
|
121
|
+
});
|
|
152
122
|
});
|
|
153
123
|
}
|
|
154
|
-
|
|
155
|
-
|
|
124
|
+
function searchAsYouTypeHandler(q) {
|
|
125
|
+
fetch('{BASE_URL}/search-as-you-type{SEARCH}', {
|
|
156
126
|
body: JSON.stringify({
|
|
157
127
|
query: q.query,
|
|
158
128
|
max: maxResults
|
|
159
129
|
}),
|
|
160
130
|
method: 'POST'
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
131
|
+
}).then(f => f.json()).then(searchAsYouTypeResults => {
|
|
132
|
+
for (let i = 0; i < searchAsYouTypeResults.top.length; i++) {
|
|
133
|
+
const entry = searchAsYouTypeResults.top[i];
|
|
134
|
+
const t = preparseTitle({
|
|
135
|
+
value: entry.title,
|
|
136
|
+
popularity: 0
|
|
137
|
+
});
|
|
138
|
+
const charScores = getCharScores(q, t);
|
|
139
|
+
entry.segments = getHighlightSegments(entry.title, charScores);
|
|
140
|
+
entry.key = q.query + ':' + entry.title;
|
|
141
|
+
}
|
|
142
|
+
postMessage({
|
|
143
|
+
query: q.query,
|
|
144
|
+
result: searchAsYouTypeResults,
|
|
145
|
+
action: 'search-as-you-type'
|
|
167
146
|
});
|
|
168
|
-
const charScores = getCharScores(q, t);
|
|
169
|
-
entry.segments = getHighlightSegments(entry.title, charScores);
|
|
170
|
-
entry.key = q.query + ':' + entry.title;
|
|
171
|
-
}
|
|
172
|
-
postMessage({
|
|
173
|
-
query: q.query,
|
|
174
|
-
result: searchAsYouTypeResults,
|
|
175
|
-
action: 'search-as-you-type'
|
|
176
147
|
});
|
|
177
148
|
}
|
|
178
149
|
|
|
@@ -187,17 +158,17 @@ function worker() {
|
|
|
187
158
|
// Bonus scores are includes in certain cases
|
|
188
159
|
function getTotalScore(q, t, charScores) {
|
|
189
160
|
let fromChars = 0;
|
|
190
|
-
for (
|
|
191
|
-
fromChars +=
|
|
161
|
+
for (let i = 0; i < charScores.length; i++) {
|
|
162
|
+
fromChars += charScores[i];
|
|
192
163
|
}
|
|
193
164
|
const fromPopularity = (t.popularity || 0) * popularityStrength;
|
|
194
165
|
let bonus = 0;
|
|
195
166
|
// for each complete word present in both the title and query
|
|
196
|
-
|
|
167
|
+
q.words.forEach(qw => {
|
|
197
168
|
if (t.words.has(qw)) {
|
|
198
169
|
bonus += 100;
|
|
199
170
|
}
|
|
200
|
-
}
|
|
171
|
+
});
|
|
201
172
|
|
|
202
173
|
// if the title starts with the query
|
|
203
174
|
if (t.lower.startsWith(q.query)) {
|
|
@@ -321,9 +292,11 @@ function worker() {
|
|
|
321
292
|
// "authentication" or other words ending in "ion"
|
|
322
293
|
const minMatchLength = Math.ceil(avgQueryWordLength ** 0.65);
|
|
323
294
|
const charScores = [];
|
|
324
|
-
for (
|
|
295
|
+
for (let i = 0; i < cmcsForTitleChars.length; i++) {
|
|
296
|
+
const cmcs = cmcsForTitleChars[i];
|
|
325
297
|
let score = 0;
|
|
326
|
-
for (
|
|
298
|
+
for (let j = 0; j < cmcs.length; j++) {
|
|
299
|
+
const cmc = cmcs[j];
|
|
327
300
|
// if the string of consecutive matching chars meets the minMatchLength
|
|
328
301
|
// or if it's a standalone word in the query (e.g "C")
|
|
329
302
|
if (cmc.value.length >= minMatchLength || q.words.has(cmc.value)) {
|
|
@@ -5,74 +5,27 @@ const mockWorker = () => ({
|
|
|
5
5
|
postMessage: () => null
|
|
6
6
|
});
|
|
7
7
|
export function createSearchWorker() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// eslint-disable-next-line no-console
|
|
32
|
-
console.log('[createSearchWorker] Checking Worker availability');
|
|
33
|
-
const w = typeof Worker === 'undefined' ? mockWorker() : new Worker(dataUrl);
|
|
34
|
-
// eslint-disable-next-line no-console
|
|
35
|
-
console.log('[createSearchWorker] Worker instance created');
|
|
36
|
-
const results = new PromiseLookup({
|
|
37
|
-
onKeyInit: (action, query) => {
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
console.log('[createSearchWorker.onKeyInit] Posting message to worker:', {
|
|
40
|
-
action,
|
|
41
|
-
query
|
|
42
|
-
});
|
|
43
|
-
w.postMessage({
|
|
44
|
-
action,
|
|
45
|
-
query
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
w.onmessage = ({
|
|
50
|
-
data
|
|
51
|
-
}) => {
|
|
52
|
-
// eslint-disable-next-line no-console
|
|
53
|
-
console.log('[createSearchWorker.onmessage] Received message from worker:', data);
|
|
54
|
-
const {
|
|
55
|
-
query,
|
|
56
|
-
result,
|
|
57
|
-
action
|
|
58
|
-
} = data;
|
|
59
|
-
results.resolve(action, query, result);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Add error handler for the worker
|
|
63
|
-
if ('onerror' in w) {
|
|
64
|
-
w.onerror = error => {
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.error('[createSearchWorker] Worker error:', error);
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// eslint-disable-next-line no-console
|
|
71
|
-
console.log('[createSearchWorker] Worker fully configured');
|
|
72
|
-
return (action, query) => results.get(action, query);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
// eslint-disable-next-line no-console
|
|
75
|
-
console.error('[createSearchWorker] Error creating worker:', error);
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
8
|
+
const src = serializeSearchWorkerSrc();
|
|
9
|
+
const blob = new Blob([src], {
|
|
10
|
+
type: 'text/javascript'
|
|
11
|
+
});
|
|
12
|
+
const dataUrl = URL.createObjectURL?.(blob);
|
|
13
|
+
const w = typeof Worker === 'undefined' ? mockWorker() : new Worker(dataUrl);
|
|
14
|
+
const results = new PromiseLookup({
|
|
15
|
+
onKeyInit: (action, query) => w.postMessage({
|
|
16
|
+
action,
|
|
17
|
+
query
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
w.onmessage = ({
|
|
21
|
+
data
|
|
22
|
+
}) => {
|
|
23
|
+
const {
|
|
24
|
+
query,
|
|
25
|
+
result,
|
|
26
|
+
action
|
|
27
|
+
} = data;
|
|
28
|
+
results.resolve(action, query, result);
|
|
29
|
+
};
|
|
30
|
+
return (action, query) => results.get(action, query);
|
|
78
31
|
}
|
|
@@ -5,31 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import { ALLOWED_HOSTS } from './consts';
|
|
7
7
|
export function safelyRedirect(url) {
|
|
8
|
-
// eslint-disable-next-line no-console
|
|
9
|
-
console.log('[safelyRedirect] Attempting to redirect to:', url);
|
|
10
8
|
try {
|
|
11
|
-
// eslint-disable-next-line no-console
|
|
12
|
-
console.log('[safelyRedirect] Sanitizing URL');
|
|
13
9
|
const sanitizedURL = sanitizeURL(url);
|
|
14
|
-
|
|
15
|
-
console.log('[safelyRedirect] Sanitized URL:', sanitizedURL);
|
|
16
|
-
const isRelative = isRelativeUrl(sanitizedURL);
|
|
17
|
-
// eslint-disable-next-line no-console
|
|
18
|
-
console.log('[safelyRedirect] Is relative URL:', isRelative);
|
|
19
|
-
const hasAllowed = hasAllowedHost(sanitizedURL);
|
|
20
|
-
// eslint-disable-next-line no-console
|
|
21
|
-
console.log('[safelyRedirect] Has allowed host:', hasAllowed);
|
|
22
|
-
if (isRelative || hasAllowed) {
|
|
23
|
-
// eslint-disable-next-line no-console
|
|
24
|
-
console.log('[safelyRedirect] Redirecting to:', sanitizedURL);
|
|
10
|
+
if (isRelativeUrl(sanitizedURL) || hasAllowedHost(sanitizedURL)) {
|
|
25
11
|
window.location.assign(sanitizedURL);
|
|
26
12
|
} else {
|
|
27
13
|
throw new Error(`Invalid redirect url: ${sanitizedURL}`);
|
|
28
14
|
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
// eslint-disable-next-line no-console
|
|
31
|
-
console.error('[safelyRedirect] Error during redirect:', error);
|
|
32
|
-
}
|
|
15
|
+
} catch (error) {}
|
|
33
16
|
}
|
|
34
17
|
function hasAllowedHost(url) {
|
|
35
18
|
const parsedUrl = new URL(url);
|
|
@@ -54,35 +37,13 @@ function isRelativeUrl(url) {
|
|
|
54
37
|
// supports already encoded urls by decoding and re-encoding.
|
|
55
38
|
// params are handled separately to allow for the encoding of / characters
|
|
56
39
|
function sanitizeURL(url) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!params) return sanitizedPath;
|
|
67
|
-
|
|
68
|
-
// eslint-disable-next-line no-console
|
|
69
|
-
console.log('[sanitizeURL] Creating URLSearchParams with:', params);
|
|
70
|
-
const urlParams = new URLSearchParams(params);
|
|
71
|
-
// eslint-disable-next-line no-console
|
|
72
|
-
console.log('[sanitizeURL] URLSearchParams created');
|
|
73
|
-
urlParams.forEach((value, key) => {
|
|
74
|
-
// eslint-disable-next-line no-console
|
|
75
|
-
console.log('[sanitizeURL] Processing param:', key, '=', value);
|
|
76
|
-
const decoded = decodeURIComponent(value);
|
|
77
|
-
urlParams.set(key, decoded);
|
|
78
|
-
});
|
|
79
|
-
const result = `${sanitizedPath}?${urlParams.toString()}`;
|
|
80
|
-
// eslint-disable-next-line no-console
|
|
81
|
-
console.log('[sanitizeURL] Final result:', result);
|
|
82
|
-
return result;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
// eslint-disable-next-line no-console
|
|
85
|
-
console.error('[sanitizeURL] Error sanitizing URL:', error);
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
40
|
+
const [path, params] = url.split('?', 2);
|
|
41
|
+
const sanitizedPath = encodeURI(decodeURI(path));
|
|
42
|
+
if (!params) return sanitizedPath;
|
|
43
|
+
const urlParams = new URLSearchParams(params);
|
|
44
|
+
urlParams.forEach((value, key) => {
|
|
45
|
+
const decoded = decodeURIComponent(value);
|
|
46
|
+
urlParams.set(key, decoded);
|
|
47
|
+
});
|
|
48
|
+
return `${sanitizedPath}?${urlParams.toString()}`;
|
|
88
49
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecademy/brand",
|
|
3
3
|
"description": "Brand component library for Codecademy",
|
|
4
|
-
"version": "3.36.2
|
|
4
|
+
"version": "3.36.2",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@emotion/is-prop-valid": "^1.2.1",
|