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",
|
"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",
|
||||||
{
|
{
|
||||||
|
|
|
@ -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"
|
||||||
],
|
],
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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', () => {
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface WebWorkerMessage {
|
||||||
|
type: string;
|
||||||
|
payload: any;
|
||||||
|
id?: number;
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
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"
|
||||||
|
|
Loading…
Reference in New Issue