mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Workbox 5 in gulp (#5843)
This commit is contained in:
parent
10358abbec
commit
581fafdcc9
@ -20,7 +20,7 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-service-worker-dev",
|
"gen-service-worker-app-dev",
|
||||||
"gen-icons-json",
|
"gen-icons-json",
|
||||||
"gen-pages-dev",
|
"gen-pages-dev",
|
||||||
"gen-index-app-dev",
|
"gen-index-app-dev",
|
||||||
@ -46,7 +46,7 @@ gulp.task(
|
|||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-pages-prod",
|
"gen-pages-prod",
|
||||||
"gen-index-app-prod",
|
"gen-index-app-prod",
|
||||||
"gen-service-worker-prod"
|
"gen-service-worker-app-prod"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -5,13 +5,15 @@
|
|||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const config = require("../paths.js");
|
const workboxBuild = require("workbox-build");
|
||||||
|
const sourceMapUrl = require("source-map-url");
|
||||||
|
const paths = require("../paths.js");
|
||||||
|
|
||||||
const swPath = path.resolve(config.root, "service_worker.js");
|
const swDest = path.resolve(paths.root, "service_worker.js");
|
||||||
|
|
||||||
const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n");
|
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
|
||||||
|
|
||||||
gulp.task("gen-service-worker-dev", (done) => {
|
gulp.task("gen-service-worker-app-dev", (done) => {
|
||||||
writeSW(
|
writeSW(
|
||||||
`
|
`
|
||||||
console.debug('Service worker disabled in development');
|
console.debug('Service worker disabled in development');
|
||||||
@ -24,10 +26,58 @@ self.addEventListener('install', (event) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("gen-service-worker-prod", (done) => {
|
gulp.task("gen-service-worker-app-prod", async () => {
|
||||||
fs.copySync(
|
const workboxManifest = await workboxBuild.getManifest({
|
||||||
path.resolve(config.output, "service_worker.js"),
|
// Files that mach this pattern will be considered unique and skip revision check
|
||||||
path.resolve(config.root, "service_worker.js")
|
// ignore JS files + translation files
|
||||||
);
|
dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
|
||||||
done();
|
|
||||||
|
globDirectory: paths.root,
|
||||||
|
globPatterns: [
|
||||||
|
"frontend_latest/*.js",
|
||||||
|
// Cache all English translations because we catch them as fallback
|
||||||
|
// Using pattern to match hash instead of * to avoid caching en-GB
|
||||||
|
"static/translations/**/en-+([a-f0-9]).json",
|
||||||
|
// Icon shown on splash screen
|
||||||
|
"static/icons/favicon-192x192.png",
|
||||||
|
"static/icons/favicon.ico",
|
||||||
|
// Common fonts
|
||||||
|
"static/fonts/roboto/Roboto-Light.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Medium.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Regular.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Bold.woff2",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const warning of workboxManifest.warnings) {
|
||||||
|
console.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace `null` with 0 for better compression
|
||||||
|
for (const entry of workboxManifest.manifestEntries) {
|
||||||
|
if (entry.revision === null) {
|
||||||
|
entry.revision = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = require(path.resolve(paths.output, "manifest.json"));
|
||||||
|
|
||||||
|
// Write bundled source file
|
||||||
|
let serviceWorkerContent = fs.readFileSync(
|
||||||
|
paths.root + manifest["service_worker.js"],
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
// remove source map and add WB manifest
|
||||||
|
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
|
||||||
|
serviceWorkerContent = serviceWorkerContent.replace(
|
||||||
|
"WB_MANIFEST",
|
||||||
|
JSON.stringify(workboxManifest.manifestEntries)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write new file to root
|
||||||
|
fs.writeFileSync(swDest, serviceWorkerContent);
|
||||||
|
|
||||||
|
// Delete old file from frontend_latest
|
||||||
|
fs.removeSync(paths.root + manifest["service_worker.js"]);
|
||||||
|
fs.removeSync(paths.root + manifest["service_worker.js.map"]);
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
|
||||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||||
const paths = require("./paths.js");
|
const paths = require("./paths.js");
|
||||||
const env = require("./env.js");
|
const env = require("./env.js");
|
||||||
@ -107,8 +106,9 @@ const createWebpackConfig = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||||
const config = createWebpackConfig({
|
return createWebpackConfig({
|
||||||
entry: {
|
entry: {
|
||||||
|
service_worker: "./src/entrypoints/service_worker.ts",
|
||||||
app: "./src/entrypoints/app.ts",
|
app: "./src/entrypoints/app.ts",
|
||||||
authorize: "./src/entrypoints/authorize.ts",
|
authorize: "./src/entrypoints/authorize.ts",
|
||||||
onboarding: "./src/entrypoints/onboarding.ts",
|
onboarding: "./src/entrypoints/onboarding.ts",
|
||||||
@ -121,48 +121,6 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
|||||||
latestBuild,
|
latestBuild,
|
||||||
isStatsBuild,
|
isStatsBuild,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (latestBuild) {
|
|
||||||
// Create an object mapping browser urls to their paths during build
|
|
||||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
|
||||||
const workBoxTranslationsTemplatedURLs = {};
|
|
||||||
const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
|
|
||||||
|
|
||||||
// core
|
|
||||||
workBoxTranslationsTemplatedURLs[
|
|
||||||
`/static/translations/${englishFilename}`
|
|
||||||
] = `build-translations/output/${englishFilename}`;
|
|
||||||
|
|
||||||
translationMetadata.fragments.forEach((fragment) => {
|
|
||||||
workBoxTranslationsTemplatedURLs[
|
|
||||||
`/static/translations/${fragment}/${englishFilename}`
|
|
||||||
] = `build-translations/output/${fragment}/${englishFilename}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
config.plugins.push(
|
|
||||||
new WorkboxPlugin.InjectManifest({
|
|
||||||
swSrc: "./src/entrypoints/service-worker-hass.js",
|
|
||||||
swDest: "service_worker.js",
|
|
||||||
importWorkboxFrom: "local",
|
|
||||||
include: [/\.js$/],
|
|
||||||
templatedURLs: {
|
|
||||||
...workBoxTranslationsTemplatedURLs,
|
|
||||||
"/static/icons/favicon-192x192.png":
|
|
||||||
"public/icons/favicon-192x192.png",
|
|
||||||
"/static/fonts/roboto/Roboto-Light.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Medium.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Regular.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Bold.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||||
|
@ -114,6 +114,10 @@
|
|||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
|
"workbox-core": "^5.1.3",
|
||||||
|
"workbox-precaching": "^5.1.3",
|
||||||
|
"workbox-routing": "^5.1.3",
|
||||||
|
"workbox-strategies": "^5.1.3",
|
||||||
"xss": "^1.0.6"
|
"xss": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -177,6 +181,7 @@
|
|||||||
"reify": "^0.18.1",
|
"reify": "^0.18.1",
|
||||||
"require-dir": "^1.2.0",
|
"require-dir": "^1.2.0",
|
||||||
"sinon": "^7.3.1",
|
"sinon": "^7.3.1",
|
||||||
|
"source-map-url": "^0.4.0",
|
||||||
"terser-webpack-plugin": "^1.2.3",
|
"terser-webpack-plugin": "^1.2.3",
|
||||||
"ts-lit-plugin": "^1.1.10",
|
"ts-lit-plugin": "^1.1.10",
|
||||||
"ts-mocha": "^6.0.0",
|
"ts-mocha": "^6.0.0",
|
||||||
@ -188,7 +193,7 @@
|
|||||||
"webpack-cli": "^3.3.9",
|
"webpack-cli": "^3.3.9",
|
||||||
"webpack-dev-server": "^3.10.3",
|
"webpack-dev-server": "^3.10.3",
|
||||||
"webpack-manifest-plugin": "^2.0.4",
|
"webpack-manifest-plugin": "^2.0.4",
|
||||||
"workbox-webpack-plugin": "^4.1.1",
|
"workbox-build": "^5.1.3",
|
||||||
"workerize-loader": "^1.1.0"
|
"workerize-loader": "^1.1.0"
|
||||||
},
|
},
|
||||||
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
||||||
|
@ -1,40 +1,51 @@
|
|||||||
/*
|
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||||
This file is not run through webpack, but instead is directly manipulated
|
// eslint-disable-next-line spaced-comment
|
||||||
by Workbox Webpack plugin. So we cannot use __DEV__ or other constants.
|
/// <reference path="../types/service-worker.d.ts" />
|
||||||
*/
|
/* eslint-env serviceworker */
|
||||||
/* global workbox clients */
|
import {
|
||||||
|
CacheFirst,
|
||||||
|
StaleWhileRevalidate,
|
||||||
|
NetworkOnly,
|
||||||
|
} from "workbox-strategies";
|
||||||
|
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
|
||||||
|
import { registerRoute } from "workbox-routing";
|
||||||
|
import { cacheNames } from "workbox-core";
|
||||||
|
|
||||||
|
// Clean up caches from older workboxes and old service workers.
|
||||||
|
// Will help with cleaning up Workbox v4 stuff
|
||||||
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
function initRouting() {
|
function initRouting() {
|
||||||
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
|
precacheAndRoute(
|
||||||
|
// @ts-ignore
|
||||||
|
WB_MANIFEST
|
||||||
|
);
|
||||||
|
|
||||||
// Cache static content (including translations) on first access.
|
// Cache static content (including translations) on first access.
|
||||||
workbox.routing.registerRoute(
|
registerRoute(
|
||||||
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
|
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
|
||||||
new workbox.strategies.CacheFirst()
|
new CacheFirst()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get api from network.
|
// Get api from network.
|
||||||
workbox.routing.registerRoute(
|
registerRoute(
|
||||||
new RegExp(`${location.host}/(api|auth)/.*`),
|
new RegExp(`${location.host}/(api|auth)/.*`),
|
||||||
new workbox.strategies.NetworkOnly()
|
new NetworkOnly()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get manifest, service worker, onboarding from network.
|
// Get manifest, service worker, onboarding from network.
|
||||||
workbox.routing.registerRoute(
|
registerRoute(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`${location.host}/(service_worker.js|manifest.json|onboarding.html)`
|
`${location.host}/(service_worker.js|manifest.json|onboarding.html)`
|
||||||
),
|
),
|
||||||
new workbox.strategies.NetworkOnly()
|
new NetworkOnly()
|
||||||
);
|
);
|
||||||
|
|
||||||
// For rest of the files (on Home Assistant domain only) try both cache and network.
|
// For rest of the files (on Home Assistant domain only) try both cache and network.
|
||||||
// This includes the root "/" or "/states" response and user files from "/local".
|
// This includes the root "/" or "/states" response and user files from "/local".
|
||||||
// First access might bring stale data from cache, but a single refresh will bring updated
|
// First access might bring stale data from cache, but a single refresh will bring updated
|
||||||
// file.
|
// file.
|
||||||
workbox.routing.registerRoute(
|
registerRoute(new RegExp(`${location.host}/.*`), new StaleWhileRevalidate());
|
||||||
new RegExp(`${location.host}/.*`),
|
|
||||||
new workbox.strategies.StaleWhileRevalidate()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPushNotifications() {
|
function initPushNotifications() {
|
||||||
@ -149,7 +160,7 @@ function initPushNotifications() {
|
|||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
// Delete all runtime caching, so that index.html has to be refetched.
|
// Delete all runtime caching, so that index.html has to be refetched.
|
||||||
const cacheName = workbox.core.cacheNames.runtime;
|
const cacheName = cacheNames.runtime;
|
||||||
event.waitUntil(caches.delete(cacheName));
|
event.waitUntil(caches.delete(cacheName));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,9 +171,5 @@ self.addEventListener("message", (message) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
workbox.setConfig({
|
|
||||||
debug: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
initRouting();
|
initRouting();
|
||||||
initPushNotifications();
|
initPushNotifications();
|
126
src/types/service-worker.d.ts
vendored
Normal file
126
src/types/service-worker.d.ts
vendored
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// From https://gist.githubusercontent.com/tiernan/c18a380935e45a6d942ac1e88c5bbaf3/raw/1d103eb5882504505ccc84cbc9398ac20418ef8a/serviceworker.d.ts
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018, Tiernan Cridland
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
|
||||||
|
* granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*
|
||||||
|
* Service Worker Typings to supplement lib.webworker.ts
|
||||||
|
* @author Tiernan Cridland
|
||||||
|
* @email tiernanc@gmail.com
|
||||||
|
* @license: ISC
|
||||||
|
*
|
||||||
|
* lib.webworker.d.ts as well as an es5+ library (es5, es2015, etc) are required.
|
||||||
|
* Recommended to be used with a triple slash directive in the files requiring the typings only.
|
||||||
|
* e.g. your-service-worker.js, register-service-worker.js
|
||||||
|
* e.g. /// <reference path="path/to/serviceworker.d.ts" />
|
||||||
|
*/
|
||||||
|
/* eslint-disable */
|
||||||
|
// Registration
|
||||||
|
|
||||||
|
interface WorkerNavigator {
|
||||||
|
readonly serviceWorker: ServiceWorkerContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceWorkerContainer {
|
||||||
|
readonly controller: ServiceWorker;
|
||||||
|
readonly ready: Promise<ServiceWorkerRegistration>;
|
||||||
|
oncontrollerchange:
|
||||||
|
| ((this: ServiceWorkerContainer, event: Event) => any)
|
||||||
|
| null;
|
||||||
|
onerror: ((this: ServiceWorkerContainer, event?: Event) => any) | null;
|
||||||
|
onmessage:
|
||||||
|
| ((this: ServiceWorkerContainer, event: ServiceWorkerMessageEvent) => any)
|
||||||
|
| null;
|
||||||
|
getRegistration(scope?: string): Promise<ServiceWorkerRegistration>;
|
||||||
|
getRegistrations(): Promise<ServiceWorkerRegistration[]>;
|
||||||
|
register(
|
||||||
|
url: string,
|
||||||
|
options?: ServiceWorkerRegistrationOptions
|
||||||
|
): Promise<ServiceWorkerRegistration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceWorkerMessageEvent extends Event {
|
||||||
|
readonly data: any;
|
||||||
|
readonly lastEventId: string;
|
||||||
|
readonly origin: string;
|
||||||
|
readonly ports: ReadonlyArray<MessagePort> | null;
|
||||||
|
readonly source: ServiceWorker | MessagePort | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceWorkerRegistrationOptions {
|
||||||
|
scope?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client API
|
||||||
|
|
||||||
|
interface Client {
|
||||||
|
readonly frameType: ClientFrameType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientFrameType = "auxiliary" | "top-level" | "nested" | "none";
|
||||||
|
|
||||||
|
// Events
|
||||||
|
|
||||||
|
interface ActivateEvent extends ExtendableEvent {}
|
||||||
|
|
||||||
|
interface InstallEvent extends ExtendableEvent {
|
||||||
|
readonly activeWorker: ServiceWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch API
|
||||||
|
|
||||||
|
interface Body {
|
||||||
|
readonly body: ReadableStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Headers {
|
||||||
|
entries(): string[][];
|
||||||
|
keys(): string[];
|
||||||
|
values(): string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response extends Body {
|
||||||
|
readonly useFinalURL: boolean;
|
||||||
|
clone(): Response;
|
||||||
|
error(): Response;
|
||||||
|
redirect(): Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification API
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
readonly actions: NotificationAction[];
|
||||||
|
readonly requireInteraction: boolean;
|
||||||
|
readonly silent: boolean;
|
||||||
|
readonly tag: string;
|
||||||
|
readonly renotify: boolean;
|
||||||
|
readonly timestamp: number;
|
||||||
|
readonly title: string;
|
||||||
|
readonly vibrate: number[];
|
||||||
|
close(): void;
|
||||||
|
requestPermission(): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationAction {}
|
||||||
|
|
||||||
|
// ServiceWorkerGlobalScope
|
||||||
|
|
||||||
|
declare var clients: Clients;
|
||||||
|
declare var onactivate: ((event?: ActivateEvent) => any) | null;
|
||||||
|
declare var onfetch: ((event?: FetchEvent) => any) | null;
|
||||||
|
declare var oninstall: ((event?: InstallEvent) => any) | null;
|
||||||
|
declare var onnotificationclick: ((event?: NotificationEvent) => any) | null;
|
||||||
|
declare var onnotificationclose: ((event?: NotificationEvent) => any) | null;
|
||||||
|
declare var onpush: ((event?: PushEvent) => any) | null;
|
||||||
|
declare var onpushsubscriptionchange: (() => any) | null;
|
||||||
|
declare var onsync: ((event?: SyncEvent) => any) | null;
|
||||||
|
declare var registration: ServiceWorkerRegistration;
|
||||||
|
|
||||||
|
declare function skipWaiting(): void;
|
@ -3,7 +3,7 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": ["es2017", "dom", "dom.iterable"],
|
"lib": ["es2017", "dom", "dom.iterable", "WebWorker"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user