init commit

This commit is contained in:
rxliuli
2025-11-04 05:03:50 +08:00
commit bce557cc2d
1396 changed files with 172991 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./models"), exports);
__exportStar(require("./types/globals"), exports);
__exportStar(require("./types/javascriptcore"), exports);
__exportStar(require("./types/metrics"), exports);
__exportStar(require("./types/models"), exports);
__exportStar(require("./types/optional"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,250 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.unexpectedNull = exports.catchingContext = exports.context = exports.recordValidationIncidents = exports.endContext = exports.getContextNames = exports.beginContext = exports.messageForRecoveryAction = exports.isValidatable = exports.unexpectedType = exports.extendedTypeof = void 0;
const optional_1 = require("../types/optional");
/**
* Returns a string containing the type of a given value.
* This function augments the built in `typeof` operator
* to return sensible values for arrays and null values.
*
* @privateRemarks
* This function is exported for testing.
*
* @param value - The value to find the type of.
* @returns A string containing the type of `value`.
*/
function extendedTypeof(value) {
if (Array.isArray(value)) {
return "array";
}
else if (value === null) {
return "null";
}
else {
return typeof value;
}
}
exports.extendedTypeof = extendedTypeof;
/**
* Reports a non-fatal validation failure, logging a message to the console.
* @param recovery - The recovery action taken when the bad type was found.
* @param expected - The expected type of the value.
* @param actual - The actual value.
* @param pathString - A string containing the path to the value on the object which failed type validation.
*/
function unexpectedType(recovery, expected, actual, pathString) {
const actualType = extendedTypeof(actual);
const prettyPath = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : "<this>";
trackIncident({
type: "badType",
expected: expected,
// Our test assertions are matching the string interpolation of ${actual} value.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
actual: `${actualType} (${actual})`,
objectPath: prettyPath,
contextNames: getContextNames(),
recoveryAction: recovery,
stack: new Error().stack,
});
}
exports.unexpectedType = unexpectedType;
// endregion
/**
* Determines if a given object conforms to the Validatable interface
* @param possibleValidatable - An object that might be considered validatable
*
* @returns `true` if it is an instance of Validatable, `false` if not
*/
function isValidatable(possibleValidatable) {
if ((0, optional_1.isNothing)(possibleValidatable)) {
return false;
}
// MAINTAINER'S NOTE: We must check for either the existence of a pre-existing incidents
// property *or* the ability to add one. Failure to do so will cause
// problems for clients that either a) use interfaces to define their
// view models; or b) return collections from their service routes.
return (Object.prototype.hasOwnProperty.call(possibleValidatable, "$incidents") ||
Object.isExtensible(possibleValidatable));
}
exports.isValidatable = isValidatable;
/**
* Returns a developer-readable diagnostic message for a given recovery action.
* @param action - The recovery action to get the message for.
* @returns The message for `action`.
*/
function messageForRecoveryAction(action) {
switch (action) {
case "coercedValue":
return "Coerced format";
case "defaultValue":
return "Default value used";
case "ignoredValue":
return "Ignored value";
default:
return "Unknown";
}
}
exports.messageForRecoveryAction = messageForRecoveryAction;
// region Contexts
/**
* Shared validation context "stack".
*
* Because validation incidents propagate up the context stack,
* the representation used here is optimized for memory usage.
* A more literal representation of this would be a singly linked
* list describing a basic stack, but that will produce a large
* amount of unnecessary garbage and require copying `incidents`
* arrays backwards.
*/
const contextState = {
/// The names of each validation context on the stack.
nameStack: Array(),
/// All incidents reported so far. Cleared when the
/// context stack is emptied.
incidents: Array(),
// TODO: Removal of this is being tracked here:
// <rdar://problem/35015460> Intro Pricing: Un-suppress missing parent 'offers' error when server address missing key
/// The paths for incidents we wish to forgo tracking.
suppressedIncidentPaths: Array(),
};
/**
* Begin a new validation context with a given name,
* pushing it onto the validation context stack.
* @param name - The name for the validation context.
*/
function beginContext(name) {
contextState.nameStack.push(name);
}
exports.beginContext = beginContext;
/**
* Traverses the validation context stack and collects all of the context names.
* @returns The names of all validation contexts on the stack, from oldest to newest.
*/
function getContextNames() {
if (contextState.nameStack.length === 0) {
return ["<empty stack>"];
}
return contextState.nameStack.slice(0);
}
exports.getContextNames = getContextNames;
/**
* Ends the current validation context
*/
function endContext() {
if (contextState.nameStack.length === 0) {
console.warn("endContext() called without active validation context, ignoring");
}
contextState.nameStack.pop();
}
exports.endContext = endContext;
/**
* Records validation incidents back into an object that implements Validatable.
*
* Note: This method has a side-effect that the incident queue and name stack are cleared
* to prepare for the next thread's invocation.
*
* @param possibleValidatable - An object that may conform to Validatable, onto which we
* want to stash our validation incidents
*/
function recordValidationIncidents(possibleValidatable) {
if (isValidatable(possibleValidatable)) {
possibleValidatable.$incidents = contextState.incidents;
}
contextState.incidents = [];
contextState.nameStack = [];
contextState.suppressedIncidentPaths = [];
}
exports.recordValidationIncidents = recordValidationIncidents;
/**
* Create a transient validation context, and call a function that will return a value.
*
* Prefer this function over manually calling begin/endContext,
* it is exception safe.
*
* @param name - The name of the context
* @param producer - A function that produces a result
* @returns <Result> The resulting type
*/
function context(name, producer, suppressingPath) {
let suppressingName = null;
if ((0, optional_1.isSome)(suppressingPath) && suppressingPath.length > 0) {
suppressingName = name;
contextState.suppressedIncidentPaths.push(suppressingPath);
}
let result;
try {
beginContext(name);
result = producer();
}
catch (e) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!e.hasThrown) {
unexpectedType("defaultValue", "no exception", e.message);
e.hasThrown = true;
}
throw e;
}
finally {
if (name === suppressingName) {
contextState.suppressedIncidentPaths.pop();
}
endContext();
}
return result;
}
exports.context = context;
/**
* Create a transient validation context, that catches errors and returns null
*
* @param name - The name of the context
* @param producer - A function that produces a result
* @param caught - An optional handler to provide a value when an error is caught
* @returns <Result> The resulting type
*/
function catchingContext(name, producer, caught) {
let result = null;
try {
result = context(name, producer);
}
catch (e) {
result = null;
if ((0, optional_1.isSome)(caught)) {
result = caught(e);
}
}
return result;
}
exports.catchingContext = catchingContext;
/**
* Track an incident within the current validation context.
* @param incident - An incident object describing the problem.
*/
function trackIncident(incident) {
if (contextState.suppressedIncidentPaths.includes(incident.objectPath)) {
return;
}
contextState.incidents.push(incident);
}
// endregion
// region Nullability
/**
* Reports a non-fatal error indicating a value was unexpectedly null.
* @param recovery - The recovery action taken when the null value was found.
* @param expected - The expected type of the value.
* @param pathString - A string containing the path to the value on the object which was null.
*/
function unexpectedNull(recovery, expected, pathString) {
const prettyPath = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : "<this>";
trackIncident({
type: "nullValue",
expected: expected,
actual: "null",
objectPath: prettyPath,
contextNames: getContextNames(),
recoveryAction: recovery,
stack: new Error().stack,
});
}
exports.unexpectedNull = unexpectedNull;
// endregion
//# sourceMappingURL=validation.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=alert-action.js.map

View File

@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeCompoundAction = exports.isCompoundAction = void 0;
/**
* Custom type guard to determine if an action is a CompoundAction.
*/
function isCompoundAction(action) {
return (action === null || action === void 0 ? void 0 : action.$kind) === "compoundAction";
}
exports.isCompoundAction = isCompoundAction;
/**
* Helper that returns a CompoundAction, given an ActionMetrics and ActionModel[] of subactions.
*/
function makeCompoundAction(actionMetrics, subactions) {
return {
$kind: "compoundAction",
subactions,
actionMetrics,
};
}
exports.makeCompoundAction = makeCompoundAction;
//# sourceMappingURL=compound-action.js.map

View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeEmptyAction = exports.isEmptyAction = void 0;
/**
* Custom type guard to determine if an action is an EmptyAction.
*/
function isEmptyAction(action) {
return (action === null || action === void 0 ? void 0 : action.$kind) === "emptyAction";
}
exports.isEmptyAction = isEmptyAction;
/**
* Helper that returns an EmptyAction, given an ActionMetrics.
*/
function makeEmptyAction(actionMetrics) {
return {
$kind: "emptyAction",
actionMetrics,
};
}
exports.makeEmptyAction = makeEmptyAction;
//# sourceMappingURL=empty-action.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=external-url-action.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=flow-action.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=flow-back-action.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=http-action.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=http-template-action.js.map

View File

