build(docs-infra): convert `search-worker.js` to TypeScript (#29764)

PR Close #29764
This commit is contained in:
Filipe Silva 2019-04-15 10:15:43 +01:00 committed by Alex Rickabaugh
parent ee603a3b01
commit 3a836c362d
14 changed files with 162 additions and 132 deletions

View File

@ -21,6 +21,7 @@
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json", "tsConfig": "src/tsconfig.app.json",
"webWorkerTsConfig": "src/tsconfig.worker.json",
"aot": true, "aot": true,
"optimization": true, "optimization": true,
"buildOptimizer": true, "buildOptimizer": true,
@ -35,7 +36,6 @@
"assets": [ "assets": [
"src/assets", "src/assets",
"src/generated", "src/generated",
"src/app/search/search-worker.js",
"src/pwa-manifest.json", "src/pwa-manifest.json",
"src/google385281288605d160.html", "src/google385281288605d160.html",
{ {
@ -123,6 +123,7 @@
"karmaConfig": "src/karma.conf.js", "karmaConfig": "src/karma.conf.js",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json", "tsConfig": "src/tsconfig.spec.json",
"webWorkerTsConfig": "src/tsconfig.worker.json",
"scripts": [], "scripts": [],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"
@ -130,7 +131,6 @@
"assets": [ "assets": [
"src/assets", "src/assets",
"src/generated", "src/generated",
"src/app/search/search-worker.js",
"src/pwa-manifest.json", "src/pwa-manifest.json",
"src/google385281288605d160.html", "src/google385281288605d160.html",
{ {

View File

@ -9,10 +9,8 @@
"files": [ "files": [
"/index.html", "/index.html",
"/pwa-manifest.json", "/pwa-manifest.json",
"/app/search/search-worker.js",
"/assets/images/favicons/favicon.ico", "/assets/images/favicons/favicon.ico",
"/assets/js/*.js", "/assets/js/*.js",
"/generated/lunr.min.js",
"/*.css", "/*.css",
"/*.js" "/*.js"
], ],

View File

@ -26,7 +26,7 @@
"e2e": "ng e2e --no-webdriver-update", "e2e": "ng e2e --no-webdriver-update",
"presetup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env && yarn ~~clean-generated && yarn boilerplate:remove", "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", "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", "presetup-local": "yarn presetup",
"setup-local": "yarn aio-use-local && yarn example-use-local", "setup-local": "yarn aio-use-local && yarn example-use-local",
"postsetup-local": "yarn postsetup", "postsetup-local": "yarn postsetup",
@ -69,8 +69,7 @@
"~~build": "ng build --configuration=stable", "~~build": "ng build --configuration=stable",
"post~~build": "yarn build-404-page", "post~~build": "yarn build-404-page",
"~~build-ie-polyfills": "webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production", "~~build-ie-polyfills": "webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production",
"~~http-server": "http-server", "~~http-server": "http-server"
"~~minify-lunr": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/generated/lunr.min.js --source-map"
}, },
"engines": { "engines": {
"node": ">=10.9.0 <11.0.0", "node": ">=10.9.0 <11.0.0",
@ -89,6 +88,7 @@
"@angular/platform-browser-dynamic": "^7.0.0", "@angular/platform-browser-dynamic": "^7.0.0",
"@angular/router": "^7.0.0", "@angular/router": "^7.0.0",
"@angular/service-worker": "^7.0.0", "@angular/service-worker": "^7.0.0",
"@types/lunr": "^2.3.2",
"@webcomponents/custom-elements": "^1.2.0", "@webcomponents/custom-elements": "^1.2.0",
"classlist.js": "^1.1.20150312", "classlist.js": "^1.1.20150312",
"core-js": "^2.4.1", "core-js": "^2.4.1",

View File

@ -110,7 +110,7 @@ export class AppComponent implements OnInit {
// Do not initialize the search on browsers that lack web worker support // Do not initialize the search on browsers that lack web worker support
if ('Worker' in window) { if ('Worker' in window) {
// Delay initialization by up to 2 seconds // Delay initialization by up to 2 seconds
this.searchService.initWorker('app/search/search-worker.js', 2000); this.searchService.initWorker(2000);
} }
this.onResize(window.innerWidth); this.onResize(window.innerWidth);

View File

@ -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 [];
}

View File

@ -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;

View File

@ -25,11 +25,11 @@ describe('SearchService', () => {
describe('initWorker', () => { describe('initWorker', () => {
it('should create the worker and load the index after the specified delay', fakeAsync(() => { 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(WebWorkerClient.create).not.toHaveBeenCalled();
expect(mockWorker.sendMessage).not.toHaveBeenCalled(); expect(mockWorker.sendMessage).not.toHaveBeenCalled();
tick(100); 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'); expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
})); }));
}); });
@ -37,7 +37,7 @@ describe('SearchService', () => {
describe('search', () => { describe('search', () => {
beforeEach(() => { beforeEach(() => {
// We must initialize the service before calling connectSearches // 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 // Simulate the index being ready so that searches get sent to the worker
(service as any).ready = of(true); (service as any).ready = of(true);
}); });

View File

@ -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 * initial rendering of the web page. Triggering a search will override this delay and cause the index to be
* loaded immediately. * 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 * @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 // Wait for the initDelay or the first search
const ready = this.ready = race<any>( const ready = this.ready = race<any>(
timer(initDelay), timer(initDelay),
@ -28,7 +27,8 @@ export class SearchService {
.pipe( .pipe(
concatMap(() => { concatMap(() => {
// Create the worker and load the index // 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'); return this.worker.sendMessage<boolean>('load-index');
}), }),
publishReplay(1), publishReplay(1),

View File

@ -0,0 +1,5 @@
export interface WebWorkerMessage {
type: string;
payload: any;
id?: number;
}

View File

@ -1,17 +1,12 @@
import {NgZone} from '@angular/core'; import {NgZone} from '@angular/core';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {WebWorkerMessage} from './web-worker-message';
export interface WebWorkerMessage {
type: string;
payload: any;
id?: number;
}
export class WebWorkerClient { export class WebWorkerClient {
private nextId = 0; private nextId = 0;
static create(workerUrl: string, zone: NgZone) { static create(worker: Worker, zone: NgZone) {
return new WebWorkerClient(new Worker(workerUrl), zone); return new WebWorkerClient(worker, zone);
} }
private constructor(private worker: Worker, private zone: NgZone) { private constructor(private worker: Worker, private zone: NgZone) {

View File

@ -1,19 +1,23 @@
{ {
"extends": "../tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../out-tsc/app", "outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "", "baseUrl": "",
"types": [], "types": [],
"lib": [
"es2018",
"dom"
],
"importHelpers": true "importHelpers": true
}, },
"exclude": [ "exclude": [
"testing/**/*", "testing/**/*",
"test.ts", "test.ts",
"test-specs.ts", "test-specs.ts",
"**/*.spec.ts" "**/*.spec.ts",
"**/*-worker.ts"
], ],
"angularCompilerOptions": { "angularCompilerOptions": {
"disableTypeScriptVersionCheck": true "disableTypeScriptVersionCheck": true
} }
} }

11
aio/src/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "es2015",
"lib": [
"es2018",
"dom",
"webworker"
],
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/worker",
"lib": [
"es2018",
"webworker"
],
"types": [
"lunr"
],
},
"include": ["**/*-worker.ts"]
}

View File

@ -372,6 +372,11 @@
dependencies: dependencies:
"@types/jasmine" "*" "@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": "@types/mkdirp@^0.3.29":
version "0.3.29" version "0.3.29"
resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066" resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066"