diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js index 0563fc0e9e..0536b14a30 100644 --- a/build-scripts/gulp/app.js +++ b/build-scripts/gulp/app.js @@ -37,7 +37,11 @@ gulp.task( "clean", gulp.parallel("gen-icons", "build-translations"), "copy-static", - gulp.parallel("webpack-prod-app", "compress-static"), + gulp.parallel( + "webpack-prod-app", + // Do not compress static files in CI, it's SLOW. + ...(process.env.CI === "true" ? [] : ["compress-static"]) + ), gulp.parallel( "gen-pages-prod", "gen-index-html-prod", diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index 8eb98bef36..9008b63f58 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -95,7 +95,7 @@ gulp.task("copy-static", (done) => { done(); }); -gulp.task("compress-static", () => compressStatic(paths.root)); +gulp.task("compress-static", () => compressStatic(paths.static)); gulp.task("copy-static-demo", (done) => { // Copy app static files diff --git a/package.json b/package.json index 5b3213a8a7..3cf476e4b9 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "es6-object-assign": "^1.1.0", "fecha": "^3.0.2", "hls.js": "^0.12.4", - "home-assistant-js-websocket": "^3.4.0", + "home-assistant-js-websocket": "^4.1.1", "intl-messageformat": "^2.2.0", "jquery": "^3.3.1", "js-yaml": "^3.13.0", diff --git a/src/data/auth.ts b/src/data/auth.ts index 77c7b40d17..c768b95f41 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -14,6 +14,8 @@ export interface SignedPath { path: string; } +export const hassUrl = `${location.protocol}//${location.host}`; + export const getSignedPath = ( hass: HomeAssistant, path: string diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 1acf44f39b..f54a84d6da 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -1,6 +1,17 @@ import { HomeAssistant } from "../types"; import { createCollection } from "home-assistant-js-websocket"; import { debounce } from "../common/util/debounce"; +import { LocalizeFunc } from "../common/translations/localize"; + +export interface ConfigEntry { + entry_id: string; + domain: string; + title: string; + source: string; + state: string; + connection_class: string; + supports_options: boolean; +} export interface FieldSchema { name: string; @@ -11,7 +22,10 @@ export interface FieldSchema { export interface ConfigFlowProgress { flow_id: string; handler: string; - context: { [key: string]: any }; + context: { + title_placeholders: { [key: string]: string }; + [key: string]: any; + }; } export interface ConfigFlowStepForm { @@ -106,3 +120,23 @@ export const subscribeConfigFlowInProgress = ( hass.connection, onChange ); + +export const getConfigEntries = (hass: HomeAssistant) => + hass.callApi("GET", "config/config_entries/entry"); + +export const localizeConfigFlowTitle = ( + localize: LocalizeFunc, + flow: ConfigFlowProgress +) => { + const placeholders = flow.context.title_placeholders || {}; + const placeholderKeys = Object.keys(placeholders); + if (placeholderKeys.length === 0) { + return localize(`component.${flow.handler}.config.title`); + } + const args: string[] = []; + placeholderKeys.forEach((key) => { + args.push(key); + args.push(placeholders[key]); + }); + return localize(`component.${flow.handler}.config.flow_title`, ...args); +}; diff --git a/src/data/onboarding.ts b/src/data/onboarding.ts index 46e6775989..80e6dce995 100644 --- a/src/data/onboarding.ts +++ b/src/data/onboarding.ts @@ -1,12 +1,17 @@ import { handleFetchPromise } from "../util/hass-call-api"; +import { HomeAssistant } from "../types"; export interface OnboardingUserStepResponse { auth_code: string; } +export interface OnboardingIntegrationStepResponse { + auth_code: string; +} + export interface OnboardingResponses { user: OnboardingUserStepResponse; - bla: number; + integration: OnboardingIntegrationStepResponse; } export type ValidOnboardingStep = keyof OnboardingResponses; @@ -24,6 +29,7 @@ export const onboardUserStep = (params: { name: string; username: string; password: string; + language: string; }) => handleFetchPromise( fetch("/api/onboarding/users", { @@ -32,3 +38,13 @@ export const onboardUserStep = (params: { body: JSON.stringify(params), }) ); + +export const onboardIntegrationStep = ( + hass: HomeAssistant, + params: { client_id: string } +) => + hass.callApi( + "POST", + "onboarding/integration", + params + ); diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index be4b268389..ac38ba95ed 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -169,6 +169,18 @@ class StepFlowCreateEntry extends LitElement { .buttons > *:last-child { margin-left: auto; } + paper-dropdown-menu-light { + cursor: pointer; + } + paper-item { + cursor: pointer; + white-space: nowrap; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + .device { + width: auto; + } + } `, ]; } diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index 1de10ea11f..17f2794167 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -14,6 +14,7 @@ import { subscribePanels } from "../data/ws-panels"; import { subscribeThemes } from "../data/ws-themes"; import { subscribeUser } from "../data/ws-user"; import { HomeAssistant } from "../types"; +import { hassUrl } from "../data/auth"; declare global { interface Window { @@ -21,7 +22,6 @@ declare global { } } -const hassUrl = `${location.protocol}//${location.host}`; const isExternal = location.search.includes("external_auth=1"); const authProm = isExternal diff --git a/src/html/onboarding.html.template b/src/html/onboarding.html.template index 3febae22e3..80b3f2a39f 100644 --- a/src/html/onboarding.html.template +++ b/src/html/onboarding.html.template @@ -44,7 +44,7 @@ Home Assistant - Initializing + <%= renderTemplate('_js_base') %> diff --git a/src/mixins/localize-lite-base-mixin.ts b/src/mixins/localize-lite-base-mixin.ts index 91a81ef5b6..a4c9791c28 100644 --- a/src/mixins/localize-lite-base-mixin.ts +++ b/src/mixins/localize-lite-base-mixin.ts @@ -31,10 +31,10 @@ export const localizeLiteBaseMixin = (superClass) => return; } - this._updateResources(); + this._downloadResources(); } - private async _updateResources() { + private async _downloadResources() { const { language, data } = await getTranslation( this.translationFragment, this.language diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 0c9398a67e..b37040bd45 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -1,22 +1,29 @@ import { - LitElement, html, PropertyValues, customElement, TemplateResult, property, } from "lit-element"; -import { genClientId } from "home-assistant-js-websocket"; +import { + getAuth, + createConnection, + genClientId, + Auth, +} from "home-assistant-js-websocket"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { OnboardingStep, ValidOnboardingStep, OnboardingResponses, + fetchOnboardingOverview, } from "../data/onboarding"; import { registerServiceWorker } from "../util/register-service-worker"; import { HASSDomEvent } from "../common/dom/fire_event"; import "./onboarding-create-user"; import "./onboarding-loading"; +import { hassUrl } from "../data/auth"; +import { HassElement } from "../state/hass-element"; interface OnboardingEvent { type: T; @@ -34,43 +41,55 @@ declare global { } @customElement("ha-onboarding") -class HaOnboarding extends litLocalizeLiteMixin(LitElement) { +class HaOnboarding extends litLocalizeLiteMixin(HassElement) { public translationFragment = "page-onboarding"; + @property() private _loading = false; @property() private _steps?: OnboardingStep[]; protected render(): TemplateResult | void { - if (!this._steps) { + const step = this._curStep()!; + + if (this._loading || !step) { return html` `; - } - - const step = this._steps.find((stp) => !stp.done)!; - - if (step.step === "user") { + } else if (step.step === "user") { return html` `; + } else if (step.step === "integration") { + return html` + + `; } } protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._fetchOnboardingSteps(); + import("./onboarding-integrations"); registerServiceWorker(false); this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev)); } + private _curStep() { + return this._steps ? this._steps.find((stp) => !stp.done) : undefined; + } + private async _fetchOnboardingSteps() { try { - const response = await window.stepsPromise; + const response = await (window.stepsPromise || fetchOnboardingOverview()); if (response.status === 404) { // We don't load the component when onboarding is done - document.location.href = "/"; + document.location.assign("/"); return; } @@ -78,7 +97,16 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) { if (steps.every((step) => step.done)) { // Onboarding is done! - document.location.href = "/"; + document.location.assign("/"); + return; + } + + if (steps[0].done) { + // First step is already done, so we need to get auth somewhere else. + const auth = await getAuth({ + hassUrl, + }); + await this._connectHass(auth); } this._steps = steps; @@ -91,20 +119,52 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) { ev: HASSDomEvent> ) { const stepResult = ev.detail; + this._steps = this._steps!.map((step) => + step.step === stepResult.type ? { ...step, done: true } : step + ); if (stepResult.type === "user") { const result = stepResult.result as OnboardingResponses["user"]; + this._loading = true; + try { + const auth = await getAuth({ + hassUrl, + authCode: result.auth_code, + }); + await this._connectHass(auth); + } catch (err) { + alert("Ah snap, something went wrong!"); + location.reload(); + } finally { + this._loading = false; + } + } else if (stepResult.type === "integration") { + const result = stepResult.result as OnboardingResponses["integration"]; + this._loading = true; + + // Revoke current auth token. + await this.hass!.auth.revoke(); + const state = btoa( JSON.stringify({ hassUrl: `${location.protocol}//${location.host}`, clientId: genClientId(), }) ); - document.location.href = `/?auth_callback=1&code=${encodeURIComponent( - result.auth_code - )}&state=${state}`; + document.location.assign( + `/?auth_callback=1&code=${encodeURIComponent( + result.auth_code + )}&state=${state}` + ); } } + + private async _connectHass(auth: Auth) { + const conn = await createConnection({ auth }); + this.initializeHass(auth, conn); + // Load config strings for integrations + (this as any)._loadFragmentTranslations(this.hass!.language, "config"); + } } declare global { diff --git a/src/onboarding/integration-badge.ts b/src/onboarding/integration-badge.ts new file mode 100644 index 0000000000..3035c8b893 --- /dev/null +++ b/src/onboarding/integration-badge.ts @@ -0,0 +1,86 @@ +import { + LitElement, + TemplateResult, + html, + customElement, + property, + CSSResult, + css, +} from "lit-element"; +import "../components/ha-icon"; + +@customElement("integration-badge") +class IntegrationBadge extends LitElement { + @property() public icon!: string; + @property() public title!: string; + @property() public badgeIcon?: string; + @property({ type: Boolean, reflect: true }) public clickable = false; + + protected render(): TemplateResult | void { + return html` +
+ + ${this.badgeIcon + ? html` + + ` + : ""} +
+
${this.title}
+ `; + } + + static get styles(): CSSResult { + return css` + :host { + display: inline-flex; + flex-direction: column; + text-align: center; + color: var(--primary-text-color); + } + + :host([clickable]) { + color: var(--primary-text-color); + } + + .icon { + position: relative; + margin: 0 auto 8px; + height: 40px; + width: 40px; + border-radius: 50%; + border: 1px solid var(--secondary-text-color); + display: flex; + align-items: center; + justify-content: center; + } + + :host([clickable]) .icon { + border-color: var(--primary-color); + border-width: 2px; + } + + .badge { + position: absolute; + color: var(--primary-color); + bottom: -5px; + right: -5px; + background-color: white; + border-radius: 50%; + width: 18px; + display: block; + height: 18px; + } + + .title { + min-height: 2.3em; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "integration-badge": IntegrationBadge; + } +} diff --git a/src/onboarding/onboarding-create-user.ts b/src/onboarding/onboarding-create-user.ts index e04fd07936..837bba1b7d 100644 --- a/src/onboarding/onboarding-create-user.ts +++ b/src/onboarding/onboarding-create-user.ts @@ -19,6 +19,7 @@ import { fireEvent } from "../common/dom/fire_event"; @customElement("onboarding-create-user") class OnboardingCreateUser extends LitElement { @property() public localize!: LocalizeFunc; + @property() public language!: string; @property() private _name = ""; @property() private _username = ""; @@ -173,6 +174,7 @@ class OnboardingCreateUser extends LitElement { name: this._name, username: this._username, password: this._password, + language: this.language, }); fireEvent(this, "onboarding-step", { diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts new file mode 100644 index 0000000000..bdbfc9c969 --- /dev/null +++ b/src/onboarding/onboarding-integrations.ts @@ -0,0 +1,196 @@ +import { + LitElement, + TemplateResult, + html, + customElement, + PropertyValues, + property, + CSSResult, + css, +} from "lit-element"; +import "@material/mwc-button/mwc-button"; +import { + loadConfigFlowDialog, + showConfigFlowDialog, +} from "../dialogs/config-flow/show-dialog-config-flow"; +import { HomeAssistant } from "../types"; +import { + getConfigFlowsInProgress, + getConfigEntries, + ConfigEntry, + ConfigFlowProgress, + localizeConfigFlowTitle, +} from "../data/config_entries"; +import { compare } from "../common/string/compare"; +import "./integration-badge"; +import { LocalizeFunc } from "../common/translations/localize"; +import { debounce } from "../common/util/debounce"; +import { fireEvent } from "../common/dom/fire_event"; +import { onboardIntegrationStep } from "../data/onboarding"; +import { genClientId } from "home-assistant-js-websocket"; + +@customElement("onboarding-integrations") +class OnboardingIntegrations extends LitElement { + @property() public hass!: HomeAssistant; + @property() public onboardingLocalize!: LocalizeFunc; + @property() private _entries?: ConfigEntry[]; + @property() private _discovered?: ConfigFlowProgress[]; + private _unsubEvents?: () => void; + + public connectedCallback() { + super.connectedCallback(); + this.hass.connection + .subscribeEvents( + debounce(() => this._loadData(), 500), + "config_entry_discovered" + ) + .then((unsub) => { + this._unsubEvents = unsub; + }); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsubEvents) { + this._unsubEvents(); + } + } + + protected render(): TemplateResult | void { + if (!this._entries || !this._discovered) { + return html``; + } + // Render discovered and existing entries together sorted by localized title. + const entries: Array<[string, TemplateResult]> = this._entries.map( + (entry) => { + const title = this.hass.localize( + `component.${entry.domain}.config.title` + ); + return [ + title, + html` + + `, + ]; + } + ); + const discovered: Array<[string, TemplateResult]> = this._discovered.map( + (flow) => { + const title = localizeConfigFlowTitle(this.hass.localize, flow); + return [ + title, + html` + + `, + ]; + } + ); + const content = [...entries, ...discovered] + .sort((a, b) => compare(a[0], b[0])) + .map((item) => item[1]); + + return html` +

+ ${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")} +

+
+ ${content} + +
+ + `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + loadConfigFlowDialog(); + this._loadData(); + /* polyfill for paper-dropdown */ + import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"); + } + + private _createFlow() { + showConfigFlowDialog(this, { + dialogClosedCallback: () => this._loadData(), + }); + } + + private _continueFlow(ev) { + showConfigFlowDialog(this, { + continueFlowId: ev.currentTarget.flowId, + dialogClosedCallback: () => this._loadData(), + }); + } + + private async _loadData() { + const [discovered, entries] = await Promise.all([ + getConfigFlowsInProgress(this.hass!), + getConfigEntries(this.hass!), + ]); + this._discovered = discovered; + this._entries = entries; + } + + private async _finish() { + const result = await onboardIntegrationStep(this.hass, { + client_id: genClientId(), + }); + fireEvent(this, "onboarding-step", { + type: "integration", + result, + }); + } + + static get styles(): CSSResult { + return css` + .badges { + margin-top: 24px; + } + .badges > * { + width: 24%; + min-width: 90px; + margin-bottom: 24px; + } + button { + display: inline-block; + cursor: pointer; + padding: 0; + border: 0; + background: 0; + font: inherit; + } + .footer { + text-align: right; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "onboarding-integrations": OnboardingIntegrations; + } +} diff --git a/src/onboarding/onboarding-loading.ts b/src/onboarding/onboarding-loading.ts index ee8ced2dca..96964350fe 100644 --- a/src/onboarding/onboarding-loading.ts +++ b/src/onboarding/onboarding-loading.ts @@ -1,10 +1,64 @@ -import { LitElement, TemplateResult, html, customElement } from "lit-element"; +import { + LitElement, + TemplateResult, + html, + customElement, + CSSResult, + css, +} from "lit-element"; @customElement("onboarding-loading") class OnboardingLoading extends LitElement { protected render(): TemplateResult | void { return html` - Loading… +
+ `; + } + + static get styles(): CSSResult { + return css` + /* MIT License (MIT). Copyright (c) 2014 Luke Haas */ + .loader, + .loader:after { + border-radius: 50%; + width: 40px; + height: 40px; + } + .loader { + margin: 60px auto; + font-size: 4px; + position: relative; + text-indent: -9999em; + border-top: 1.1em solid rgba(3, 169, 244, 0.2); + border-right: 1.1em solid rgba(3, 169, 244, 0.2); + border-bottom: 1.1em solid rgba(3, 169, 244, 0.2); + border-left: 1.1em solid rgb(3, 168, 244); + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: load8 1.4s infinite linear; + animation: load8 1.4s infinite linear; + } + @-webkit-keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + @keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } `; } } diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.js b/src/panels/config/integrations/ha-config-entries-dashboard.js index ff5b4ea1dc..937fcc21f8 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.js +++ b/src/panels/config/integrations/ha-config-entries-dashboard.js @@ -23,6 +23,7 @@ import { loadConfigFlowDialog, showConfigFlowDialog, } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { localizeConfigFlowTitle } from "../../../data/config_entries"; /* * @appliesMixin LocalizeMixin @@ -207,21 +208,8 @@ class HaConfigManagerDashboard extends LocalizeMixin( return localize(`component.${integration}.config.title`); } - _computeActiveFlowTitle(localize, integration) { - const placeholders = integration.context.title_placeholders || {}; - const placeholderKeys = Object.keys(placeholders); - if (placeholderKeys.length === 0) { - return localize(`component.${integration.handler}.config.title`); - } - const args = []; - placeholderKeys.forEach((key) => { - args.push(key); - args.push(placeholders[key]); - }); - return localize( - `component.${integration.handler}.config.flow_title`, - ...args - ); + _computeActiveFlowTitle(localize, flow) { + return localizeConfigFlowTitle(localize, flow); } _computeConfigEntryEntities(hass, configEntry, entities) { diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index ee34177bde..dcf34e1aab 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -107,6 +107,7 @@ export const connectionMixin = ( return resp; }, ...getState(), + ...this._pendingHass, }; this.hassConnected(); diff --git a/src/state/hass-base-mixin.ts b/src/state/hass-base-mixin.ts index 8810d60403..f41d6752d2 100644 --- a/src/state/hass-base-mixin.ts +++ b/src/state/hass-base-mixin.ts @@ -10,6 +10,7 @@ import { HomeAssistant } from "../types"; export class HassBaseEl { protected hass?: HomeAssistant; + protected _pendingHass: Partial = {}; protected initializeHass(_auth: Auth, _conn: Connection) {} protected hassConnected() {} protected hassReconnected() {} @@ -23,6 +24,7 @@ export class HassBaseEl { export default (superClass: Constructor): Constructor => // @ts-ignore class extends superClass { + protected _pendingHass: Partial = {}; private __provideHass: HTMLElement[] = []; // @ts-ignore @property() protected hass: HomeAssistant; @@ -55,7 +57,11 @@ export default (superClass: Constructor): Constructor => el.hass = this.hass; } - protected async _updateHass(obj) { + protected async _updateHass(obj: Partial) { + if (!this.hass) { + this._pendingHass = { ...this._pendingHass, ...obj }; + return; + } this.hass = { ...this.hass, ...obj }; } }; diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index 8bf9f8f5ee..bbe3dde4c9 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -115,7 +115,7 @@ export default (superClass: Constructor) => }, }; const changes: Partial = { resources }; - if (language === this.hass!.language) { + if (this.hass && language === this.hass.language) { changes.localize = computeLocalize(this, language, resources); } this._updateHass(changes); diff --git a/src/translations/en.json b/src/translations/en.json index 43c310c22b..a232b91784 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1172,6 +1172,11 @@ "required_fields": "Fill in all required fields", "password_not_match": "Passwords don't match" } + }, + "integration": { + "intro": "Devices and services are represented in Home Assistant as integrations. You can set them up now, or do it later from the configuration screen.", + "more_integrations": "More", + "finish": "Finish" } } } diff --git a/yarn.lock b/yarn.lock index 3302efc068..593634e982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7237,10 +7237,10 @@ hoek@6.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== -home-assistant-js-websocket@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.4.0.tgz#3ba47cc8f8b7620619a675e7488d6108e8733a70" - integrity sha512-Uq5/KIAh4kF13MKzMyd0efBDoU+pNF0O1CfdGpSmT3La3tpt5h+ykpUYlq/vEBj6WwzU6iv3Czt4UK1o0IJHcA== +home-assistant-js-websocket@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.1.tgz#b85152c223a20bfe8827b817b927fd97cc021157" + integrity sha512-hNk8bj9JObd3NpgQ1+KtQCbSoz/TWockC8T/L8KvsPrDtkl1oQddajirumaMDgrJg/su4QsxFNUcDPGJyJ05UA== homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3"