@@ -0,0 +1,26 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./alert-action"), exports);
__exportStar(require("./compound-action"), exports);
__exportStar(require("./empty-action"), exports);
__exportStar(require("./external-url-action"), exports);
__exportStar(require("./flow-action"), exports);
__exportStar(require("./flow-back-action"), exports);
__exportStar(require("./http-action"), exports);
__exportStar(require("./http-template-action"), exports);
__exportStar(require("./toast-action"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=toast-action.js.map

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeArtworkURLTemplate = void 0;
const validation = require("../json/validation");
const optional_1 = require("../types/optional");
const urls_1 = require("../util/urls");
/**
* Regex to parse artwork URL template string.
*/
const URL_TEMPLATE_PARSER = new RegExp("^({w}|[0-9]+(?:.[0-9]*)?)x({h}|[0-9]+(?:.[0-9]*)?)({c}|[a-z]{2}).({f}|[a-z]+)$");
/**
* Create an instance of artwork URL template from string.
* @param fromString - String to create artwork URL template from.
* @returns A new artwork URL template or `null` if string
* does not represent a valid artwork URL template.
*/
function makeArtworkURLTemplate(fromString) {
// A valid URL that ends with '{w}x{h}{c}.{f}'
// with any of placeholders possibly resolved to an actual value.
const url = new urls_1.URL(fromString);
if (url.pathname === undefined) {
validation.context("makeArtworkURLTemplate", () => {
validation.unexpectedType("ignoredValue", "A valid URL string", fromString);
});
return null;
}
// Expecting 5 matches: whole string + width, height, crop code and format.
const lastPathComponent = fromString.substring(fromString.lastIndexOf("/") + 1);
const matches = URL_TEMPLATE_PARSER.exec(lastPathComponent);
if ((0, optional_1.isNothing)(matches) || matches.length !== 5) {
validation.context("makeArtworkURLTemplate", () => {
validation.unexpectedType("ignoredValue", "A valid artwork URL template ending with {w}x{h}{c}.{f} format", lastPathComponent);
});
return null;
}
return fromString;
}
exports.makeArtworkURLTemplate = makeArtworkURLTemplate;
//# sourceMappingURL=artwork.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=button.js.map

View File

@@ -0,0 +1,196 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.areEqual = exports.luminanceFrom = exports.dynamicWith = exports.named = exports.rgbWith = exports.htmlWith = exports.Color = void 0;
const optional_1 = require("../types/optional");
// endregion
// region Constructors
// eslint-disable-next-line no-redeclare, @typescript-eslint/no-redeclare
exports.Color = {
/**
* Create new `HTMLColor` from hexadecimal string representation.
*
* @param hexString - Hexadecimal string representation.
*/
fromHex(string) {
if ((0, optional_1.isNothing)(string)) {
return null;
}
return {
$kind: "html",
value: string,
};
},
/**
* Create new `RBGColor` with RGB components and opacity value.
*
* @param red - Red color value.
* @param green - Green color value.
* @param blue - Blue color value.
* @param alpha - Opacity value.
*/
fromRGB(red, green, blue, alpha = 1.0) {
const newColor = {
$kind: "rgb",
red: red,
green: green,
blue: blue,
alpha: alpha,
};
return newColor;
},
/**
* Create new named color using the color name.
*
* @param name - The name of the color.
*/
named(name) {
const newColor = {
$kind: "named",
name: name,
};
return newColor;
},
/**
* Create new dynamic color with light and dark color variants.
*
* @param lightColor - The light color variant.
* @param lightHighContrastColor - The light hight-contrast color variant.
* @param darkColor - The dark color variant.
* @param darkHighContrastColor - The dark hight-contrast color variant.
*/
dynamicWith(lightColor, lightHighContrastColor, darkColor, darkHighContrastColor) {
const newColor = {
$kind: "dynamic",
lightColor: lightColor,
lightHighContrastColor: lightHighContrastColor,
darkColor: darkColor,
darkHighContrastColor: darkHighContrastColor,
};
return newColor;
},
// endregion
// region Properties
/**
* Get the luminance of the color.
*
* @param rgbColor - The RGB color to get luminance for.
*/
luminanceFrom(rgbColor) {
// Note: This is lifted from UIColor_Private
// Using RGB color components, calculates and returns (0.2126 * r) + (0.7152 * g) + (0.0722 * b).
return rgbColor.red * 0.2126 + rgbColor.green * 0.7152 + rgbColor.blue * 0.0722;
},
// endregion
// region Identity
/**
* Compare two colors for equality.
*
* @param color1 - Left hand side color to compare.
* @param color2 - Right hand side color to compare.
* @returns A Boolean indicating whether the colors are equal.
*/
areEqual(color1, color2) {
if ((0, optional_1.isNothing)(color1)) {
return (0, optional_1.isNothing)(color2);
}
else if ((0, optional_1.isNothing)(color2)) {
return (0, optional_1.isNothing)(color1);
}
const kind1 = color1.$kind;
const kind2 = color2.$kind;
if (kind1 === "named" && kind2 === "named") {
const namedColor1 = color1;
const namedColor2 = color2;
return namedColor1.name === namedColor2.name;
}
else if (kind1 === "rgb" && kind2 === "rgb") {
const rgbColor1 = color1;
const rgbColor2 = color2;
return (rgbColor1.red === rgbColor2.red &&
rgbColor1.green === rgbColor2.green &&
rgbColor1.blue === rgbColor2.blue &&
rgbColor1.alpha === rgbColor2.alpha);
}
else if (kind1 === "dynamic" && kind2 === "dynamic") {
const dynamicColor1 = color1;
const dynamicColor2 = color2;
return (exports.Color.areEqual(dynamicColor1.lightColor, dynamicColor2.lightColor) &&
exports.Color.areEqual(dynamicColor1.lightHighContrastColor, dynamicColor2.lightHighContrastColor) &&
exports.Color.areEqual(dynamicColor1.darkColor, dynamicColor2.darkColor) &&
exports.Color.areEqual(dynamicColor1.darkHighContrastColor, dynamicColor2.darkHighContrastColor));
}
else {
return false;
}
},
};
/**
* Create new `HTMLColor` from hexadecimal string representation.
*
* @param hexString - Hexadecimal string representation.
*
* @deprecated This symbol has been moved to `Color.fromHex` and will be removed
* in the future.
*/
const htmlWith = exports.Color.fromHex;
exports.htmlWith = htmlWith;
/**
* Create new `RBGColor` with RGB components and opacity value.
*
* @param red - Red color value.
* @param green - Green color value.
* @param blue - Blue color value.
* @param alpha - Opacity value.
*
* @deprecated This symbol has been moved to `Color.fromRGB` and will be removed
* in the future.
*/
const rgbWith = exports.Color.fromRGB;
exports.rgbWith = rgbWith;
/**
* Create new named color using the color name.
*
* @param name - The name of the color.
*
* @deprecated This symbol has been moved to `Color.named` and will be removed
* in the future.
*/
const named = exports.Color.named;
exports.named = named;
/**
* Create new dynamic color with light and dark color variants.
*
* @param lightColor - The light color variant.
* @param lightHighContrastColor - The light hight-contrast color variant.
* @param darkColor - The dark color variant.
* @param darkHighContrastColor - The dark hight-contrast color variant.
*
* @deprecated This symbol has been moved to `Color.dynamicWith` and will be removed
* in the future.
*/
const dynamicWith = exports.Color.dynamicWith;
exports.dynamicWith = dynamicWith;
/**
* Get the luminance of the color.
*
* @param rgbColor - The RGB color to get luminance for.
*
* @deprecated This symbol has been moved to `Color.luminanceFrom` and will be removed
* in the future.
*/
const luminanceFrom = exports.Color.luminanceFrom;
exports.luminanceFrom = luminanceFrom;
/**
* Compare two colors for equality.
*
* @param color1 - Left hand side color to compare.
* @param color2 - Right hand side color to compare.
* @returns A Boolean indicating whether the colors are equal.
*
* @deprecated This symbol has been moved to `Color.areEqual` and will be removed
* in the future.
*/
const areEqual = exports.Color.areEqual;
exports.areEqual = areEqual;
// endregion
//# sourceMappingURL=color.js.map

View File

@@ -0,0 +1,25 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./actions"), exports);
__exportStar(require("./artwork"), exports);
__exportStar(require("./button"), exports);
__exportStar(require("./color"), exports);
__exportStar(require("./menu"), exports);
__exportStar(require("./paragraph"), exports);
__exportStar(require("./programmed-text"), exports);
__exportStar(require("./video"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.menuSeparatorID = void 0;
/**
* A standard identifier for including a separator in a menu.
*/
exports.menuSeparatorID = "com.apple.JetEngine.separator";
//# sourceMappingURL=menu.js.map

View File

@@ -0,0 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// endregion
//# sourceMappingURL=paragraph.js.map

View File

@@ -0,0 +1,5 @@
"use strict";
// region ProgrammedText
Object.defineProperty(exports, "__esModule", { value: true });
// endregion
//# sourceMappingURL=programmed-text.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=video.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=bag.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=bundle.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=cookie-provider.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=cryptography.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=host.js.map

View File

@@ -0,0 +1,55 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
/* `preprocessor` and `testContent` are normally replaced by inline literals while bundling an app's JS.
*
* If these values have not been set we want to provide defaults however
* attempting to access them can trigger a ReferenceError as the
* variables are undefined (distinct from a defined variable being set to
* `undefined`).
*
* `typeof` checks can safely test undefined variables, note that these checks will become:
* `typeof { DEBUG_BUILD: true, ... }` when webpack's DefinePlugin is used in @jet/build's webpack task.
* When these variables have not been replaced we need to use `globalThis` to set them on the global scope
* in order to avoid ReferenceErrors attempting to access them.
*/
if (typeof preprocessor === "undefined") {
globalThis.preprocessor = {
PRODUCTION_BUILD: false,
CARRY_BUILD: false,
DEBUG_BUILD: false,
INTERNAL_BUILD: false,
};
}
if (typeof testContent === "undefined") {
globalThis.testContent = {
INCLUDE_TEST_CONTENT: false,
};
}
__exportStar(require("./bag"), exports);
__exportStar(require("./bundle"), exports);
__exportStar(require("./cookie-provider"), exports);
__exportStar(require("./cryptography"), exports);
__exportStar(require("./host"), exports);
__exportStar(require("./jscookie"), exports);
__exportStar(require("./net"), exports);
__exportStar(require("./platform"), exports);
__exportStar(require("./plist"), exports);
__exportStar(require("./preprocessor"), exports);
__exportStar(require("./random"), exports);
__exportStar(require("./service"), exports);
__exportStar(require("./types"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=jscookie.js.map

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProxiedNetwork = void 0;
/**
* `Network` object designed to wrap the bridged native network object,
* which automatically includes page intent instrumentation data in the fetch request.
*/
class ProxiedNetwork {
constructor(original, pageIntentInstrumentation) {
this.original = original;
this.pageIntentInstrumentation = pageIntentInstrumentation;
}
async fetch(request) {
request["$pageIntentInstrumentation"] = this.pageIntentInstrumentation;
return await this.original.fetch(request);
}
}
exports.ProxiedNetwork = ProxiedNetwork;
//# sourceMappingURL=net.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=platform.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=plist.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=preprocessor.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=random.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=service.js.map

View File

@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.services = exports.random = exports.plist = exports.platform = exports.net = exports.localizer = exports.host = exports.cryptography = exports.cookieProvider = exports.bundle = exports.bag = void 0;
const metatype_1 = require("../../util/metatype");
exports.bag = (0, metatype_1.makeMetatype)("jet-engine:bag");
exports.bundle = (0, metatype_1.makeMetatype)("jet-engine:bundle");
exports.cookieProvider = (0, metatype_1.makeMetatype)("jet-engine:cookieProvider");
exports.cryptography = (0, metatype_1.makeMetatype)("jet-engine:cryptography");
exports.host = (0, metatype_1.makeMetatype)("jet-engine:host");
exports.localizer = (0, metatype_1.makeMetatype)("jet-engine:localizer");
exports.net = (0, metatype_1.makeMetatype)("jet-engine:net");
exports.platform = (0, metatype_1.makeMetatype)("jet-engine:platform");
exports.plist = (0, metatype_1.makeMetatype)("jet-engine:plist");
exports.random = (0, metatype_1.makeMetatype)("jet-engine:random");
exports.services = (0, metatype_1.makeMetatype)("jet-engine:services");
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1,14 @@
"use strict";
/*
* Describes standard functionality available in JSContexts
*
* Types are defined here to allow us to match the behavior available in JSContext in the target OS
* which may not exactly match the definitions in standard TypeScript lib files, particularly on a
* pre-release OS.
*
* The living standard for the Console API is available at https://console.spec.whatwg.org
* The WebKit team has documented their interfaces at https://webkit.org/web-inspector/console-object-api/
* The equivalent interface in Node is https://nodejs.org/api/console.html
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=console.js.map

View File

@@ -0,0 +1,18 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./console"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.notInstrumented = exports.PageInvocationPoint = exports.EMPTY_LINTED_METRICS_EVENT = void 0;
/**
* An empty linted metrics event.
*
* The empty events should be skipped from recording
* by metrics event recorders.
*/
exports.EMPTY_LINTED_METRICS_EVENT = {
fields: {},
issues: [],
};
var PageInvocationPoint;
(function (PageInvocationPoint) {
PageInvocationPoint["pageEnter"] = "pageEnter";
PageInvocationPoint["pageExit"] = "pageExit";
PageInvocationPoint["appExit"] = "appExit";
PageInvocationPoint["appEnter"] = "appEnter";
PageInvocationPoint["backButton"] = "backButton";
})(PageInvocationPoint = exports.PageInvocationPoint || (exports.PageInvocationPoint = {}));
/**
* Returns an empty metrics instance of the specified metrics type.
* @param metricsType - Type of the metrics data to return.
*
* @deprecated Do not use, all metrics events should be instrumented.
*/
function notInstrumented(metricsType) {
switch (metricsType) {
case 0 /* NotInstrumentedMetricsType.ActionMetrics */:
return {
data: [],
custom: {},
};
case 1 /* NotInstrumentedMetricsType.FetchTimingMetrics */:
return {};
case 2 /* NotInstrumentedMetricsType.PageMetrics */:
return {
instructions: [],
custom: {},
};
case 3 /* NotInstrumentedMetricsType.ImpressionMetrics */:
return {
id: {
id: "",
impressionIndex: NaN,
},
fields: {},
custom: {},
};
default:
return {};
}
}
exports.notInstrumented = notInstrumented;
// endregion
//# sourceMappingURL=metrics.js.map

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=models.js.map

View File

@@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flatMapOptional = exports.mapOptional = exports.unsafeUnwrapOptional = exports.unwrapOptional = exports.isSome = exports.isNothing = exports.unsafeUninitialized = void 0;
/**
* Bypass the protection provided by the `Optional` type
* and pretend to produce a value of `Some<T>` while
* actually returning `Nothing`.
*/
function unsafeUninitialized() {
return undefined;
}
exports.unsafeUninitialized = unsafeUninitialized;
/**
* Test whether an optional does not contain a value.
*
* @param value - An optional value to test.
*/
function isNothing(value) {
return value === undefined || value === null;
}
exports.isNothing = isNothing;
/**
* Test whether an optional contains a value.
* @param value - An optional value to test.
*/
function isSome(value) {
return value !== undefined && value !== null;
}
exports.isSome = isSome;
/**
* Unwrap the value contained in a given optional,
* throwing an error if there is no value.
*
* @param value - A value to unwrap.
*/
function unwrapOptional(value) {
if (isNothing(value)) {
throw new ReferenceError();
}
return value;
}
exports.unwrapOptional = unwrapOptional;
/**
* Unwrap the value contained in a given optional
* without checking if the value exists.
*
* @param value - A value to unwrap.
*/
function unsafeUnwrapOptional(value) {
return value;
}
exports.unsafeUnwrapOptional = unsafeUnwrapOptional;
function mapOptional(value, body) {
if (isSome(value)) {
return body(value);
}
else {
return value;
}
}
exports.mapOptional = mapOptional;
function flatMapOptional(value, body) {
if (isSome(value)) {
return body(value);
}
else {
return value;
}
}
exports.flatMapOptional = flatMapOptional;
//# sourceMappingURL=optional.js.map

View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeMetatype = void 0;
function makeMetatype(name) {
return {
name: name,
};
}
exports.makeMetatype = makeMetatype;
//# sourceMappingURL=metatype.js.map

View File

@@ -0,0 +1,373 @@
"use strict";
// MARK: - Parsing Regular Expressions
Object.defineProperty(exports, "__esModule", { value: true });
exports.URL = void 0;
const optional_1 = require("../types/optional");
const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i;
const queryParamRegex = /([^=?&]+)=?([^&]*)/g;
const componentOrder = ["hash", "query", "pathname", "host"];
class URL {
constructor(url) {
var _a;
this.query = {};
if ((0, optional_1.isNothing)(url)) {
return;
}
// Split the protocol from the rest of the urls
let remainder = url;
const match = protocolRegex.exec(url);
if ((0, optional_1.isSome)(match)) {
// Pull out the protocol
let protocol = match[1];
if (protocol !== null && protocol !== undefined) {
protocol = protocol.split(":")[0];
}
this.protocol = protocol !== null && protocol !== void 0 ? protocol : undefined;
// Save the remainder
remainder = (_a = match[3]) !== null && _a !== void 0 ? _a : undefined;
}
// Then match each component in a specific order
let parse = { remainder: remainder, result: undefined };
for (const component of componentOrder) {
if (parse === undefined || parse.remainder === undefined) {
break;
}
switch (component) {
case "hash": {
parse = splitUrlComponent(parse.remainder, "#", "suffix");
this.hash = parse === null || parse === void 0 ? void 0 : parse.result;
break;
}
case "query": {
parse = splitUrlComponent(parse.remainder, "?", "suffix");
if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) {
this.query = URL.queryFromString(parse.result);
}
break;
}
case "pathname": {
parse = splitUrlComponent(parse.remainder, "/", "suffix");
if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) {
// Replace the initial /, since paths require it
this.pathname = "/" + parse.result;
}
break;
}
case "host": {
const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix");
const userInfo = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.result;
const hostPort = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.remainder;
if (userInfo !== undefined) {
const userInfoSplit = userInfo.split(":");
this.username = decodeURIComponent(userInfoSplit[0]);
this.password = decodeURIComponent(userInfoSplit[1]);
}
if (hostPort !== undefined) {
const hostPortSplit = hostPort.split(":");
this.host = hostPortSplit[0];
this.port = hostPortSplit[1];
}
break;
}
default: {
throw new Error("Unhandled case!");
}
}
}
}
get(component) {
switch (component) {
// Exhaustive match to make sure TS property minifiers and other
// transformer plugins do not break this code.
case "protocol":
return this.protocol;
case "username":
return this.username;
case "password":
return this.password;
case "port":
return this.port;
case "pathname":
return this.pathname;
case "query":
return this.query;
case "hash":
return this.hash;
default:
// The fallback for component which is not a property of URL object.
return this[component];
}
}
set(component, value) {
if (value === undefined) {
return this;
}
if (component === "query") {
if (typeof value === "string") {
value = URL.queryFromString(value);
}
}
switch (component) {
// Exhaustive match to make sure TS property minifiers and other
// transformer plugins do not break this code.
case "protocol":
this.protocol = value;
break;
case "username":
this.username = value;
break;
case "password":
this.password = value;
break;
case "port":
this.port = value;
break;
case "pathname":
this.pathname = value;
break;
case "query":
this.query = value;
break;
case "hash":
this.hash = value;
break;
default:
// The fallback for component which is not a property of URL object.
this[component] = value;
break;
}
return this;
}
append(component, value) {
let existingValue = this.get(component);
let newValue;
if (component === "query") {
if (existingValue === undefined) {
existingValue = {};
}
if (typeof value === "string") {
value = URL.queryFromString(value);
}
if (typeof existingValue === "string") {
newValue = { existingValue, ...value };
}
else {
newValue = { ...existingValue, ...value };
}
}
else {
if (existingValue === undefined) {
existingValue = "";
}
let existingValueString = existingValue;
if (existingValueString === undefined) {
existingValueString = "";
}
let newValueString = existingValueString;
if (component === "pathname") {
const pathLength = existingValueString.length;
if (pathLength === 0 || existingValue[pathLength - 1] !== "/") {
newValueString += "/";
}
}
// The component is not "query" so we treat value as string.
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-plus-operands
newValueString += value;
newValue = newValueString;
}
return this.set(component, newValue);
}
param(key, value) {
if (key === null) {
return this;
}
if (this.query === undefined) {
this.query = {};
}
if (value === undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.query[key];
}
else {
this.query[key] = value;
}
return this;
}
removeParam(key) {
if (key === undefined || this.query === undefined) {
return this;
}
if (key in this.query) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.query[key];
}
return this;
}
path(value) {
return this.append("pathname", value);
}
pathExtension() {
var _a, _b;
// Extract path extension if one exists
if (this.pathname === undefined) {
return undefined;
}
const lastFilenameComponents = (_b = (_a = this.pathname
.split("/")
.filter((item) => item.length > 0) // Remove any double or trailing slashes
.pop()) === null || _a === void 0 ? void 0 : _a.split(".")) !== null && _b !== void 0 ? _b : [];
if (lastFilenameComponents.filter(function (part) {
return part !== "";
}).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"])
) {
return undefined;
}
return lastFilenameComponents.pop();
}
/**
* Returns the path components of the URL
* @returns An array of non-empty path components from `urls`.
*/
pathComponents() {
if (this.pathname === undefined) {
return [];
}
return this.pathname.split("/").filter((component) => component.length > 0);
}
/**
* Same as toString
*
* @returns A string representation of the URL
*/
build() {
return this.toString();
}
/**
* Converts the URL to a string
*
* @returns A string representation of the URL
*/
toString() {
let url = "";
if (this.protocol !== undefined) {
url += this.protocol + "://";
}
if (this.username !== undefined) {
url += encodeURIComponent(this.username);
if (this.password !== undefined) {
url += ":" + encodeURIComponent(this.password);
}
url += "@";
}
if (this.host !== undefined) {
url += this.host;
if (this.port !== undefined) {
url += ":" + this.port;
}
}
if (this.pathname !== undefined) {
url += this.pathname;
}
if (this.query !== undefined && Object.keys(this.query).length !== 0) {
url += "?" + URL.toQueryString(this.query);
}
if (this.hash !== undefined) {
url += "#" + this.hash;
}
return url;
}
// ----------------
// Static API
// ----------------
/**
* Converts a string into a query dictionary
* @param query - The string to parse
* @returns The query dictionary containing the key-value pairs in the query string
*/
static queryFromString(query) {
const result = {};
let parseResult = queryParamRegex.exec(query);
while (parseResult !== null && parseResult.length >= 3) {
// We support the legacy query format for "application/x-www-form-urlencoded" which can represent spaces as "+" symbols.
// https://url.spec.whatwg.org/#concept-urlencoded-parser
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
const key = decodeURIComponent(parseResult[1].replace(/\+/g, " "));
const value = decodeURIComponent(parseResult[2].replace(/\+/g, " "));
result[key] = value;
parseResult = queryParamRegex.exec(query);
}
return result;
}
/**
* Converts a query dictionary into a query string
*
* @param query - The query dictionary
* @returns The string representation of the query dictionary
*/
static toQueryString(query) {
let queryString = "";
let first = true;
for (const key of Object.keys(query)) {
if (!first) {
queryString += "&";
}
first = false;
queryString += encodeURIComponent(key);
const value = query[key];
if (value !== null && value.length > 0) {
queryString += "=" + encodeURIComponent(value);
}
}
return queryString;
}
/**
* Convenience method to instantiate a URL from a string
* @param url - The URL string to parse
* @returns The new URL object representing the URL
*/
static from(url) {
return new URL(url);
}
/**
* Convenience method to instantiate a URL from numerous (optional) components
* @param protocol - The protocol type
* @param host - The host name
* @param path - The path
* @param query - The query
* @param hash - The hash
* @returns The new URL object representing the URL
*/
static fromComponents(protocol, host, path, query, hash) {
const url = new URL();
url.protocol = protocol;
url.host = host;
url.pathname = path;
url.query = query !== null && query !== void 0 ? query : {};
url.hash = hash;
return url;
}
}
exports.URL = URL;
// MARK: - Helpers
function splitUrlComponent(input, marker, style) {
const index = input.indexOf(marker);
let result;
let remainder = input;
if (index !== -1) {
const prefix = input.slice(0, index);
const suffix = input.slice(index + marker.length, input.length);
if (style === "prefix") {
result = prefix;
remainder = suffix;
}
else {
result = suffix;
remainder = prefix;
}
}
return {
result: result,
remainder: remainder,
};
}
//# sourceMappingURL=urls.js.map

View File

@@ -0,0 +1,289 @@
import { Opt, isNothing } from "@jet/environment/types/optional";
import * as serverData from "./server-data";
import * as media from "./data-structure";
import { JSONValue, MapLike, JSONData } from "./json-types";
import * as errors from "./errors";
// region Generic Attribute retrieval
// region Attribute retrieval
/**
* Retrieve the specified attribute from the data, coercing it to a JSONData dictionary
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @param defaultValue The object to return if the path search fails.
* @returns The dictionary of data
*/
export function attributeAsDictionary<Type extends JSONValue>(
data: media.Data,
attributePath?: serverData.ObjectPath,
defaultValue?: MapLike<Type>,
): MapLike<Type> | null {
if (serverData.isNull(data)) {
return null;
}
return serverData.asDictionary(data.attributes, attributePath, defaultValue);
}
/**
* Retrieve the specified attribute from the data, coercing it to an Interface
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @param defaultValue The object to return if the path search fails.
* @returns The dictionary of data as an interface
*/
export function attributeAsInterface<Interface>(
data: media.Data,
attributePath?: serverData.ObjectPath,
defaultValue?: JSONData,
): Interface | null {
return attributeAsDictionary(data, attributePath, defaultValue) as unknown as Interface;
}
/**
* Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @returns {any[]} The attribute value as an array.
*/
export function attributeAsArrayOrEmpty<T extends JSONValue>(
data: media.Data,
attributePath?: serverData.ObjectPath,
): T[] {
if (serverData.isNull(data)) {
return [];
}
return serverData.asArrayOrEmpty(data.attributes, attributePath);
}
/**
* Retrieve the specified attribute from the data as a string.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The object path for the attribute.
* @param policy The validation policy to use when resolving this value.
* @returns {string} The attribute value as a string.
*/
export function attributeAsString(
data: media.Data,
attributePath?: serverData.ObjectPath,
policy: serverData.ValidationPolicy = "coercible",
): Opt<string> {
if (serverData.isNull(data)) {
return null;
}
return serverData.asString(data.attributes, attributePath, policy);
}
/**
* Retrieve the specified meta from the data as a string.
*
* @param data The data from which to retrieve the attribute.
* @param metaPath The object path for the meta.
* @param policy The validation policy to use when resolving this value.
* @returns {string} The meta value as a string.
*/
export function metaAsString(
data: media.Data,
metaPath?: serverData.ObjectPath,
policy: serverData.ValidationPolicy = "coercible",
): Opt<string> {
if (serverData.isNull(data)) {
return null;
}
return serverData.asString(data.meta, metaPath, policy);
}
/**
* Retrieve the specified attribute from the data as a date.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The object path for the attribute.
* @param policy The validation policy to use when resolving this value.
* @returns {Date} The attribute value as a date.
*/
export function attributeAsDate(
data: media.Data,
attributePath?: serverData.ObjectPath,
policy: serverData.ValidationPolicy = "coercible",
): Opt<Date> {
if (serverData.isNull(data)) {
return null;
}
const dateString = serverData.asString(data.attributes, attributePath, policy);
if (isNothing(dateString)) {
return null;
}
return new Date(dateString);
}
/**
* Retrieve the specified attribute from the data as a boolean.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @param policy The validation policy to use when resolving this value.
* @returns {boolean} The attribute value as a boolean.
*/
export function attributeAsBoolean(
data: media.Data,
attributePath?: serverData.ObjectPath,
policy: serverData.ValidationPolicy = "coercible",
): boolean | null {
if (serverData.isNull(data)) {
return null;
}
return serverData.asBoolean(data.attributes, attributePath, policy);
}
/**
* Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present..
*/
export function attributeAsBooleanOrFalse(data: media.Data, attributePath?: serverData.ObjectPath): boolean {
if (serverData.isNull(data)) {
return false;
}
return serverData.asBooleanOrFalse(data.attributes, attributePath);
}
/**
* Retrieve the specified attribute from the data as a number.
*
* @param data The data from which to retrieve the attribute.
* @param attributePath The path of the attribute.
* @param policy The validation policy to use when resolving this value.
* @returns {boolean} The attribute value as a number.
*/
export function attributeAsNumber(
data: media.Data,
attributePath?: serverData.ObjectPath,
policy: serverData.ValidationPolicy = "coercible",
): Opt<number> {
if (serverData.isNull(data)) {
return null;
}
return serverData.asNumber(data.attributes, attributePath, policy);
}
export function hasAttributes(data: media.Data): boolean {
return !serverData.isNull(serverData.asDictionary(data, "attributes"));
}
/**
* The canonical way to detect if an item from Media API is hydrated or not.
*
* @param data The data from which to retrieve the attributes.
*/
export function isNotHydrated(data: media.Data): boolean {
return !hasAttributes(data);
}
// region Custom Attributes
/**
* Performs conversion for a custom variant of given attribute, if any are available.
* @param attribute Attribute to get custom attribute key for, if any.
*/
export function attributeKeyAsCustomAttributeKey(attribute: string): string | undefined {
return customAttributeMapping[attribute];
}
/**
* Whether or not given custom attributes key allows fallback to default page with AB testing treatment within a nondefault page.
* This is to allow AB testing to affect only icons within custom product pages.
*/
export function attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttribute: string): boolean {
return customAttribute === "customArtwork" || customAttribute === "customIconArtwork"; // Only the icon artwork.
}
/**
* Defines mapping of attribute to custom attribute.
*/
const customAttributeMapping: { [key: string]: string } = {
artwork: "customArtwork",
iconArtwork: "customIconArtwork",
screenshotsByType: "customScreenshotsByType",
promotionalText: "customPromotionalText",
videoPreviewsByType: "customVideoPreviewsByType",
customScreenshotsByTypeForAd: "customScreenshotsByTypeForAd",
customVideoPreviewsByTypeForAd: "customVideoPreviewsByTypeForAd",
};
export function requiredAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string {
const value = attributeAsString(data, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath));
}
return value;
}
export function requiredAttributeAsDate(data: media.Data, attributePath: serverData.ObjectPath): Date {
const value = attributeAsDate(data, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath));
}
return value;
}
export function requiredAttributeAsDictionary<Type extends JSONValue>(
data: media.Data,
attributePath: serverData.ObjectPath,
): MapLike<Type> {
const value: MapLike<Type> | null = attributeAsDictionary(data, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath));
}
return value;
}
export function requiredMeta(data: media.Data): MapLike<JSONValue> {
const value = serverData.asDictionary(data, "meta");
if (isNothing(value)) {
throw new errors.MissingFieldError(data, "meta");
}
return value;
}
export function requiredMetaAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string {
const meta = requiredMeta(data);
const value = serverData.asString(meta, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath));
}
return value;
}
export function requiredMetaAttributeAsNumber(data: media.Data, attributePath: serverData.ObjectPath): number {
const meta = requiredMeta(data);
const value = serverData.asNumber(meta, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath));
}
return value;
}
export function concatObjectPaths(prefix: serverData.ObjectPath, suffix: serverData.ObjectPath): serverData.ObjectPath {
let finalPath: string[];
if (Array.isArray(prefix)) {
finalPath = prefix;
} else {
finalPath = [prefix];
}
if (Array.isArray(suffix)) {
finalPath.push(...suffix);
} else {
finalPath.push(suffix);
}
return finalPath;
}
// endregion

