This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,826 @@
import DataModel from "../abstract/data.mjs";
import * as fields from "../data/fields.mjs";
import {
COMPENDIUM_DOCUMENT_TYPES, DOCUMENT_OWNERSHIP_LEVELS,
PACKAGE_AVAILABILITY_CODES,
PACKAGE_TYPES,
SYSTEM_SPECIFIC_COMPENDIUM_TYPES,
USER_ROLES
} from "../constants.mjs";
import {isNewerVersion, logCompatibilityWarning, mergeObject} from "../utils/module.mjs";
import BaseFolder from "../documents/folder.mjs";
import {ObjectField} from "../data/fields.mjs";
import {DataModelValidationFailure} from "../data/validation-failure.mjs";
/**
* A custom SchemaField for defining package compatibility versions.
* @property {string} minimum The Package will not function before this version
* @property {string} verified Verified compatible up to this version
* @property {string} maximum The Package will not function after this version
*/
export class PackageCompatibility extends fields.SchemaField {
constructor(options) {
super({
minimum: new fields.StringField({required: false, blank: false, initial: undefined}),
verified: new fields.StringField({required: false, blank: false, initial: undefined}),
maximum: new fields.StringField({required: false, blank: false, initial: undefined})
}, options);
}
}
/* -------------------------------------------- */
/**
* A custom SchemaField for defining package relationships.
* @property {RelatedPackage[]} systems Systems that this Package supports
* @property {RelatedPackage[]} requires Packages that are required for base functionality
* @property {RelatedPackage[]} recommends Packages that are recommended for optimal functionality
*/
export class PackageRelationships extends fields.SchemaField {
/** @inheritdoc */
constructor(options) {
super({
systems: new PackageRelationshipField(new RelatedPackage({packageType: "system"})),
requires: new PackageRelationshipField(new RelatedPackage()),
recommends: new PackageRelationshipField(new RelatedPackage()),
conflicts: new PackageRelationshipField(new RelatedPackage()),
flags: new fields.ObjectField()
}, options);
}
}
/* -------------------------------------------- */
/**
* A SetField with custom casting behavior.
*/
class PackageRelationshipField extends fields.SetField {
/** @override */
_cast(value) {
return value instanceof Array ? value : [value];
}
}
/* -------------------------------------------- */
/**
* A custom SchemaField for defining a related Package.
* It may be required to be a specific type of package, by passing the packageType option to the constructor.
*/
export class RelatedPackage extends fields.SchemaField {
constructor({packageType, ...options}={}) {
let typeOptions = {choices: PACKAGE_TYPES, initial:"module"};
if ( packageType ) typeOptions = {choices: [packageType], initial: packageType};
super({
id: new fields.StringField({required: true, blank: false}),
type: new fields.StringField(typeOptions),
manifest: new fields.StringField({required: false, blank: false, initial: undefined}),
compatibility: new PackageCompatibility(),
reason: new fields.StringField({required: false, blank: false, initial: undefined})
}, options);
}
}
/* -------------------------------------------- */
/**
* A custom SchemaField for defining the folder structure of the included compendium packs.
*/
export class PackageCompendiumFolder extends fields.SchemaField {
constructor({depth=1, ...options}={}) {
const schema = {
name: new fields.StringField({required: true, blank: false}),
sorting: new fields.StringField({required: false, blank: false, initial: undefined,
choices: BaseFolder.SORTING_MODES}),
color: new fields.ColorField(),
packs: new fields.SetField(new fields.StringField({required: true, blank: false}))
};
if ( depth < 4 ) schema.folders = new fields.SetField(new PackageCompendiumFolder(
{depth: depth+1, options}));
super(schema, options);
}
}
/* -------------------------------------------- */
/**
* A special ObjectField which captures a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS.
*/
export class CompendiumOwnershipField extends ObjectField {
/** @inheritdoc */
static get _defaults() {
return mergeObject(super._defaults, {
initial: {PLAYER: "OBSERVER", ASSISTANT: "OWNER"},
validationError: "is not a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS"
});
}
/** @override */
_validateType(value, options) {
for ( let [k, v] of Object.entries(value) ) {
if ( !(k in USER_ROLES) ) throw new Error(`Compendium ownership key "${k}" is not a valid choice in USER_ROLES`);
if ( !(v in DOCUMENT_OWNERSHIP_LEVELS) ) throw new Error(`Compendium ownership value "${v}" is not a valid
choice in DOCUMENT_OWNERSHIP_LEVELS`);
}
}
}
/* -------------------------------------------- */
/**
* A special SetField which provides additional validation and initialization behavior specific to compendium packs.
*/
export class PackageCompendiumPacks extends fields.SetField {
/** @override */
_cleanType(value, options) {
return value.map(v => {
v = this.element.clean(v, options);
if ( v.path ) v.path = v.path.replace(/\.db$/, ""); // Strip old NEDB extensions
else v.path = `packs/${v.name}`; // Auto-populate a default pack path
return v;
})
}
/* ---------------------------------------- */
/** @override */
initialize(value, model, options={}) {
const packs = new Set();
const packageName = model._source.id;
for ( let v of value ) {
try {
const pack = this.element.initialize(v, model, options);
pack.packageType = model.constructor.type;
pack.packageName = packageName;
pack.id = `${model.constructor.type === "world" ? "world" : packageName}.${pack.name}`;
packs.add(pack);
} catch(err) {
logger.warn(err.message);
}
}
return packs;
}
/* ---------------------------------------- */
/**
* Extend the logic for validating the complete set of packs to ensure uniqueness.
* @inheritDoc
*/
_validateElements(value, options) {
const packNames = new Set();
const duplicateNames = new Set();
const packPaths = new Set();
const duplicatePaths = new Set();
for ( const pack of value ) {
if ( packNames.has(pack.name) ) duplicateNames.add(pack.name);
packNames.add(pack.name);
if ( pack.path ) {
if ( packPaths.has(pack.path) ) duplicatePaths.add(pack.path);
packPaths.add(pack.path);
}
}
return super._validateElements(value, {...options, duplicateNames, duplicatePaths});
}
/* ---------------------------------------- */
/**
* Validate each individual compendium pack, ensuring its name and path are unique.
* @inheritDoc
*/
_validateElement(value, {duplicateNames, duplicatePaths, ...options}={}) {
if ( duplicateNames.has(value.name) ) {
return new DataModelValidationFailure({
invalidValue: value.name,
message: `Duplicate Compendium name "${value.name}" already declared by some other pack`,
unresolved: true
});
}
if ( duplicatePaths.has(value.path) ) {
return new DataModelValidationFailure({
invalidValue: value.path,
message: `Duplicate Compendium path "${value.path}" already declared by some other pack`,
unresolved: true
});
}
return this.element.validate(value, options);
}
}
/* -------------------------------------------- */
/**
* The data schema used to define a Package manifest.
* Specific types of packages extend this schema with additional fields.
*/
export default class BasePackage extends DataModel {
/**
* @param {PackageManifestData} data Source data for the package
* @param {object} [options={}] Options which affect DataModel construction
*/
constructor(data, options={}) {
const {availability, locked, exclusive, owned, tags, hasStorage} = data;
super(data, options);
/**
* An availability code in PACKAGE_AVAILABILITY_CODES which defines whether this package can be used.
* @type {number}
*/
this.availability = availability ?? this.constructor.testAvailability(this);
/**
* A flag which tracks whether this package is currently locked.
* @type {boolean}
*/
this.locked = locked ?? false;
/**
* A flag which tracks whether this package is a free Exclusive pack
* @type {boolean}
*/
this.exclusive = exclusive ?? false;
/**
* A flag which tracks whether this package is owned, if it is protected.
* @type {boolean|null}
*/
this.owned = owned ?? false;
/**
* A set of Tags that indicate what kind of Package this is, provided by the Website
* @type {string[]}
*/
this.tags = tags ?? [];
/**
* A flag which tracks if this package has files stored in the persistent storage folder
* @type {boolean}
*/
this.hasStorage = hasStorage ?? false;
}
/**
* Define the package type in CONST.PACKAGE_TYPES that this class represents.
* Each BasePackage subclass must define this attribute.
* @virtual
* @type {string}
*/
static type = "package";
/**
* The type of this package instance. A value in CONST.PACKAGE_TYPES.
* @type {string}
*/
get type() {
return this.constructor.type;
}
/**
* The canonical identifier for this package
* @return {string}
* @deprecated
*/
get name() {
logCompatibilityWarning("You are accessing BasePackage#name which is now deprecated in favor of id.",
{since: 10, until: 13});
return this.id;
}
/**
* A flag which defines whether this package is unavailable to be used.
* @type {boolean}
*/
get unavailable() {
return this.availability > PACKAGE_AVAILABILITY_CODES.UNVERIFIED_GENERATION;
}
/**
* Is this Package incompatible with the currently installed core Foundry VTT software version?
* @type {boolean}
*/
get incompatibleWithCoreVersion() {
return this.constructor.isIncompatibleWithCoreVersion(this.availability);
}
/**
* Test if a given availability is incompatible with the core version.
* @param {number} availability The availability value to test.
* @returns {boolean}
*/
static isIncompatibleWithCoreVersion(availability) {
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
return (availability >= codes.REQUIRES_CORE_DOWNGRADE) && (availability <= codes.REQUIRES_CORE_UPGRADE_UNSTABLE);
}
/**
* The named collection to which this package type belongs
* @type {string}
*/
static get collection() {
return `${this.type}s`;
}
/** @inheritDoc */
static defineSchema() {
const optionalString = {required: false, blank: false, initial: undefined};
return {
// Package metadata
id: new fields.StringField({required: true, blank: false, validate: this.validateId}),
title: new fields.StringField({required: true, blank: false}),
description: new fields.StringField({required: true}),
authors: new fields.SetField(new fields.SchemaField({
name: new fields.StringField({required: true, blank: false}),
email: new fields.StringField(optionalString),
url: new fields.StringField(optionalString),
discord: new fields.StringField(optionalString),
flags: new fields.ObjectField(),
})),
url: new fields.StringField(optionalString),
license: new fields.StringField(optionalString),
readme: new fields.StringField(optionalString),
bugs: new fields.StringField(optionalString),
changelog: new fields.StringField(optionalString),
flags: new fields.ObjectField(),
media: new fields.SetField(new fields.SchemaField({
type: new fields.StringField(optionalString),
url: new fields.StringField(optionalString),
caption: new fields.StringField(optionalString),
loop: new fields.BooleanField({required: false, blank: false, initial: false}),
thumbnail: new fields.StringField(optionalString),
flags: new fields.ObjectField(),
})),
// Package versioning
version: new fields.StringField({required: true, blank: false, initial: "0"}),
compatibility: new PackageCompatibility(),
// Included content
scripts: new fields.SetField(new fields.StringField({required: true, blank: false})),
esmodules: new fields.SetField(new fields.StringField({required: true, blank: false})),
styles: new fields.SetField(new fields.StringField({required: true, blank: false})),
languages: new fields.SetField(new fields.SchemaField({
lang: new fields.StringField({required: true, blank: false, validate: Intl.getCanonicalLocales,
validationError: "must be supported by the Intl.getCanonicalLocales function"
}),
name: new fields.StringField({required: false}),
path: new fields.StringField({required: true, blank: false}),
system: new fields.StringField(optionalString),
module: new fields.StringField(optionalString),
flags: new fields.ObjectField(),
})),
packs: new PackageCompendiumPacks(new fields.SchemaField({
name: new fields.StringField({required: true, blank: false, validate: this.validateId}),
label: new fields.StringField({required: true, blank: false}),
banner: new fields.StringField({...optionalString, nullable: true}),
path: new fields.StringField({required: false}),
type: new fields.StringField({required: true, blank: false, choices: COMPENDIUM_DOCUMENT_TYPES,
validationError: "must be a value in CONST.COMPENDIUM_DOCUMENT_TYPES"}),
system: new fields.StringField(optionalString),
ownership: new CompendiumOwnershipField(),
flags: new fields.ObjectField(),
}, {validate: BasePackage.#validatePack})),
packFolders: new fields.SetField(new PackageCompendiumFolder()),
// Package relationships
relationships: new PackageRelationships(),
socket: new fields.BooleanField(),
// Package downloading
manifest: new fields.StringField(),
download: new fields.StringField({required: false, blank: false, initial: undefined}),
protected: new fields.BooleanField(),
exclusive: new fields.BooleanField(),
persistentStorage: new fields.BooleanField(),
}
}
/* -------------------------------------------- */
/**
* Check the given compatibility data against the current installation state and determine its availability.
* @param {Partial<PackageManifestData>} data The compatibility data to test.
* @param {object} [options]
* @param {ReleaseData} [options.release] A specific software release for which to test availability.
* Tests against the current release by default.
* @returns {number}
*/
static testAvailability({ compatibility }, { release }={}) {
release ??= globalThis.release ?? game.release;
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
const {minimum, maximum, verified} = compatibility;
const isGeneration = version => Number.isInteger(Number(version));
// Require a certain minimum core version.
if ( minimum && isNewerVersion(minimum, release.version) ) {
const generation = Number(minimum.split(".").shift());
const isStable = generation <= release.maxStableGeneration;
const exists = generation <= release.maxGeneration;
if ( isStable ) return codes.REQUIRES_CORE_UPGRADE_STABLE;
return exists ? codes.REQUIRES_CORE_UPGRADE_UNSTABLE : codes.UNKNOWN;
}
// Require a certain maximum core version.
if ( maximum ) {
const compatible = isGeneration(maximum)
? release.generation <= Number(maximum)
: !isNewerVersion(release.version, maximum);
if ( !compatible ) return codes.REQUIRES_CORE_DOWNGRADE;
}
// Require a certain compatible core version.
if ( verified ) {
const compatible = isGeneration(verified)
? Number(verified) >= release.generation
: !isNewerVersion(release.version, verified);
const sameGeneration = release.generation === Number(verified.split(".").shift());
if ( compatible ) return codes.VERIFIED;
return sameGeneration ? codes.UNVERIFIED_BUILD : codes.UNVERIFIED_GENERATION;
}
// FIXME: Why do we not check if all of this package's dependencies are satisfied?
// Proposal: Check all relationships.requires and set MISSING_DEPENDENCY if any dependencies are not VERIFIED,
// UNVERIFIED_BUILD, or UNVERIFIED_GENERATION, or if they do not satisfy the given compatibility range for the
// relationship.
// No compatible version is specified.
return codes.UNKNOWN;
}
/* -------------------------------------------- */
/**
* Test that the dependencies of a package are satisfied as compatible.
* This method assumes that all packages in modulesCollection have already had their own availability tested.
* @param {Collection<string,Module>} modulesCollection A collection which defines the set of available modules
* @returns {Promise<boolean>} Are all required dependencies satisfied?
* @internal
*/
async _testRequiredDependencies(modulesCollection) {
const requirements = this.relationships.requires;
for ( const {id, type, manifest, compatibility} of requirements ) {
if ( type !== "module" ) continue; // Only test modules
let pkg;
// If the requirement specifies an explicit remote manifest URL, we need to load it
if ( manifest ) {
try {
pkg = await this.constructor.fromRemoteManifest(manifest, {strict: true});
} catch(err) {
return false;
}
}
// Otherwise the dependency must belong to the known modulesCollection
else pkg = modulesCollection.get(id);
if ( !pkg ) return false;
// Ensure that the package matches the required compatibility range
if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) ) return false;
// Test compatibility of the dependency
if ( pkg.unavailable ) return false;
}
return true;
}
/* -------------------------------------------- */
/**
* Test compatibility of a package's supported systems.
* @param {Collection<string, System>} systemCollection A collection which defines the set of available systems.
* @returns {Promise<boolean>} True if all supported systems which are currently installed
* are compatible or if the package has no supported systems.
* Returns false otherwise, or if no supported systems are
* installed.
* @internal
*/
async _testSupportedSystems(systemCollection) {
const systems = this.relationships.systems;
if ( !systems?.size ) return true;
let supportedSystem = false;
for ( const { id, compatibility } of systems ) {
const pkg = systemCollection.get(id);
if ( !pkg ) continue;
if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) || pkg.unavailable ) return false;
supportedSystem = true;
}
return supportedSystem;
}
/* -------------------------------------------- */
/**
* Determine if a dependency is within the given compatibility range.
* @param {PackageCompatibility} compatibility The compatibility range declared for the dependency, if any
* @param {BasePackage} dependency The known dependency package
* @returns {boolean} Is the dependency compatible with the required range?
*/
static testDependencyCompatibility(compatibility, dependency) {
if ( !compatibility ) return true;
const {minimum, maximum} = compatibility;
if ( minimum && isNewerVersion(minimum, dependency.version) ) return false;
if ( maximum && isNewerVersion(dependency.version, maximum) ) return false;
return true;
}
/* -------------------------------------------- */
/** @inheritDoc */
static cleanData(source={}, { installed, ...options }={}) {
// Auto-assign language name
for ( let l of source.languages || [] ) {
l.name = l.name ?? l.lang;
}
// Identify whether this package depends on a single game system
let systemId = undefined;
if ( this.type === "system" ) systemId = source.id;
else if ( this.type === "world" ) systemId = source.system;
else if ( source.relationships?.systems?.length === 1 ) systemId = source.relationships.systems[0].id;
// Auto-configure some package data
for ( const pack of source.packs || [] ) {
if ( !pack.system && systemId ) pack.system = systemId; // System dependency
if ( typeof pack.ownership === "string" ) pack.ownership = {PLAYER: pack.ownership};
}
/**
* Clean unsupported non-module dependencies in requires or recommends.
* @deprecated since v11
*/
["requires", "recommends"].forEach(rel => {
const pkgs = source.relationships?.[rel];
if ( !Array.isArray(pkgs) ) return;
const clean = [];
for ( const pkg of pkgs ) {
if ( !pkg.type || (pkg.type === "module") ) clean.push(pkg);
}
const diff = pkgs.length - clean.length;
if ( diff ) {
source.relationships[rel] = clean;
this._logWarning(
source.id,
`The ${this.type} "${source.id}" has a ${rel} relationship on a non-module, which is not supported.`,
{ since: 11, until: 13, stack: false, installed });
}
});
return super.cleanData(source, options);
}
/* -------------------------------------------- */
/**
* Validate that a Package ID is allowed.
* @param {string} id The candidate ID
* @throws An error if the candidate ID is invalid
*/
static validateId(id) {
const allowed = /^[A-Za-z0-9-_]+$/;
if ( !allowed.test(id) ) {
throw new Error("Package and compendium pack IDs may only be alphanumeric with hyphens or underscores.");
}
const prohibited = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
if ( prohibited.test(id) ) throw new Error(`The ID "${id}" uses an operating system prohibited value.`);
}
/* -------------------------------------------- */
/**
* Validate a single compendium pack object
* @param {PackageCompendiumData} packData Candidate compendium packs data
* @throws An error if the data is invalid
*/
static #validatePack(packData) {
if ( SYSTEM_SPECIFIC_COMPENDIUM_TYPES.includes(packData.type) && !packData.system ) {
throw new Error(`The Compendium pack "${packData.name}" of the "${packData.type}" type must declare the "system"`
+ " upon which it depends.");
}
}
/* -------------------------------------------- */
/**
* A wrapper around the default compatibility warning logger which handles some package-specific interactions.
* @param {string} packageId The package ID being logged
* @param {string} message The warning or error being logged
* @param {object} options Logging options passed to foundry.utils.logCompatibilityWarning
* @param {object} [options.installed] Is the package installed?
* @internal
*/
static _logWarning(packageId, message, { installed, ...options }={}) {
logCompatibilityWarning(message, options);
if ( installed ) globalThis.packages?.warnings?.add(packageId, {type: this.type, level: "warning", message});
}
/* -------------------------------------------- */
/**
* A set of package manifest keys that are migrated.
* @type {Set<string>}
*/
static migratedKeys = new Set([
/** @deprecated since 10 until 13 */
"name", "dependencies", "minimumCoreVersion", "compatibleCoreVersion"
]);
/* -------------------------------------------- */
/** @inheritdoc */
static migrateData(data, { installed }={}) {
this._migrateNameToId(data, {since: 10, until: 13, stack: false, installed});
this._migrateDependenciesNameToId(data, {since: 10, until: 13, stack: false, installed});
this._migrateToRelationships(data, {since: 10, until: 13, stack: false, installed});
this._migrateCompatibility(data, {since: 10, until: 13, stack: false, installed});
this._migrateMediaURL(data, {since: 11, until: 13, stack: false, installed});
this._migrateOwnership(data, {since: 11, until: 13, stack: false, installed});
this._migratePackIDs(data, {since: 12, until: 14, stack: false, installed});
this._migratePackEntityToType(data, {since: 9, stack: false, installed});
return super.migrateData(data);
}
/* -------------------------------------------- */
/** @internal */
static _migrateNameToId(data, logOptions) {
if ( data.name && !data.id ) {
data.id = data.name;
delete data.name;
if ( this.type !== "world" ) {
const warning = `The ${this.type} "${data.id}" is using "name" which is deprecated in favor of "id"`;
this._logWarning(data.id, warning, logOptions);
}
}
}
/* -------------------------------------------- */
/** @internal */
static _migrateDependenciesNameToId(data, logOptions) {
if ( data.relationships ) return;
if ( data.dependencies ) {
let hasDependencyName = false;
for ( const dependency of data.dependencies ) {
if ( dependency.name && !dependency.id ) {
hasDependencyName = true;
dependency.id = dependency.name;
delete dependency.name;
}
}
if ( hasDependencyName ) {
const msg = `The ${this.type} "${data.id}" contains dependencies using "name" which is deprecated in favor of "id"`;
this._logWarning(data.id, msg, logOptions);
}
}
}
/* -------------------------------------------- */
/** @internal */
static _migrateToRelationships(data, logOptions) {
if ( data.relationships ) return;
data.relationships = {
requires: [],
systems: []
};
// Dependencies -> Relationships.Requires
if ( data.dependencies ) {
for ( const d of data.dependencies ) {
const relationship = {
"id": d.id,
"type": d.type,
"manifest": d.manifest,
"compatibility": {
"compatible": d.version
}
};
d.type === "system" ? data.relationships.systems.push(relationship) : data.relationships.requires.push(relationship);
}
const msg = `The ${this.type} "${data.id}" contains "dependencies" which is deprecated in favor of "relationships.requires"`;
this._logWarning(data.id, msg, logOptions);
delete data.dependencies;
}
// V9: system -> relationships.systems
else if ( data.system && (this.type === "module") ) {
data.system = data.system instanceof Array ? data.system : [data.system];
const newSystems = data.system.map(id => ({id})).filter(s => !data.relationships.systems.find(x => x.id === s.id));
data.relationships.systems = data.relationships.systems.concat(newSystems);
const msg = `${this.type} "${data.id}" contains "system" which is deprecated in favor of "relationships.systems"`;
this._logWarning(data.id, msg, logOptions);
delete data.system;
}
}
/* -------------------------------------------- */
/** @internal */
static _migrateCompatibility(data, logOptions) {
if ( !data.compatibility && (data.minimumCoreVersion || data.compatibleCoreVersion) ) {
this._logWarning(data.id, `The ${this.type} "${data.id}" is using the old flat core compatibility fields which `
+ `are deprecated in favor of the new "compatibility" object`,
logOptions);
data.compatibility = {
minimum: data.minimumCoreVersion,
verified: data.compatibleCoreVersion
};
delete data.minimumCoreVersion;
delete data.compatibleCoreVersion;
}
}
/* -------------------------------------------- */
/** @internal */
static _migrateMediaURL(data, logOptions) {
if ( !data.media ) return;
let hasMediaLink = false;
for ( const media of data.media ) {
if ( "link" in media ) {
hasMediaLink = true;
media.url = media.link;
delete media.link;
}
}
if ( hasMediaLink ) {
const msg = `${this.type} "${data.id}" declares media.link which is unsupported, media.url should be used`;
this._logWarning(data.id, msg, logOptions);
}
}
/* -------------------------------------------- */
/** @internal */
static _migrateOwnership(data, logOptions) {
if ( !data.packs ) return;
let hasPrivatePack = false;
for ( const pack of data.packs ) {
if ( pack.private && !("ownership" in pack) ) {
pack.ownership = {PLAYER: "LIMITED", ASSISTANT: "OWNER"};
hasPrivatePack = true;
}
delete pack.private;
}
if ( hasPrivatePack ) {
const msg = `${this.type} "${data.id}" uses pack.private which has been replaced with pack.ownership`;
this._logWarning(data.id, msg, logOptions);
}
return data;
}
/* -------------------------------------------- */
/** @internal */
static _migratePackIDs(data, logOptions) {
if ( !data.packs ) return;
for ( const pack of data.packs ) {
const slugified = pack.name.replace(/[^A-Za-z0-9-_]/g, "");
if ( pack.name !== slugified ) {
const msg = `The ${this.type} "${data.id}" contains a pack with an invalid name "${pack.name}". `
+ "Pack names containing any character that is non-alphanumeric or an underscore will cease loading in "
+ "version 14 of the software.";
pack.name = slugified;
this._logWarning(data.id, msg, logOptions);
}
}
}
/* -------------------------------------------- */
/** @internal */
static _migratePackEntityToType(data, logOptions) {
if ( !data.packs ) return;
let hasPackEntity = false;
for ( const pack of data.packs ) {
if ( ("entity" in pack) && !("type" in pack) ) {
pack.type = pack.entity;
hasPackEntity = true;
}
delete pack.entity;
}
if ( hasPackEntity ) {
const msg = `${this.type} "${data.id}" uses pack.entity which has been replaced with pack.type`;
this._logWarning(data.id, msg, logOptions);
}
}
/* -------------------------------------------- */
/**
* Retrieve the latest Package manifest from a provided remote location.
* @param {string} manifestUrl A remote manifest URL to load
* @param {object} options Additional options which affect package construction
* @param {boolean} [options.strict=true] Whether to construct the remote package strictly
* @return {Promise<ServerPackage>} A Promise which resolves to a constructed ServerPackage instance
* @throws An error if the retrieved manifest data is invalid
*/
static async fromRemoteManifest(manifestUrl, {strict=true}={}) {
throw new Error("Not implemented");
}
}