build(docs-infra): convert `search-worker.js` to TypeScript (#29764)
PR Close #29764
This commit is contained in:
parent
ee603a3b01
commit
3a836c362d
|
@ -21,6 +21,7 @@
|
|||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"webWorkerTsConfig": "src/tsconfig.worker.json",
|
||||
"aot": true,
|
||||
"optimization": true,
|
||||
"buildOptimizer": true,
|
||||
|
@ -35,7 +36,6 @@
|
|||
"assets": [
|
||||
"src/assets",
|
||||
"src/generated",
|
||||
"src/app/search/search-worker.js",
|
||||
"src/pwa-manifest.json",
|
||||
"src/google385281288605d160.html",
|
||||
{
|
||||
|
@ -123,6 +123,7 @@
|
|||
"karmaConfig": "src/karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"webWorkerTsConfig": "src/tsconfig.worker.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
|
@ -130,7 +131,6 @@
|
|||
"assets": [
|
||||
"src/assets",
|
||||
"src/generated",
|
||||
"src/app/search/search-worker.js",
|
||||
"src/pwa-manifest.json",
|
||||
"src/google385281288605d160.html",
|
||||
{
|
||||
|
|
|
@ -9,10 +9,8 @@
|
|||
"files": [
|
||||
"/index.html",
|
||||
"/pwa-manifest.json",
|
||||
"/app/search/search-worker.js",
|
||||
"/assets/images/favicons/favicon.ico",
|
||||
"/assets/js/*.js",
|
||||
"/generated/lunr.min.js",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
],
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"e2e": "ng e2e --no-webdriver-update",
|
||||
"presetup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env && yarn ~~clean-generated && yarn boilerplate:remove",
|
||||
"setup": "yarn aio-use-npm && yarn example-use-npm",
|
||||
"postsetup": "yarn ~~build-ie-polyfills && yarn ~~minify-lunr && yarn boilerplate:add && yarn extract-cli-command-docs && yarn docs",
|
||||
"postsetup": "yarn ~~build-ie-polyfills && yarn boilerplate:add && yarn extract-cli-command-docs && yarn docs",
|
||||
"presetup-local": "yarn presetup",
|
||||
"setup-local": "yarn aio-use-local && yarn example-use-local",
|
||||
"postsetup-local": "yarn postsetup",
|
||||
|
@ -69,8 +69,7 @@
|
|||
"~~build": "ng build --configuration=stable",
|
||||
"post~~build": "yarn build-404-page",
|
||||
"~~build-ie-polyfills": "webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production",
|
||||
"~~http-server": "http-server",
|
||||
"~~minify-lunr": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/generated/lunr.min.js --source-map"
|
||||
"~~http-server": "http-server"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.9.0 <11.0.0",
|
||||
|
@ -89,6 +88,7 @@
|
|||
"@angular/platform-browser-dynamic": "^7.0.0",
|
||||
"@angular/router": "^7.0.0",
|
||||
"@angular/service-worker": "^7.0.0",
|
||||
"@types/lunr": "^2.3.2",
|
||||
"@webcomponents/custom-elements": "^1.2.0",
|
||||
"classlist.js": "^1.1.20150312",
|
||||
"core-js": "^2.4.1",
|
||||
|
|
|
@ -110,7 +110,7 @@ export class AppComponent implements OnInit {
|
|||
// Do not initialize the search on browsers that lack web worker support
|
||||
if ('Worker' in window) {
|
||||
// Delay initialization by up to 2 seconds
|
||||
this.searchService.initWorker('app/search/search-worker.js', 2000);
|
||||
this.searchService.initWorker(2000);
|
||||
}
|
||||
|
||||
this.onResize(window.innerWidth);
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/* eslint-env worker */
|
||||
/* global importScripts, lunr */
|
||||
|
||||
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
|
||||
|
||||
// NOTE: This needs to be kept in sync with `ngsw-config.json`.
|
||||
importScripts('/generated/lunr.min.js');
|
||||
|
||||
var index;
|
||||
var pages /* : SearchInfo */ = {};
|
||||
|
||||
// interface SearchInfo {
|
||||
// [key: string]: PageInfo;
|
||||
// }
|
||||
|
||||
// interface PageInfo {
|
||||
// path: string;
|
||||
// type: string,
|
||||
// titleWords: string;
|
||||
// keyWords: string;
|
||||
// }
|
||||
|
||||
self.onmessage = handleMessage;
|
||||
|
||||
// Create the lunr index - the docs should be an array of objects, each object containing
|
||||
// the path and search terms for a page
|
||||
function createIndex(addFn) {
|
||||
lunr.QueryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
|
||||
return lunr(/** @this */function() {
|
||||
this.ref('path');
|
||||
this.field('titleWords', {boost: 10});
|
||||
this.field('headingWords', {boost: 5});
|
||||
this.field('members', {boost: 4});
|
||||
this.field('keywords', {boost: 2});
|
||||
addFn(this);
|
||||
});
|
||||
}
|
||||
|
||||
// The worker receives a message to load the index and to query the index
|
||||
function handleMessage(message) {
|
||||
var type = message.data.type;
|
||||
var id = message.data.id;
|
||||
var payload = message.data.payload;
|
||||
switch(type) {
|
||||
case 'load-index':
|
||||
makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
|
||||
index = createIndex(loadIndex(searchInfo));
|
||||
self.postMessage({type: type, id: id, payload: true});
|
||||
});
|
||||
break;
|
||||
case 'query-index':
|
||||
self.postMessage({type: type, id: id, payload: {query: payload, results: queryIndex(payload)}});
|
||||
break;
|
||||
default:
|
||||
self.postMessage({type: type, id: id, payload: {error: 'invalid message type'}})
|
||||
}
|
||||
}
|
||||
|
||||
// Use XHR to make a request to the server
|
||||
function makeRequest(url, callback) {
|
||||
|
||||
// The JSON file that is loaded should be an array of PageInfo:
|
||||
var searchDataRequest = new XMLHttpRequest();
|
||||
searchDataRequest.onload = function() {
|
||||
callback(JSON.parse(this.responseText));
|
||||
};
|
||||
searchDataRequest.open('GET', url);
|
||||
searchDataRequest.send();
|
||||
}
|
||||
|
||||
|
||||
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
||||
function loadIndex(searchInfo /*: SearchInfo */) {
|
||||
return function(index) {
|
||||
// Store the pages data to be used in mapping query results back to pages
|
||||
// Add search terms from each page to the search index
|
||||
searchInfo.forEach(function(page /*: PageInfo */) {
|
||||
index.add(page);
|
||||
pages[page.path] = page;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Query the index and return the processed results
|
||||
function queryIndex(query) {
|
||||
try {
|
||||
if (query.length) {
|
||||
var results = index.search(query);
|
||||
if (results.length === 0) {
|
||||
// Add a relaxed search in the title for the first word in the query
|
||||
// E.g. if the search is "ngCont guide" then we search for "ngCont guide titleWords:ngCont*"
|
||||
var titleQuery = 'titleWords:*' + query.split(' ', 1)[0] + '*';
|
||||
results = index.search(query + ' ' + titleQuery);
|
||||
}
|
||||
// Map the hits into info about each page to be returned as results
|
||||
return results.map(function(hit) { return pages[hit.ref]; });
|
||||
}
|
||||
} catch(e) {
|
||||
// If the search query cannot be parsed the index throws an error
|
||||
// Log it and recover
|
||||
console.log(e);
|
||||
}
|
||||
return [];
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import { WebWorkerMessage } from '../shared/web-worker-message';
|
||||
import * as lunr from 'lunr';
|
||||
|
||||
const SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
|
||||
let index: lunr.Index;
|
||||
const pages: SearchInfo = {};
|
||||
|
||||
interface SearchInfo {
|
||||
[key: string]: PageInfo;
|
||||
}
|
||||
|
||||
interface PageInfo {
|
||||
path: string;
|
||||
type: string;
|
||||
titleWords: string;
|
||||
keyWords: string;
|
||||
}
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
|
||||
// Create the lunr index - the docs should be an array of objects, each object containing
|
||||
// the path and search terms for a page
|
||||
function createIndex(loadIndex: IndexLoader): lunr.Index {
|
||||
// The lunr typings are missing QueryLexer so we have to add them here manually.
|
||||
const queryLexer = (lunr as any as { QueryLexer: { termSeparator: RegExp } }).QueryLexer;
|
||||
queryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
|
||||
return lunr(/** @this */function () {
|
||||
this.ref('path');
|
||||
this.field('titleWords', { boost: 10 });
|
||||
this.field('headingWords', { boost: 5 });
|
||||
this.field('members', { boost: 4 });
|
||||
this.field('keywords', { boost: 2 });
|
||||
loadIndex(this);
|
||||
});
|
||||
}
|
||||
|
||||
// The worker receives a message to load the index and to query the index
|
||||
function handleMessage(message: { data: WebWorkerMessage }): void {
|
||||
const type = message.data.type;
|
||||
const id = message.data.id;
|
||||
const payload = message.data.payload;
|
||||
switch (type) {
|
||||
case 'load-index':
|
||||
makeRequest(SEARCH_TERMS_URL, function (searchInfo: PageInfo[]) {
|
||||
index = createIndex(loadIndex(searchInfo));
|
||||
postMessage({ type: type, id: id, payload: true });
|
||||
});
|
||||
break;
|
||||
case 'query-index':
|
||||
postMessage({ type: type, id: id, payload: { query: payload, results: queryIndex(payload) } });
|
||||
break;
|
||||
default:
|
||||
postMessage({ type: type, id: id, payload: { error: 'invalid message type' } })
|
||||
}
|
||||
}
|
||||
|
||||
// Use XHR to make a request to the server
|
||||
function makeRequest(url: string, callback: (response: any) => void): void {
|
||||
|
||||
// The JSON file that is loaded should be an array of PageInfo:
|
||||
const searchDataRequest = new XMLHttpRequest();
|
||||
searchDataRequest.onload = function () {
|
||||
callback(JSON.parse(this.responseText));
|
||||
};
|
||||
searchDataRequest.open('GET', url);
|
||||
searchDataRequest.send();
|
||||
}
|
||||
|
||||
|
||||
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
||||
function loadIndex(pagesData: PageInfo[]): IndexLoader {
|
||||
return (indexBuilder: lunr.Builder) => {
|
||||
// Store the pages data to be used in mapping query results back to pages
|
||||
// Add search terms from each page to the search index
|
||||
pagesData.forEach(page => {
|
||||
indexBuilder.add(page);
|
||||
pages[page.path] = page;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Query the index and return the processed results
|
||||
function queryIndex(query: string): PageInfo[] {
|
||||
try {
|
||||
if (query.length) {
|
||||
let results = index.search(query);
|
||||
if (results.length === 0) {
|
||||
// Add a relaxed search in the title for the first word in the query
|
||||
// E.g. if the search is "ngCont guide" then we search for "ngCont guide titleWords:ngCont*"
|
||||
const titleQuery = 'titleWords:*' + query.split(' ', 1)[0] + '*';
|
||||
results = index.search(query + ' ' + titleQuery);
|
||||
}
|
||||
// Map the hits into info about each page to be returned as results
|
||||
return results.map(function (hit) { return pages[hit.ref]; });
|
||||
}
|
||||
} catch (e) {
|
||||
// If the search query cannot be parsed the index throws an error
|
||||
// Log it and recover
|
||||
console.log(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
type IndexLoader = (indexBuilder: lunr.Builder) => void;
|
|
@ -25,11 +25,11 @@ describe('SearchService', () => {
|
|||
|
||||
describe('initWorker', () => {
|
||||
it('should create the worker and load the index after the specified delay', fakeAsync(() => {
|
||||
service.initWorker('some/url', 100);
|
||||
service.initWorker(100);
|
||||
expect(WebWorkerClient.create).not.toHaveBeenCalled();
|
||||
expect(mockWorker.sendMessage).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(WebWorkerClient.create).toHaveBeenCalledWith('some/url', jasmine.any(NgZone));
|
||||
expect(WebWorkerClient.create).toHaveBeenCalledWith(jasmine.any(Worker), jasmine.any(NgZone));
|
||||
expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
|
||||
}));
|
||||
});
|
||||
|
@ -37,7 +37,7 @@ describe('SearchService', () => {
|
|||
describe('search', () => {
|
||||
beforeEach(() => {
|
||||
// We must initialize the service before calling connectSearches
|
||||
service.initWorker('some/url', 1000);
|
||||
service.initWorker(1000);
|
||||
// Simulate the index being ready so that searches get sent to the worker
|
||||
(service as any).ready = of(true);
|
||||
});
|
||||
|
|
|
@ -16,10 +16,9 @@ export class SearchService {
|
|||
* initial rendering of the web page. Triggering a search will override this delay and cause the index to be
|
||||
* loaded immediately.
|
||||
*
|
||||
* @param workerUrl the url of the WebWorker script that runs the searches
|
||||
* @param initDelay the number of milliseconds to wait before we load the WebWorker and generate the search index
|
||||
*/
|
||||
initWorker(workerUrl: string, initDelay: number) {
|
||||
initWorker(initDelay: number) {
|
||||
// Wait for the initDelay or the first search
|
||||
const ready = this.ready = race<any>(
|
||||
timer(initDelay),
|
||||
|
@ -28,7 +27,8 @@ export class SearchService {
|
|||
.pipe(
|
||||
concatMap(() => {
|
||||
// Create the worker and load the index
|
||||
this.worker = WebWorkerClient.create(workerUrl, this.zone);
|
||||
const worker = new Worker('./search-worker', { type: 'module' });
|
||||
this.worker = WebWorkerClient.create(worker, this.zone);
|
||||
return this.worker.sendMessage<boolean>('load-index');
|
||||
}),
|
||||
publishReplay(1),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface WebWorkerMessage {
|
||||
type: string;
|
||||
payload: any;
|
||||
id?: number;
|
||||
}
|
|
@ -1,17 +1,12 @@
|
|||
import {NgZone} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
export interface WebWorkerMessage {
|
||||
type: string;
|
||||
payload: any;
|
||||
id?: number;
|
||||
}
|
||||
import {WebWorkerMessage} from './web-worker-message';
|
||||
|
||||
export class WebWorkerClient {
|
||||
private nextId = 0;
|
||||
|
||||
static create(workerUrl: string, zone: NgZone) {
|
||||
return new WebWorkerClient(new Worker(workerUrl), zone);
|
||||
static create(worker: Worker, zone: NgZone) {
|
||||
return new WebWorkerClient(worker, zone);
|
||||
}
|
||||
|
||||
private constructor(private worker: Worker, private zone: NgZone) {
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "es2015",
|
||||
"baseUrl": "",
|
||||
"types": [],
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
],
|
||||
"importHelpers": true
|
||||
},
|
||||
"exclude": [
|
||||
"testing/**/*",
|
||||
"test.ts",
|
||||
"test-specs.ts",
|
||||
"**/*.spec.ts"
|
||||
"**/*.spec.ts",
|
||||
"**/*-worker.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"disableTypeScriptVersionCheck": true
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "es2015",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom",
|
||||
"webworker"
|
||||
],
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/worker",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"webworker"
|
||||
],
|
||||
"types": [
|
||||
"lunr"
|
||||
],
|
||||
},
|
||||
"include": ["**/*-worker.ts"]
|
||||
}
|
|
@ -372,6 +372,11 @@
|
|||
dependencies:
|
||||
"@types/jasmine" "*"
|
||||
|
||||
"@types/lunr@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/lunr/-/lunr-2.3.2.tgz#d4a51703315ed0e53c43257216f1014ce6491562"
|
||||
integrity sha512-zcUZYquYDUEegRRPQtkZ068U9CoIjW6pJMYCVDRK25r76FEWvMm1oHqZQUfQh4ayIZ42lipXOpXEiAtGXc1XUg==
|
||||
|
||||
"@types/mkdirp@^0.3.29":
|
||||
version "0.3.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066"
|
||||
|
|
Loading…
Reference in New Issue