From f1d60e4cf5f2be2993a5a0641713f320977cfb9c Mon Sep 17 00:00:00 2001 From: "Justin Willis (HE / HIM)" Date: Wed, 13 Jul 2022 09:49:09 -0700 Subject: [PATCH] feat(): Can package for Meta Quest devices --- src/library/package-utils.ts | 156 ++++++++++++++++++++++++ src/questions.ts | 2 +- src/services/package/meta-interfaces.ts | 13 ++ src/services/package/package-app.ts | 66 ++++++++-- 4 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 src/services/package/meta-interfaces.ts diff --git a/src/library/package-utils.ts b/src/library/package-utils.ts index 37a1d42..b132a6d 100644 --- a/src/library/package-utils.ts +++ b/src/library/package-utils.ts @@ -9,12 +9,15 @@ import { URL } from "url"; import { trackEvent } from "../services/usage-analytics"; import { getURL } from "../services/web-publish"; +import { MetaPackageOptions } from "../services/package/meta-interfaces"; export const WindowsDocsURL = "https://blog.pwabuilder.com/docs/windows-platform/"; export const iosDocsURL = "https://blog.pwabuilder.com/docs/ios-platform/"; +export const metaDocsURL = "https://docs.pwabuilder.com/#/builder/meta"; + /* * To-Do: More code re-use */ @@ -146,6 +149,28 @@ export async function buildAndroidPackage(options: AndroidPackageOptions) { return response; } +export async function packageForMeta(options: MetaPackageOptions) { + const generateAppUrl = `https://pwabuilder-oculus-linux-docker-app.azurewebsites.net/packages/create`; + + let response: Response | undefined; + + try { + response = await fetch(generateAppUrl, { + method: "POST", + body: JSON.stringify(options), + headers: new Headers({ "content-type": "application/json" }), + }); + } catch (err) { + vscode.window.showErrorMessage( + ` + There was an error packaging for Meta: ${err} + ` + ); + } + + return response; +} + export async function buildIOSPackage(options: IOSAppPackageOptions) { const generateAppUrl = "https://pwabuilder-ios.azurewebsites.net/packages/create"; @@ -251,6 +276,99 @@ export async function buildIOSOptions(): Promise { } } +export async function buildMetaOptions(): Promise { + const appUrl = await vscode.window.showInputBox({ + prompt: "Enter the URL to your app", + }); + + if (!appUrl) { + await vscode.window.showErrorMessage("Please enter a URL"); + return; + }; + + const manifestUrl = await vscode.window.showInputBox({ + prompt: "Enter the URL to your manifest", + }); + + const packageId = await vscode.window.showInputBox({ + prompt: "Enter the package ID", + }); + + const version = await vscode.window.showInputBox({ + prompt: "Enter your app's version number", + placeHolder: "1.0.0.0", + }); + + if (manifestUrl && packageId) { + // fetch manifest from manifestUrl using node-fetch + + let manifestData: Manifest | undefined; + let manifest: Manifest | undefined; + try { + manifestData = await (await fetch(manifestUrl)).json(); + + if (manifestData) { + manifest = manifestData; + } + } catch (err) { + // show error message + vscode.window.showErrorMessage( + `Error generating package: The Web Manifest could not be found at the URL entered. + This most likely means that the URL you entered for your Web Manifest is incorrect. + However, it can also mean that your Web Manifest is being served with the incorrect mimetype by + your web server or hosting service. More info: ${err} + ` + ); + } + + // find icon with a size of 512x512 from manifest.icons + const icon = manifest?.icons?.find((icon: any) => { + if (icon.sizes && icon.sizes.includes("512x512")) { + return icon; + } + }); + + const maskableIcon = manifest?.icons?.find((icon: any) => { + if (icon.purpose && icon.purpose.includes("maskable")) { + return icon; + } + }); + + if (!icon) { + await vscode.window.showErrorMessage( + "Your app cannot be packaged, please add an icon with a size of 512x512" + ); + + return; + } + + if (!maskableIcon) { + await vscode.window.showWarningMessage( + "We highly recommend adding a maskable icon to your app, however your app can still be packaged without one" + ); + } + + let packageResults: MetaPackageOptions | undefined = undefined; + + if (manifest && icon && version) { + packageResults = { + name: manifest.short_name || manifest.name || "My PWA", + packageId: packageId, + versionCode: parseInt(version), + versionName: version, + url: appUrl, + manifestUrl, + manifest, + existingSigningKey: null, + signingMode: 1 + }; + } + + return packageResults; + } + +} + export async function buildAndroidOptions(): Promise< AndroidPackageOptions | undefined > { @@ -547,6 +665,44 @@ export function emptyIOSPackageOptions(): IOSAppPackageOptions { }; } +export function validateMetaOptions(options: MetaPackageOptions): string[] { + const errors: string[] = []; + + if (!options.name) { + errors.push("Name required"); + } + + if (!options.versionCode) { + errors.push("Version code required"); + } + + if (!options.versionName) { + errors.push("Version name required"); + } + + if (!options.manifestUrl) { + errors.push("Manifest URL required"); + } + + if (!options.manifest) { + errors.push("Manifest required"); + } + + if (!options.packageId) { + errors.push("Package ID required"); + } + + if (!options.url) { + errors.push("URL required"); + } + + if (!options.signingMode) { + errors.push("Signing mode required"); + } + + return errors; +} + function tryGetHost(url: string): string | null { try { return new URL(url).host; diff --git a/src/questions.ts b/src/questions.ts index 0876c34..36747cf 100644 --- a/src/questions.ts +++ b/src/questions.ts @@ -4,7 +4,7 @@ export const packageQuestion: Question = { type: "list", name: "platform", message: "Which platform did you want to package your app for?", - choices: ["Windows Development", "Windows Production", "Android", "iOS"], + choices: ["Windows Development", "Windows Production", "Android", "iOS", "Meta Quest"], default: "Windows Development", }; diff --git a/src/services/package/meta-interfaces.ts b/src/services/package/meta-interfaces.ts new file mode 100644 index 0000000..ab44ad9 --- /dev/null +++ b/src/services/package/meta-interfaces.ts @@ -0,0 +1,13 @@ +import { Manifest } from "../../interfaces"; + +export interface MetaPackageOptions { + existingSigningKey: string | null; + manifest: Manifest; + manifestUrl: string; + name: string; + packageId: string; + signingMode: number; + url: string; + versionCode: number; + versionName: string; +} \ No newline at end of file diff --git a/src/services/package/package-app.ts b/src/services/package/package-app.ts index 3ed01a4..b6f2798 100644 --- a/src/services/package/package-app.ts +++ b/src/services/package/package-app.ts @@ -5,12 +5,16 @@ import { MsixInfo, Question } from "../../interfaces"; import { buildAndroidOptions, buildIOSOptions, + buildMetaOptions, getPublisherMsixFromArray, getSimpleMsixFromArray, iosDocsURL, + metaDocsURL, packageForIOS, + packageForMeta, packageForWindows, validateIOSOptions, + validateMetaOptions, WindowsDocsURL, } from "../../library/package-utils"; import { @@ -28,6 +32,7 @@ import { } from "./package-android-app"; import { AndroidPackageOptions } from "../../android-interfaces"; import { getAnalyticsClient } from "../usage-analytics"; +import { MetaPackageOptions } from "./meta-interfaces"; /* * To-do Justin: More re-use */ @@ -105,11 +110,11 @@ export async function packageApp(): Promise { const packageType = await getPackageInputFromUser(); const analyticsClient = getAnalyticsClient(); - analyticsClient.trackEvent({ - name: "package", - properties: { packageType: packageType, url: url, stage: "init" } + analyticsClient.trackEvent({ + name: "package", + properties: { packageType: packageType, url: url, stage: "init" } }); - + if (packageType === "iOS") { try { vscode.window.withProgress( @@ -192,7 +197,47 @@ export async function packageApp(): Promise { }` ); } - } else { + } + else if (packageType === "Meta Quest") { + try { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + }, + async (progress) => { + progress.report({ message: "Packaging your app..." }); + const options = await getMetaQuestPackageOptions(); + if (options) { + const optionsValidation = await validateMetaOptions(options); + + if (optionsValidation.length === 0) { + // no validation errors + const responseData = await packageForMeta(options); + progress.report({ message: "Converting to zip..." }); + await convertPackageToZip(responseData, options.packageId); + + // open android docs + await vscode.env.openExternal(vscode.Uri.parse(metaDocsURL)); + } else { + // validation errors + await vscode.window.showErrorMessage( + `There are some problems with your manifest that have prevented us from packaging: ${optionsValidation.join( + "\n" + )}` + ); + return; + } + } + } + ); + } catch (err: any) { + vscode.window.showErrorMessage( + `There was an error packaging your app: ${err && err.message ? err.message : err + }` + ); + } + } + else { await getMsixInputs(); if (!didInputFail) { vscode.window.withProgress( @@ -213,6 +258,11 @@ export async function packageApp(): Promise { } } +async function getMetaQuestPackageOptions(): Promise { + const options = await buildMetaOptions(); + return options; +} + async function getAndroidPackageOptions(): Promise< AndroidPackageOptions | undefined > { @@ -246,9 +296,9 @@ async function packageWithPwaBuilder(): Promise { const url = getURL(); const analyticsClient = getAnalyticsClient(); - analyticsClient.trackEvent({ - name: "package", - properties: { packageType: "Windows", url: url, stage: "complete" } + analyticsClient.trackEvent({ + name: "package", + properties: { packageType: "Windows", url: url, stage: "complete" } }); if (packageData) {