View File

@@ -0,0 +1,476 @@
//
// server-data.ts
// AppStoreKit
//
// Created by Kevin MacWhinnie on 8/17/16.
// Copyright (c) 2016 Apple Inc. All rights reserved.
//
// TODO: Replace this utility for JSON Parsing
import * as validation from "@jet/environment/json/validation";
import { Nothing, Opt, isNothing } from "@jet/environment/types/optional";
import { JSONArray, JSONData, JSONValue, MapLike } from "./json-types";
// region Traversal
/**
* Union type that describes the possible representations for an object traversal path.
*/
export type ObjectPath = string | string[];
/**
* Returns the string representation of a given object path.
* @param path The object path to coerce to a string.
* @returns A string representation of `path`.
*/
export function objectPathToString(path: Opt<ObjectPath>): Opt<string> {
if (isNull(path)) {
return null;
} else if (Array.isArray(path)) {
return path.join(".");
} else {
return path;
}
}
const PARSED_PATH_CACHE: { [key: string]: string[] } = {};
/**
* Traverse a nested JSON object structure, short-circuiting
* when finding `undefined` or `null` values. Usage:
*
* const object = {x: {y: {z: 42}}};
* const meaningOfLife = serverData.traverse(object, 'x.y.z');
*
* @param object The JSON object to traverse.
* @param path The path to search. If falsy, `object` will be returned without being traversed.
* @param defaultValue The object to return if the path search fails.
* @return The value at `path` if found; default value otherwise.
*/
export function traverse(object: JSONValue, path?: ObjectPath, defaultValue?: JSONValue): JSONValue {
if (object === undefined || object === null) {
return defaultValue;
}
if (isNullOrEmpty(path)) {
return object;
}
let components: string[];
if (typeof path === "string") {
components = PARSED_PATH_CACHE[path];
if (isNullOrEmpty(components)) {
// Fast Path: If the path contains only a single component, we can skip
// all of the work below here and speed up storefronts that
// don't have JIT compilation enabled.
if (!path.includes(".")) {
const value = object[path];
if (value !== undefined && value !== null) {
return value;
} else {
return defaultValue;
}
}
components = path.split(".");
PARSED_PATH_CACHE[path] = components;
}
} else {
components = path;
}
let current: JSONValue = object;
for (const component of components) {
current = current[component];
if (current === undefined || current === null) {
return defaultValue;
}
}
return current;
}
// endregion
// region Nullability
/**
* Returns a bool indicating whether or not a given object null or undefined.
* @param object The object to test.
* @return true if the object is null or undefined; false otherwise.
*/
export function isNull<Type>(object: Type | Nothing): object is Nothing {
return object === null || object === undefined;
}
/**
* Returns a bool indicating whether or not a given object is null or empty.
* @param object The object to test
* @return true if object is null or empty; false otherwise.
*/
export function isNullOrEmpty<Type>(object: Type | Nothing): object is Nothing {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return isNull(object) || Object.keys(object as any).length === 0;
}
/**
* Returns a bool indicating whether or not a given object is non-null.
* @param object The object to test.
* @return true if the object is not null or undefined; false otherwise.
*/
export function isDefinedNonNull<Type>(object: Type | null | undefined): object is Type {
return typeof object !== "undefined" && object !== null;
}
/**
* Returns a bool indicating whether or not a given object is non-null or empty.
* @param object The object to test.
* @return true if the object is not null or undefined and not empty; false otherwise.
*/
export function isDefinedNonNullNonEmpty<Type>(object: Type | Nothing): object is Type {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return isDefinedNonNull(object) && Object.keys(object as any).length !== 0;
}
/**
* Checks if the passed string or number is a number
*
* @param value The value to check
* @return True if the value is an number, false if not
*/
export function isNumber(value: number | string | null | undefined): value is number {
if (isNull(value)) {
return false;
}
let valueToCheck;
if (typeof value === "string") {
valueToCheck = parseInt(value);
} else {
valueToCheck = value;
}
return !Number.isNaN(valueToCheck);
}
/**
* Returns a bool indicating whether or not a given object is defined but empty.
* @param object The object to test.
* @return true if the object is not null and empty; false otherwise.
*/
export function isArrayDefinedNonNullAndEmpty<Type extends JSONArray>(object: Type | null | undefined): object is Type {
return isDefinedNonNull(object) && object.length === 0;
}
// endregion
// region Defaulting Casts
/**
* Check that a given object is an array, substituting an empty array if not.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find an array.
* Omit this parameter if `object` is itself an array.
* @returns An untyped array.
*/
export function asArrayOrEmpty<T extends JSONValue>(object: JSONValue, path?: ObjectPath): T[] {
const target = traverse(object, path, null);
if (Array.isArray(target)) {
// Note: This is kind of a nasty cast, but I don't think we want to validate that everything is of type T
return target as T[];
} else {
if (!isNull(target)) {
validation.context("asArrayOrEmpty", () => {
validation.unexpectedType("defaultValue", "array", target, objectPathToString(path));
});
}
return [];
}
}
/**
* Check that a given object is a boolean, substituting the value `false` if not.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a boolean.
* Omit this parameter if `object` is itself a boolean.
* @returns A boolean from `object`, or defaults to `false`.
*/
export function asBooleanOrFalse(object: JSONValue, path?: ObjectPath): boolean {
const target = traverse(object, path, null);
if (typeof target === "boolean") {
return target;
} else {
if (!isNull(target)) {
validation.context("asBooleanOrFalse", () => {
validation.unexpectedType("defaultValue", "boolean", target, objectPathToString(path));
});
}
return false;
}
}
// endregion
// region Coercing Casts
export type ValidationPolicy = "strict" | "coercible" | "none";
/**
* Safely coerce an object into a string.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a string.
* Omit this parameter if `object` is itself a string.
* @param policy The validation policy to use when resolving this value
* @returns A string from `object`, or `null` if `object` is null.
*/
export function asString(object: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<string> {
const target = traverse(object, path, null);
if (isNull(target)) {
return target;
} else if (typeof target === "string") {
return target;
} else {
// We don't consider arbitrary objects as convertable to strings even through they will result in some value
const coercedValue = typeof target === "object" ? null : String(target);
switch (policy) {
case "strict": {
validation.context("asString", () => {
validation.unexpectedType("coercedValue", "string", target, objectPathToString(path));
});
break;
}
case "coercible": {
if (isNull(coercedValue)) {
validation.context("asString", () => {
validation.unexpectedType("coercedValue", "string", target, objectPathToString(path));
});
}
break;
}
case "none":
default: {
break;
}
}
return coercedValue;
}
}
/**
* Safely coerce an object into a date.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a date.
* @param policy The validation policy to use when resolving this value
* @returns A date from `object`, or `null` if `object` is null.
*/
export function asDate(object: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<Date> {
const dateString = asString(object, path, policy);
if (isNothing(dateString)) {
return null;
}
return new Date(dateString);
}
/**
* Safely coerce an object into a number.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a number.
* Omit this parameter if `object` is itself a number.
* @param policy The validation policy to use when resolving this value
* @returns A number from `object`, or `null` if `object` is null.
*/
export function asNumber(object: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<number> {
const target = traverse(object, path, null);
if (isNull(target) || typeof target === "number") {
return target;
} else {
const coercedValue = Number(target);
switch (policy) {
case "strict": {
validation.context("asNumber", () => {
validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
});
break;
}
case "coercible": {
if (isNaN(coercedValue)) {
validation.context("asNumber", () => {
validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
});
return null;
}
break;
}
case "none":
default: {
break;
}
}
return coercedValue;
}
}
/**
* Safely coerce an object into a dictionary.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find the dictionary.
* Omit this parameter if `object` is itself a dictionary.
* @param defaultValue The object to return if the path search fails.
* @returns A sub-dictionary from `object`, or `null` if `object` is null.
*/
export function asDictionary<Type extends JSONValue>(
object: JSONValue,
path?: ObjectPath,
defaultValue?: MapLike<Type>,
): MapLike<Type> | null {
const target = traverse(object, path, null);
if (target instanceof Object && !Array.isArray(target)) {
// Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time
return target as MapLike<Type>;
} else {
if (!isNull(target)) {
validation.context("asDictionary", () => {
validation.unexpectedType("defaultValue", "object", target, objectPathToString(path));
});
}
if (isDefinedNonNull(defaultValue)) {
return defaultValue;
}
return null;
}
}
/**
* Safely coerce an object into a given interface.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a string.
* Omit this parameter if `object` is itself a string.
* @param defaultValue The object to return if the path search fails.
* @returns A sub-dictionary from `object`, or `null` if `object` is null.
*/
export function asInterface<Interface>(
object: JSONValue,
path?: ObjectPath,
defaultValue?: JSONData,
): Interface | null {
return asDictionary(object, path, defaultValue) as unknown as Interface;
}
/**
* Coerce an object into a boolean.
* @param object The object to coerce.
* @param path The path to traverse on `object` to find a boolean.
* Omit this parameter if `object` is itself a boolean.
* @param policy The validation policy to use when resolving this value
* @returns A boolean from `object`, or `null` if `object` is null.
* @note This is distinct from `asBooleanOrFalse` in that it doesn't default to false,
* and it tries to convert string boolean values into actual boolean types
*/
export function asBoolean(
object: JSONValue,
path?: ObjectPath,
policy: ValidationPolicy = "coercible",
): boolean | null {
const target = traverse(object, path, null);
// Value was null
if (isNull(target)) {
return null;
}
// Value was boolean.
if (typeof target === "boolean") {
return target;
}
// Value was string.
if (typeof target === "string") {
if (target === "true") {
return true;
} else if (target === "false") {
return false;
}
}
// Else coerce.
const coercedValue = Boolean(target);
switch (policy) {
case "strict": {
validation.context("asBoolean", () => {
validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
});
break;
}
case "coercible": {
if (isNull(coercedValue)) {
validation.context("asBoolean", () => {
validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
});
return null;
}
break;
}
case "none":
default: {
break;
}
}
return coercedValue;
}
/**
* Attempts to coerce the passed value to a JSONValue
*
* Note: due to performance concerns this does not perform a deep inspection of Objects or Arrays.
*
* @param value The value to coerce
* @return A JSONValue or null if value is not a valid JSONValue type
*/
export function asJSONValue(value: unknown): JSONValue | null {
if (value === null || value === undefined) {
return null;
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
return value as JSONValue;
case "object":
// Note: It's too expensive to actually validate this is an array of JSONValues at run time
if (Array.isArray(value)) {
return value as JSONValue;
}
// Note: It's too expensive to actually validate this is a dictionary of { string : JSONValue } at run time
return value as JSONValue;
default:
validation.context("asJSONValue", () => {
validation.unexpectedType("defaultValue", "JSONValue", typeof value);
});
return null;
}
}
/**
* Attempts to coerce the passed value to JSONData
*
* @param value The value to coerce
* @return A JSONData or null if the value is not a valid JSONData object
*/
export function asJSONData(value: unknown): JSONData | null {
if (value === null || value === undefined) {
return null;
}
if (value instanceof Object && !Array.isArray(value)) {
// Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time
return value as JSONData;
}
validation.context("asJSONValue", () => {
validation.unexpectedType("defaultValue", "object", typeof value);
});
return null;
}
// endregion

View File

@@ -0,0 +1,469 @@
/**
* Created by keithpk on 12/2/16.
*/
import { isNothing, Nothing, Opt } from "@jet/environment/types/optional";
import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "./server-data";
export type Query = {
[key: string]: string | undefined;
};
export type URLComponent = "protocol" | "username" | "password" | "host" | "port" | "pathname" | "query" | "hash";
const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i;
const queryParamRegex = /([^=?&]+)=?([^&]*)/g;
const componentOrder: URLComponent[] = ["hash", "query", "pathname", "host"];
type URLSplitStyle = "prefix" | "suffix";
type URLSplitResult = {
result?: string;
remainder: string;
};
function splitUrlComponent(input: string, marker: string, style: URLSplitStyle): URLSplitResult {
const index = input.indexOf(marker);
let result;
let remainder = input;
if (index !== -1) {
const prefix = input.slice(0, index);
const suffix = input.slice(index + marker.length, input.length);
if (style === "prefix") {
result = prefix;
remainder = suffix;
} else {
result = suffix;
remainder = prefix;
}
}
// log("Token: " + marker + " String: " + input, " Result: " + result + " Remainder: " + remainder)
return {
result: result,
remainder: remainder,
};
}
export class URL {
protocol?: Opt<string>;
username: string;
password: string;
host?: Opt<string>;
port: string;
pathname?: Opt<string>;
query?: Query = {};
hash?: string;
constructor(url?: string) {
if (isNullOrEmpty(url)) {
return;
}
// Split the protocol from the rest of the urls
let remainder = url;
const match = protocolRegex.exec(url);
if (match != null) {
// Pull out the protocol
let protocol = match[1];
if (protocol) {
protocol = protocol.split(":")[0];
}
this.protocol = protocol;
// Save the remainder
remainder = match[3];
}
// Then match each component in a specific order
let parse: URLSplitResult = { remainder: remainder, result: undefined };
for (const component of componentOrder) {
if (!parse.remainder) {
break;
}
switch (component) {
case "hash": {
parse = splitUrlComponent(parse.remainder, "#", "suffix");
this.hash = parse.result;
break;
}
case "query": {
parse = splitUrlComponent(parse.remainder, "?", "suffix");
if (isDefinedNonNullNonEmpty(parse.result)) {
this.query = URL.queryFromString(parse.result);
}
break;
}
case "pathname": {
parse = splitUrlComponent(parse.remainder, "/", "suffix");
if (isDefinedNonNullNonEmpty(parse.result)) {
// Replace the initial /, since paths require it
this.pathname = "/" + parse.result;
}
break;
}
case "host": {
if (parse.remainder) {
const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix");
const userInfo = authorityParse.result;
const hostPort = authorityParse.remainder;
if (isDefinedNonNullNonEmpty(userInfo)) {
const userInfoSplit = userInfo.split(":");
this.username = decodeURIComponent(userInfoSplit[0]);
this.password = decodeURIComponent(userInfoSplit[1]);
}
if (hostPort) {
const hostPortSplit = hostPort.split(":");
this.host = hostPortSplit[0];
this.port = hostPortSplit[1];
}
}
break;
}
default: {
throw new Error("Unhandled case!");
}
}
}
}
set(component: URLComponent, value: string | Query): URL {
if (isNullOrEmpty(value)) {
return this;
}
if (component === "query") {
if (typeof value === "string") {
value = URL.queryFromString(value);
}
}
switch (component) {
// Exhaustive match to make sure TS property minifiers and other
// transformer plugins do not break this code.
case "protocol":
this.protocol = value as string;
break;
case "username":
this.username = value as string;
break;
case "password":
this.password = value as string;
break;
case "port":
this.port = value as string;
break;
case "pathname":
this.pathname = value as string;
break;
case "query":
this.query = value as Query;
break;
case "hash":
this.hash = value as string;
break;
default:
// The fallback for component which is not a property of URL object.
this[component] = value as string;
break;
}
return this;
}
private get(component: URLComponent): string | Query | Nothing {
switch (component) {
// Exhaustive match to make sure TS property minifiers and other
// transformer plugins do not break this code.
case "protocol":
return this.protocol;
case "username":
return this.username;
case "password":
return this.password;
case "port":
return this.port;
case "pathname":
return this.pathname;
case "query":
return this.query;
case "hash":
return this.hash;
default:
// The fallback for component which is not a property of URL object.
return this[component];
}
}
append(component: URLComponent, value: string | Query): URL {
const existingValue = this.get(component);
let newValue;
if (component === "query") {
if (typeof value === "string") {
value = URL.queryFromString(value);
}
if (typeof existingValue === "string") {
newValue = { existingValue, ...value };
} else {
newValue = { ...existingValue, ...value };
}
} else {
let existingValueString = existingValue as string;
if (!existingValueString) {
existingValueString = "";
}
let newValueString = existingValueString;
if (component === "pathname") {
const pathLength = existingValueString.length;
if (!pathLength || existingValueString[pathLength - 1] !== "/") {
newValueString += "/";
}
}
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string
newValueString += value;
newValue = newValueString;
}
return this.set(component, newValue);
}
param(key: string, value?: string): URL {
if (!key) {
return this;
}
if (this.query == null) {
this.query = {};
}
this.query[key] = value;
return this;
}
removeParam(key: string): URL {
if (!key || this.query == null) {
return this;
}
if (this.query[key] !== undefined) {
delete this.query[key];
}
return this;
}
/**
* Push a new string value onto the path for this url
* @returns URL this object with the updated path.
*/
path(value: string): URL {
return this.append("pathname", value);
}
pathExtension(): Opt<string> {
// Extract path extension if one exists
if (isNothing(this.pathname)) {
return null;
}
const lastFilenameComponents = this.pathname
.split("/")
.filter((item) => item.length > 0) // Remove any double or trailing slashes
.pop()
?.split(".");
if (lastFilenameComponents === undefined) {
return null;
}
if (
lastFilenameComponents.filter((part) => {
return part !== "";
}).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"])
) {
return null;
}
return lastFilenameComponents.pop();
}
/**
* Returns the path components of the URL
* @returns An array of non-empty path components from `urls`.
*/
pathComponents(): string[] {
if (isNullOrEmpty(this.pathname)) {
return [];
}
return this.pathname.split("/").filter((component) => component.length > 0);
}
/**
* Returns the last path component from this url, updating the url to not include this path component
* @returns String the last path component from this url.
*/
popPathComponent(): string | null {
if (isNullOrEmpty(this.pathname)) {
return null;
}
const lastPathComponent = this.pathname.slice(this.pathname.lastIndexOf("/") + 1);
if (lastPathComponent.length === 0) {
return null;
}
this.pathname = this.pathname.slice(0, this.pathname.lastIndexOf("/"));
return lastPathComponent;
}
/**
* Same as toString
*
* @returns {string} A string representation of the URL
*/
build(): string {
return this.toString();
}
/**
* Converts the URL to a string
*
* @returns {string} A string representation of the URL
*/
toString(): string {
let url = "";
if (isDefinedNonNullNonEmpty(this.protocol)) {
url += this.protocol + "://";
}
if (this.username) {
url += encodeURIComponent(this.username);
if (this.password) {
url += ":" + encodeURIComponent(this.password);
}
url += "@";
}
if (isDefinedNonNullNonEmpty(this.host)) {
url += this.host;
if (this.port) {
url += ":" + this.port;
}
}
if (isDefinedNonNullNonEmpty(this.pathname)) {
url += this.pathname;
/// Trim off trailing path separators when we have a valid path
/// We don't do this unless pathname has elements otherwise we will trim the `://`
if (url.endsWith("/") && this.pathname.length > 0) {
url = url.slice(0, -1);
}
}
if (this.query != null && Object.keys(this.query).length > 0) {
url += "?" + URL.toQueryString(this.query);
}
if (isDefinedNonNullNonEmpty(this.hash)) {
url += "#" + this.hash;
}
return url;
}
// ----------------
// Static API
// ----------------
/**
* Converts a string into a query dictionary
* @param query The string to parse
* @returns The query dictionary containing the key-value pairs in the query string
*/
static queryFromString(query: string): Query {
const result = {};
let parseResult = queryParamRegex.exec(query);
while (parseResult != null) {
const key = decodeURIComponent(parseResult[1]);
const value = decodeURIComponent(parseResult[2]);
result[key] = value;
parseResult = queryParamRegex.exec(query);
}
return result;
}
/**
* Converts a query dictionary into a query string
*
* @param query The query dictionary
* @returns {string} The string representation of the query dictionary
*/
static toQueryString(query: Query) {
let queryString = "";
let first = true;
for (const key of Object.keys(query)) {
if (!first) {
queryString += "&";
}
first = false;
queryString += encodeURIComponent(key);
const value = query[key];
if (isDefinedNonNullNonEmpty(value) && value.length) {
queryString += "=" + encodeURIComponent(value);
}
}
return queryString;
}
/**
* Convenience method to instantiate a URL from a string
* @param url The URL string to parse
* @returns {URL} The new URL object representing the URL
*/
static from(url: string): URL {
return new URL(url);
}
/**
* Convenience method to instantiate a URL from numerous (optional) components
* @param protocol The protocol type
* @param host The host name
* @param path The path
* @param query The query
* @param hash The hash
* @returns {URL} The new URL object representing the URL
*/
static fromComponents(
protocol?: Opt<string>,
host?: Opt<string>,
path?: Opt<string>,
query?: Query,
hash?: string,
): URL {
const url = new URL();
url.protocol = protocol;
url.host = host;
url.pathname = path;
url.query = query;
url.hash = hash;
return url;
}
}

View File

@@ -0,0 +1,403 @@
/**
* Created by ls on 9/7/2018.
*
* This `network.ts` is the NON-MEDIA API arm of network fetch requests.
* It is built on `Network` object and provides standard functionality, such as:
* 1. Parsing the body into specific format.
* 2. Adding timing metrics onto blob.
*
* This should *only* be used for objects that should have timing metrics, i.e. requests to Non-MediaAPI endpoints
* that will ultimately render some whole page. Otherwise, use `objectGraph.network.fetch` directly.
*
* @see `src/media/network.ts` for fetching from Media API endpoints
*/
import { FetchRequest, FetchResponse, HTTPTimingMetrics } from "@jet/environment/types/globals/net";
import { FetchTimingMetrics, MetricsFields } from "@jet/environment/types/metrics";
import { FetchTimingMetricsBuilder } from "@jet/environment/metrics";
import { isSome, Opt } from "@jet/environment/types/optional";
import * as serverData from "./models/server-data";
import { ParsedNetworkResponse } from "./models/data-structure";
import { Request } from "./models/request";
import * as urls from "./models/urls";
import * as urlBuilder from "./url-builder";
import { MediaConfigurationType, MediaTokenService } from "./models/mediapi-configuration";
import { HTTPMethod, HTTPCachePolicy, HTTPSigningStyle, HTTPHeaders } from "@jet/environment";
/** @public */
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ResponseMetadata {
export const requestedUrl = "_jet-internal:metricsHelpers_requestedUrl";
/**
* Symbol used to place timing metrics values onto fetch responses
* without interfering with the data returned by the server.
*/
export const timingValues = "_jet-internal:metricsHelpers_timingValues";
/**
* Key used to access the page information gathered from a response's headers
*/
export const pageInformation = "_jet-internal:metricsHelpers_pageInformation";
/**
* Key used to access the content max-age gathered from a response's headers.
*/
export const contentMaxAge = "_jet-internal:responseMetadata_contentMaxAge";
}
/**
* Module's private fetch implementation built off `net` global.
*
* @param {FetchRequest} request describes fetch request.
* @param {(value: string) => Type} parser Some function parsing response body `string` into specific type.
* @returns {Promise<Type>} Promise resolving to specific object.
* @throws {Error} Throws error if status code of request is not 200.
*
* @note Similar to `fetchWithToken` in `media` module, but excludes media token specific functionality.
* Top level data fetches to endpoints that don't do redirects, and can benefit from metrics should
* call methods that build off of this instead of calling `objectGraph.network.fetch(...)` directly.
*/
export async function fetch<Type>(
configuration: MediaConfigurationType,
request: FetchRequest,
parser: (value: Opt<string>) => Type,
): Promise<Type & ParsedNetworkResponse> {
const response = await configuration.network.fetch(request);
if (!response.ok) {
throw Error(`Bad Status code ${response.status} for ${request.url}`);
}
const parseStartTime = Date.now();
const result = parser(response.body) as Type & ParsedNetworkResponse;
const parseEndTime = Date.now();
// Build full network timing metrics.
const completeTimingMetrics = networkTimingMetricsWithParseTime(response.metrics, parseStartTime, parseEndTime);
if (serverData.isDefinedNonNull(completeTimingMetrics)) {
result[ResponseMetadata.timingValues] = completeTimingMetrics;
}
result[ResponseMetadata.requestedUrl] = request.url.toString();
return result;
}
/**
* Fetch from an endpoint with JSON response body.
*
* @param {FetchRequest} request to fetch from endpoint with JSON response..
* @returns {Promise<Type>} Promise resolving to body of response parsed as `Type`.
* @throws {Error} Throws error if status code of request is not 200.
*/
export async function fetchJSON<Type>(configuration: MediaConfigurationType, request: FetchRequest): Promise<Type> {
return await fetch(configuration, request, (body) => {
if (isSome(body)) {
return JSON.parse(body) as Type;
} else {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return {} as Type;
}
});
}
/**
* Fetch from an endpoint with XML response body.
*
* @param {FetchRequest} request to fetch from endpoint with XML response.
* @returns {Promise<Type>} Promise resolving to body of response parsed as `Type`.
* @throws {Error} Throws error if status code of request is not 200.
*/
export async function fetchPlist<Type>(configuration: MediaConfigurationType, request: FetchRequest): Promise<Type> {
return await fetch(configuration, request, (body) => {
if (isSome(body)) {
return configuration.plist.parse(body) as Type;
} else {
throw new Error(`Could not fetch Plist, response body was not defined for ${request.url}`);
}
});
}
/**
* With network requests now being created and parsed in JS, different timing metrics are measured in both Native and JS.
* This function populates the missing values from `HTTPTimingMetrics`'s native counterpart, `JSNetworkPerformanceMetrics`.
*
* @param {HTTPTimingMetrics[] | null} responseMetrics Array of response metrics provided by native.
* @param {number} parseStartTime Time at which response body string parse began in JS.
* @param {number} parseEndTime Time at which response body string parse ended in JS.
* @returns {HTTPTimingMetrics | null} Fully populated timing metrics, or `null` if native response provided no metrics events to build off of.
*/
function networkTimingMetricsWithParseTime(
responseMetrics: HTTPTimingMetrics[] | null,
parseStartTime: number,
parseEndTime: number,
): FetchTimingMetrics | null {
// No metrics events to build from.
if (serverData.isNull(responseMetrics) || responseMetrics.length === 0) {
return null;
}
// Append parse times to first partial timing metrics from native.
const firstPartialTimingMetrics: FetchTimingMetrics = {
...responseMetrics[0],
parseStartTime: parseStartTime,
parseEndTime: parseEndTime,
};
// Timing metrics with all properties populated.
return firstPartialTimingMetrics;
}
export type FetchOptions = {
headers?: { [key: string]: string };
method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH";
requestBodyString?: string;
timeout?: number; // in seconds. Check for feature 'supportsRequestTimeoutOption'.
/// When true the fetch wont throw if we dont get any data back for given request.
allowEmptyDataResponse?: boolean;
excludeIdentifierHeadersForAccount?: boolean; // Defaults to false
alwaysIncludeAuthKitHeaders?: boolean; // Defaults to true
alwaysIncludeMMeClientInfoAndDeviceHeaders?: boolean; // Defaults to true
};
/**
* Implements the MAPI fetch, building URL from MAPI Request and opaquely managing initial token request and refreshes.
*
* @param {MediaConfigurationType} configuration Base media API configuration.
* @param {Request} request MAPI Request to fetch with.
* @param {FetchOptions} [options] FetchOptions for the MAPI request.
* @returns {Promise<Type>} Promise resolving to some type for given MAPI request.
*/
export async function fetchData<Type>(
configuration: MediaConfigurationType,
mediaToken: MediaTokenService,
request: Request,
options?: FetchOptions,
): Promise<Type & ParsedNetworkResponse> {
const url = urlBuilder.buildURLFromRequest(configuration, request).toString();
const startTime = Date.now();
const token = await mediaToken.refreshToken();
const response = await fetchWithToken<Type>(
configuration,
mediaToken,
url,
token,
options,
false,
configuration.fetchTimingMetricsBuilder,
);
const endTime = Date.now();
if (request.canonicalUrl) {
response[ResponseMetadata.requestedUrl] = request.canonicalUrl;
}
const roundTripTimeIncludingWaiting = endTime - startTime;
if (roundTripTimeIncludingWaiting > 500) {
console.warn("Fetch took too long (" + roundTripTimeIncludingWaiting.toString() + "ms) " + url);
}
return response;
}
export function redirectParametersInUrl(configuration: MediaConfigurationType, url: urls.URL): string[] {
const redirectURLParams = configuration.redirectUrlWhitelistedQueryParams;
return redirectURLParams.filter((param) => serverData.isDefinedNonNull(url.query?.[param]));
}
export type MediaAPIFetchRequest = {
url: string;
excludeIdentifierHeadersForAccount?: boolean;
alwaysIncludeAuthKitHeaders?: boolean;
alwaysIncludeMMeClientInfoAndDeviceHeaders?: boolean;
method?: Opt<HTTPMethod>;
cache?: Opt<HTTPCachePolicy>;
signingStyle?: Opt<HTTPSigningStyle>;
headers?: Opt<HTTPHeaders>;
timeout?: Opt<number>;
body?: Opt<string>;
};
/**
* Given a built URL, token, and options, calls into native networking APIs to fetch content.
*
* @param {string} url URL to fetch data from.
* @param {string} token MAPI token key.
* @param {FetchOptions} options Fetch options for MAPI requests.
* @param {boolean} isRetry flag indicating whether this is a fetch retry following a 401 request, and media token was refreshed.
* @returns {Promise<Type>} Promise resolving to some type for given MAPI request.
*/
async function fetchWithToken<Type>(
configuration: MediaConfigurationType,
mediaToken: MediaTokenService,
url: string,
token: string,
options: FetchOptions = {},
isRetry = false,
fetchTimingMetricsBuilder: Opt<FetchTimingMetricsBuilder>,
): Promise<Type & ParsedNetworkResponse> {
// Removes all affiliate/redirect params for caching (https://connectme.apple.com/docs/DOC-577671)
const filteredURL = new urls.URL(url);
const redirectParameters = redirectParametersInUrl(configuration, filteredURL);
for (const param of redirectParameters) {
filteredURL.removeParam(param);
}
const filteredUrlString = filteredURL.toString();
let headers = options.headers;
if (headers == null) {
headers = {};
}
headers["Authorization"] = "Bearer " + token;
const fetchRequest: MediaAPIFetchRequest = {
url: filteredUrlString,
excludeIdentifierHeadersForAccount: options.excludeIdentifierHeadersForAccount ?? false,
alwaysIncludeAuthKitHeaders: options.alwaysIncludeAuthKitHeaders ?? true,
alwaysIncludeMMeClientInfoAndDeviceHeaders: options.alwaysIncludeMMeClientInfoAndDeviceHeaders ?? true,
headers: headers,
method: options.method,
body: options.requestBodyString,
timeout: options.timeout,
};
const response = await configuration.network.fetch(fetchRequest);
try {
if (response.status === 401 || response.status === 403) {
if (isRetry) {
throw Error("We refreshed the token but we still get 401 from the API");
}
mediaToken.resetToken();
return await mediaToken.refreshToken().then(async (newToken) => {
// Explicitly re-fetch with the original request so logging and metrics are correct
return await fetchWithToken<Type>(
configuration,
mediaToken,
url,
newToken,
options,
true,
fetchTimingMetricsBuilder,
);
});
} else if (response.status === 404) {
// item is not available in this storefront or perhaps not at all
throw noContentError();
} else if (!response.ok) {
const correlationKey = response.headers["x-apple-jingle-correlation-key"] ?? "N/A";
const error = new NetworkError(
`Bad Status code ${response.status} (correlationKey: ${correlationKey}) for ${filteredUrlString}, original ${url}`,
);
error.statusCode = response.status;
throw error;
}
const parser = (resp: FetchResponse) => {
const parseStartTime = Date.now();
let result: Type & ParsedNetworkResponse;
if (serverData.isNull(resp.body) || resp.body === "") {
if (resp.status === 204) {
// 204 indicates a success, but the response will typically be empty
// Create a fake result so that we don't throw an error when JSON parsing
const emptyData: ParsedNetworkResponse = {};
result = emptyData as Type & ParsedNetworkResponse;
} else {
throw noContentError();
}
} else {
result = JSON.parse(resp.body) as Type & ParsedNetworkResponse;
}
const parseEndTime = Date.now();
result[ResponseMetadata.pageInformation] = serverData.asJSONData(
getPageInformationFromResponse(configuration, resp),
);
if (resp.metrics.length > 0) {
const metrics: FetchTimingMetrics = {
...resp.metrics[0],
parseStartTime: parseStartTime,
parseEndTime: parseEndTime,
};
result[ResponseMetadata.timingValues] = metrics;
} else {
const fallbackMetrics: FetchTimingMetrics = {
pageURL: resp.url,
parseStartTime,
parseEndTime,
};
result[ResponseMetadata.timingValues] = fallbackMetrics;
}
result[ResponseMetadata.contentMaxAge] = getContentTimeToLiveFromResponse(resp);
// If we have an empty data object, throw a 204 (No Content).
if (
Array.isArray(result.data) &&
serverData.isArrayDefinedNonNullAndEmpty(result.data) &&
!serverData.asBooleanOrFalse(options.allowEmptyDataResponse)
) {
throw noContentError();
}
result[ResponseMetadata.requestedUrl] = url;
return result;
};
if (isSome(fetchTimingMetricsBuilder)) {
return fetchTimingMetricsBuilder.measureParsing(response, parser);
} else {
return parser(response);
}
} catch (e) {
if (e instanceof NetworkError) {
throw e;
}
throw new Error(`Error Fetching - filtered: ${filteredUrlString}, original: ${url}, ${e.name}, ${e.message}`);
}
}
export class NetworkError extends Error {
statusCode?: number;
}
function noContentError(): NetworkError {
const error = new NetworkError(`No content`);
error.statusCode = 204;
return error;
}
const serverInstanceHeader = "x-apple-application-instance";
const environmentDataCenterHeader = "x-apple-application-site";
function getPageInformationFromResponse(
configuration: MediaConfigurationType,
response: FetchResponse,
): MetricsFields | null {
const storeFrontHeader: string = configuration.storefrontIdentifier;
let storeFront: Opt<string> = null;
if (serverData.isDefinedNonNullNonEmpty(storeFrontHeader)) {
const storeFrontHeaderComponents: string[] = storeFrontHeader.split("-");
if (serverData.isDefinedNonNullNonEmpty(storeFrontHeaderComponents)) {
storeFront = storeFrontHeaderComponents[0];
}
}
return {
serverInstance: response.headers[serverInstanceHeader],
storeFrontHeader: storeFrontHeader,
language: configuration.bagLanguage,
storeFront: storeFront,
environmentDataCenter: response.headers[environmentDataCenterHeader],
};
}
function getContentTimeToLiveFromResponse(response: FetchResponse): Opt<number> {
const cacheControlHeaderKey = Object.keys(response.headers).find((key) => key.toLowerCase() === "cache-control");
if (serverData.isNull(cacheControlHeaderKey) || cacheControlHeaderKey === "") {
return null;
}
const headerValue = response.headers[cacheControlHeaderKey];
if (serverData.isNullOrEmpty(headerValue)) {
return null;
}
const matches = headerValue.match(/max-age=(\d+)/);
if (serverData.isNull(matches) || matches.length < 2) {
return null;
}
return serverData.asNumber(matches[1]);
}