@codecademy/brand 3.36.2-alpha.7b9e1afbb4.0 → 3.36.2-alpha.89d0f59c01.0
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/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 +20 -64
- 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,65 +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);
|
|
54
|
-
|
|
55
|
-
// in a prod build, webpack will handle removing comments
|
|
41
|
+
const BASE_URL = `${getPortalOrigin()}/api/portal/search`;
|
|
42
|
+
const fnAsStr = worker.toString().trim();
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
// eslint-disable-next-line no-console
|
|
59
|
-
console.log('[serializeSearchWorkerSrc] Start index of first {:', startIndex);
|
|
60
|
-
const result = fnAsStr.slice(fnAsStr.indexOf('{') + 1, -1) // remove wrapping function, which may have been renamed by minification
|
|
61
|
-
.replaceAll(' ', '') // remove indentation
|
|
62
|
-
.replaceAll('{BASE_URL}', BASE_URL) // interpolate base url
|
|
63
|
-
.replaceAll('{SEARCH}', window.location.search); // interpolate search query
|
|
44
|
+
// in a prod build, webpack will handle removing comments
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (result.includes('p.a.mark') || result.includes('p.a.wrap')) {
|
|
71
|
-
externalDeps.push('p (Regenerator runtime)');
|
|
72
|
-
}
|
|
73
|
-
if (result.includes('_createForOfIteratorHelper')) {
|
|
74
|
-
externalDeps.push('_createForOfIteratorHelper (Babel helper)');
|
|
75
|
-
}
|
|
76
|
-
if (externalDeps.length > 0) {
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.error('[serializeSearchWorkerSrc] ERROR: Worker code contains external dependencies that will not be available in worker context:', externalDeps);
|
|
79
|
-
// eslint-disable-next-line no-console
|
|
80
|
-
console.error('[serializeSearchWorkerSrc] This is likely because the worker function uses async/await or modern JS features that Babel transpiles with external helpers.');
|
|
81
|
-
// eslint-disable-next-line no-console
|
|
82
|
-
console.error('[serializeSearchWorkerSrc] The worker function needs to be self-contained or the build configuration needs to be adjusted to inline helpers.');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// eslint-disable-next-line no-console
|
|
86
|
-
console.log('[serializeSearchWorkerSrc] Serialization complete, result length:', result.length);
|
|
87
|
-
// eslint-disable-next-line no-console
|
|
88
|
-
console.log('[serializeSearchWorkerSrc] First 500 chars of result:', result.substring(0, 500));
|
|
89
|
-
// eslint-disable-next-line no-console
|
|
90
|
-
console.log('[serializeSearchWorkerSrc] Full result:', result);
|
|
91
|
-
return result;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
// eslint-disable-next-line no-console
|
|
94
|
-
console.error('[serializeSearchWorkerSrc] Error during serialization:', error);
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
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
|
|
97
50
|
}
|
|
98
51
|
|
|
99
52
|
/*
|
|
@@ -176,7 +129,8 @@ function worker() {
|
|
|
176
129
|
}),
|
|
177
130
|
method: 'POST'
|
|
178
131
|
}).then(f => f.json()).then(searchAsYouTypeResults => {
|
|
179
|
-
for (
|
|
132
|
+
for (let i = 0; i < searchAsYouTypeResults.top.length; i++) {
|
|
133
|
+
const entry = searchAsYouTypeResults.top[i];
|
|
180
134
|
const t = preparseTitle({
|
|
181
135
|
value: entry.title,
|
|
182
136
|
popularity: 0
|
|
@@ -204,17 +158,17 @@ function worker() {
|
|
|
204
158
|
// Bonus scores are includes in certain cases
|
|
205
159
|
function getTotalScore(q, t, charScores) {
|
|
206
160
|
let fromChars = 0;
|
|
207
|
-
for (
|
|
208
|
-
fromChars +=
|
|
161
|
+
for (let i = 0; i < charScores.length; i++) {
|
|
162
|
+
fromChars += charScores[i];
|
|
209
163
|
}
|
|
210
164
|
const fromPopularity = (t.popularity || 0) * popularityStrength;
|
|
211
165
|
let bonus = 0;
|
|
212
166
|
// for each complete word present in both the title and query
|
|
213
|
-
|
|
167
|
+
q.words.forEach(qw => {
|
|
214
168
|
if (t.words.has(qw)) {
|
|
215
169
|
bonus += 100;
|
|
216
170
|
}
|
|
217
|
-
}
|
|
171
|
+
});
|
|
218
172
|
|
|
219
173
|
// if the title starts with the query
|
|
220
174
|
if (t.lower.startsWith(q.query)) {
|
|
@@ -338,9 +292,11 @@ function worker() {
|
|
|
338
292
|
// "authentication" or other words ending in "ion"
|
|
339
293
|
const minMatchLength = Math.ceil(avgQueryWordLength ** 0.65);
|
|
340
294
|
const charScores = [];
|
|
341
|
-
for (
|
|
295
|
+
for (let i = 0; i < cmcsForTitleChars.length; i++) {
|
|
296
|
+
const cmcs = cmcsForTitleChars[i];
|
|
342
297
|
let score = 0;
|
|
343
|
-
for (
|
|
298
|
+
for (let j = 0; j < cmcs.length; j++) {
|
|
299
|
+
const cmc = cmcs[j];
|
|
344
300
|
// if the string of consecutive matching chars meets the minMatchLength
|
|
345
301
|
// or if it's a standalone word in the query (e.g "C")
|
|
346
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-alpha.
|
|
4
|
+
"version": "3.36.2-alpha.89d0f59c01.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@emotion/is-prop-valid": "^1.2.1",
|