1
0
Fork 0

Merge pull request #3564 from eriese/eriese-progressive-parts

feature (Pattern Config Resolution) Resolve Part Configurations Progressively
This commit is contained in:
Joost De Cock 2023-03-05 14:07:50 +01:00 committed by GitHub
commit 31873b5f96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1248 additions and 623 deletions

View file

@ -53,7 +53,7 @@ charlie:
core:
_:
'bezier-js': '6.1.0'
'bin-pack': '1.0.2'
'bin-pack-with-constraints': '1.0.1'
'hooks': '0.3.2'
'lodash.get': &_get '4.4.2'
'lodash.set': &_set '4.3.2'
@ -62,6 +62,9 @@ core:
dev:
'eslint': &eslint '8.34.0'
'nyc': '15.1.0'
'mocha': *mocha
'chai': *chai
'sinon': &sinon '^15.0.1'
diana:
peer:
'@freesewing/brian': *freesewing

View file

@ -51,7 +51,7 @@
"peerDependencies": {},
"dependencies": {
"bezier-js": "6.1.0",
"bin-pack": "1.0.2",
"bin-pack-with-constraints": "1.0.1",
"hooks": "0.3.2",
"lodash.get": "4.4.2",
"lodash.set": "4.3.2",
@ -60,7 +60,10 @@
},
"devDependencies": {
"eslint": "8.34.0",
"nyc": "15.1.0"
"nyc": "15.1.0",
"mocha": "10.0.0",
"chai": "4.2.0",
"sinon": "^15.0.1"
},
"files": [
"dist/*",

View file

@ -0,0 +1,496 @@
import { __addNonEnumProp } from './utils.mjs'
/**
* Get the name of the given plugin config
*
* @param {(Object|Object[])} plugin the plugin to get the name of
* @return {(string|false)} the name, or false if there isn't one
*/
export function getPluginName(plugin) {
const toCheck = Array.isArray(plugin) ? plugin[0] : plugin
return toCheck.name || toCheck.plugin?.name || false
}
/////////////////
// CONSTRUCTOR //
/////////////////
/**
* A class for handling config resolution for a Pattern
* @class
* @param {Pattern} pattern the pattern whose config is being handled
*/
export function PatternConfig(pattern) {
/** @type {Store} the pattern's store, for logging */
this.store = pattern.store
/** @type {Object} resolved plugins keyed by name */
this.plugins = { ...(pattern.designConfig.plugins || {}) }
/** @type {Object} resolved options keyed by name */
this.options = { ...(pattern.designConfig.options || {}) }
/** @type {string[]} required measurements */
this.measurements = [...(pattern.designConfig.measurements || [])]
/** @type {string[]} optional measurements */
this.optionalMeasurements = [...(pattern.designConfig.optionalMeasurements || [])]
/** @type {Object} the names of the parts that will be injected */
this.inject = {}
/** @type {Object} arrays of parts that are direct dependencies of the key */
this.directDependencies = {}
/** @type {Object} arrays of all dependencies of the key */
this.resolvedDependencies = {}
/** @type {Object} parts to include in the pattern */
this.parts = {}
/** @type {Object} which parts are hidden */
this.partHide = {}
/** @type {Object} to track when to overwrite options */
__addNonEnumProp(this, '__mutated', {
optionDistance: {},
partDistance: {},
})
/** @type {Object} tracking for dependency hiding */
__addNonEnumProp(this, '__hiding', {
all: {},
deps: {},
})
}
/** @type {Boolean} change me to true to get full debugging of the resolution process */
const DISTANCE_DEBUG = false
////////////////////
// PUBLIC METHODs //
////////////////////
/**
* Validate that a part meets the requirements to be added to the pattern
* @param {Object} part a part configuration
* @return {boolean} whether the part is valid
*/
PatternConfig.prototype.isPartValid = function (part) {
if (typeof part?.draft !== 'function') {
this.store.log.error(`Part must have a draft() method`)
return false
}
if (!part.name) {
this.store.log.error(`Part must have a name`)
return false
}
return true
}
/**
* Chainable method to add a part to the configuration
* @param {Object} part
*/
PatternConfig.prototype.addPart = function (part) {
if (this.isPartValid(part)) this.__addPart([part])
return this
}
/** Log the final report on part inheritance order */
PatternConfig.prototype.logPartDistances = function () {
for (const partName in this.parts) {
let qualifier = DISTANCE_DEBUG ? 'final' : ''
this.store.log.debug(
`⚪️ \`${partName}\` ${qualifier} options priority is __${this.__mutated.partDistance[partName]}__`
)
}
}
/**
* Return a configuration in the structure expected by the pattern
* @return {Object} contains parts, plugins, measurements, options, optionalMeasurements, resolvedDependencies, directDependencies, inject, draftOrder, partHide
*/
PatternConfig.prototype.asConfig = function () {
return {
parts: this.parts,
plugins: this.plugins,
measurements: this.measurements,
options: this.options,
optionalMeasurements: this.optionalMeasurements,
resolvedDependencies: this.resolvedDependencies,
directDependencies: this.directDependencies,
inject: this.inject,
draftOrder: this.__resolveDraftOrder(),
partHide: this.partHide,
}
}
/////////////////////
// PRIVATE METHODS //
/////////////////////
/**
* Add a part's configuration
* Uses recursion to also add that part's dependencies
* @private
* @param {Object[]} depChain an array starting with the current part to add and containing its dependents/descendents in order
*/
PatternConfig.prototype.__addPart = function (depChain) {
// the current part is the head of the chain
const part = depChain[0]
// the longer the chain, the deeper the part is down it
const distance = depChain.length
if (!this.parts[part.name]) this.parts[part.name] = Object.freeze(part)
else return
// if it hasn't been registered with a distance, do that now
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
this.__mutated.partDistance[part.name] = distance
if (DISTANCE_DEBUG)
this.store.log.debug(
`Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__`
)
}
// Handle various hiding possibilities
if (part.hide || part.hideAll) this.partHide[part.name] = true
if (part.hideDependencies) this.__hiding.deps[part.name] = true
if (part.hideAll) {
this.__hiding.all[part.name] = true
}
// resolve its dependencies
this.__resolvePartDependencies(depChain)
// add the part's config
this.__addPartConfig(part)
}
/**
* Resolves/Adds a part's design configuration to the pattern config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return this
*/
PatternConfig.prototype.__addPartConfig = function (part) {
return this.__addPartOptions(part) // add options
.__addPartMeasurements(part, false) // add required measurements
.__addPartMeasurements(part, true) // add optional measurements
.__addPartPlugins(part) // add plugins
}
/**
* Resolves/Adds a part's configured options to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {PatternConfig} this - The PatternConfig instance
*/
PatternConfig.prototype.__addPartOptions = function (part) {
// skip empty options
if (!part.options) return this
// get the part's option priority
const partDistance = this.__mutated.partDistance?.[part.name] || 0
// loop through options
for (const optionName in part.options) {
const option = part.options[optionName]
// get the priority of this option's current registration
const optionDistance = this.__mutated.optionDistance[optionName]
// debug the comparison
if (optionDistance && DISTANCE_DEBUG)
this.store.log.debug(
`optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__`
)
// if it's never been registered, or it's registered at a further distance
if (!optionDistance || optionDistance > partDistance) {
// Keep options immutable in the pattern or risk subtle bugs
this.options[optionName] = Object.freeze(option)
// register the new distance
this.__mutated.optionDistance[optionName] = partDistance
// debug appropriately
this.store.log.debug(
optionDistance
? `🟣 __${optionName}__ option overwritten by \`${part.name}\``
: `🔵 __${optionName}__ option loaded from part \`${part.name}\``
)
}
}
return this
}
/**
* Resolves/Adds a part's configured measurements to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {boolean} optional - are these measurements optional?
* @return {PatternConfig} this - The PatternConfig instance
*/
PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) {
// which list are we drawing from?
const listType = optional ? 'optionalMeasurements' : 'measurements'
// if the part has measurements of this type, go through them
if (part[listType]) {
part[listType].forEach((m) => {
// we need to know what lists it's already present on
const isInReqList = this.measurements.indexOf(m) !== -1
// if it's already registered as required, we're done here
if (isInReqList) return
// check if it's registered as optional
const optInd = this.optionalMeasurements.indexOf(m)
const isInOptList = optInd !== -1
// if it is optional and not in the list, push it
if (optional && !isInOptList) this.optionalMeasurements.push(m)
// if it's not optional
if (!optional) {
// push it to required list
this.measurements.push(m)
// make sure it's not also registered as optional
if (isInOptList) this.optionalMeasurements.splice(optInd, 1)
}
this.store.log.debug(
`🟠 __${m}__ measurement is ${optional ? 'optional' : 'required'} in \`${part.name}\``
)
})
}
return this
}
/**
* Resolves/Adds a part's configured plugins to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {PatternConfig} this - The PatternConfig instance
*/
PatternConfig.prototype.__addPartPlugins = function (part) {
if (!part.plugins) return this
const plugins = this.plugins
// Side-step immutability of the part object to ensure plugins is an array
let partPlugins = part.plugins
if (!Array.isArray(partPlugins)) partPlugins = [partPlugins]
// Go through list of part plugins
for (let plugin of partPlugins) {
const name = getPluginName(plugin)
this.store.log.debug(
plugin.plugin
? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\``
: `🔌 Resolved __${name}__ plugin in \`${part.name}\``
)
// Handle [plugin, data] scenario
if (Array.isArray(plugin)) {
const pluginObj = { ...plugin[0], data: plugin[1] }
plugin = pluginObj
}
if (!plugins[name]) {
// New plugin, so we load it
plugins[name] = plugin
this.store.log.info(
plugin.condition
? `New plugin conditionally added: \`${name}\``
: `New plugin added: \`${name}\``
)
} else {
// Existing plugin, takes some more work
if (plugin.plugin && plugin.condition) {
// Multiple instances of the same plugin with different conditions
// will all be added, so we need to change the name.
if (plugins[name]?.condition) {
plugins[name + '_'] = plugin
this.store.log.info(
`Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.`
)
} else
this.store.log.info(
`Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.`
)
}
// swap from a conditional if needed
else if (plugins[name].condition) {
plugins[name] = plugin
this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`)
}
}
}
return this
}
// the two types of dependencies
const depTypes = ['from', 'after']
/**
* Recursively register part dependencies
* triggers {@link __addPart} on new parts found during resolution
* @param {Object[]} depChain an array starting with the current part to register and containing its dependents/descendents in order
* @return {PatternConfig} this
* @private
*/
PatternConfig.prototype.__resolvePartDependencies = function (depChain) {
// the current part is the head of the chain
const part = depChain[0]
// get or make its array of resolved dependencies
this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || []
// for each dependency type (from, after)
depTypes.forEach((d) => {
// if the part has dependencies of that type
if (part[d]) {
if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`)
// enforce an array
const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]]
// each dependency
depsOfType.forEach((dot) => {
// add it as a direct dependency of the current part
this.__addDependency('directDependencies', part.name, dot.name)
// add it as a resolved dependency of all parts in the chain
depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, dot.name))
// handle hiding and injecting
this.__handlePartDependencyOfType(part, dot.name, d)
// if the dependency isn't registered, register it
if (!this.parts[dot.name]) {
// add the part's configuration. this will recursively add the part's dependencies to all parts in the chain
this.__addPart([dot, ...depChain])
} else {
// if it's already registered, recursion won't happen, but we still need to add its resolved dependencies to all parts in the chain
this.resolvedDependencies[dot.name].forEach((r) => {
depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, r))
})
}
})
}
})
// now that the chain has been registered, recalculate the part distances
this.__resolveMutatedPartDistance(part.name)
}
/**
* Adds a part as either a direct or a resolved dependency
* @param {string} dependencyList which list to add the part to, 'resolvedDependencies' or 'directDependencies'
* @param {string} partName the name of the part to add the dependency to in the list
* @param {string} depName the name of the dependency to add to the list
* @private
*/
PatternConfig.prototype.__addDependency = function (dependencyList, partName, depName) {
this[dependencyList][partName] = this[dependencyList][partName] || []
if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG)
this.store.log.debug(`add ${depName} to ${partName} dependencyResolution`)
if (this[dependencyList][partName].indexOf(depName) === -1)
this[dependencyList][partName].push(depName)
}
/**
* Handle dependency-type specific config business
* @param {Object} part the part to add the dependency to
* @param {string} depName the name of the dependency to add
* @param {string} depType the type of dependency, 'from' or 'after'
* @private
*/
PatternConfig.prototype.__handlePartDependencyOfType = function (part, depName, depType) {
switch (depType) {
case 'from':
this.__setFromHide(part, depName)
this.inject[part.name] = depName
break
case 'after':
this.__setAfterHide(part, depName)
}
}
/**
* Resolve part option priority
* Recursively bumps priorities down the dependency chain
* @param {string} partName the name of the part to resolve
* @private
*/
PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) {
// if the part has no dependencies, bail
if (!this.directDependencies[partName]) return
// propose that each of the part's direct dependencies should be at a distance 1 further than the part's distance
const proposed_dependency_distance = this.__mutated.partDistance[partName] + 1
// check each direct dependency
this.directDependencies[partName].forEach((dependency) => {
// if the dependency doesn't have a distance, or that distance is less than the proposal
if (
typeof this.__mutated.partDistance[dependency] === 'undefined' ||
this.__mutated.partDistance[dependency] < proposed_dependency_distance
) {
// set the new distance
this.__mutated.partDistance[dependency] = proposed_dependency_distance
// bump the dependency's dependencies as well
this.__resolveMutatedPartDistance(dependency)
}
if (DISTANCE_DEBUG)
this.store.log.debug(
`partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__`
)
})
}
/**
* Resolves the draft order based on the configuation
*
* @private
* @param {object} graph - The object of resolved dependencies, used to call itself recursively
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__resolveDraftOrder = function () {
this.__draftOrder = Object.keys(this.parts).sort(
(p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1]
)
return this.__draftOrder
}
/**
* Sets visibility of a 'from' dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} depName - The name of the dependency
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__setFromHide = function (part, depName) {
if (this.__hiding.deps[part.name]) {
this.partHide[depName] = true
this.__hiding.deps[depName] = true
}
if (this.__hiding.all[part.name]) {
this.partHide[depName] = true
this.__hiding.all[depName] = true
}
return this
}
/**
* Sets visibility of an 'after' dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__setAfterHide = function (part, depName) {
if (this.__hiding.all[part.name]) {
this.partHide[depName] = true
this.__hiding.all[depName] = true
}
return this
}

View file

@ -0,0 +1,77 @@
/**
* A queue for handling the draft order of pattern parts
* Unlike most queues, traversing this queue is non-destructive
* so that the queue can be traversed many times.
* The goal is to allow the queue to be manipulated while being traversed
* @class
* @param {Pattern} pattern the pattern that will use the queue
*/
export function PatternDraftQueue(pattern) {
// save the config resolver
this.__configResolver = pattern.__configResolver
// get the draft order in its current state
this.queue = this.__resolveDraftOrder()
// start at 0
this.start()
}
/** Go back to the beginning of the queue */
PatternDraftQueue.prototype.start = function () {
this.queueIndex = 0
}
/**
* Add a part to end of the queue. Useful for queueing up parts a draft time
* @param {string} partName the name to the part to add
*/
PatternDraftQueue.prototype.addPart = function (partName) {
if (!this.contains(partName)) this.queue.push(partName)
return this
}
/**
* Check whether the queue has a next part without moving the queue index
* @return {Boolean} whether there is a next part in the queue
*/
PatternDraftQueue.prototype.hasNext = function () {
return this.queueIndex < this.queue.length
}
/**
* Get the next part in the queue without moving the queue index
* @return {string} the next part in the queue
*/
PatternDraftQueue.prototype.peek = function () {
return this.queue[this.queueIndex]
}
/**
* Get the next part in the queue and move the queue index
* @return {string} the next part in the queue
*/
PatternDraftQueue.prototype.next = function () {
const next = this.peek()
this.queueIndex++
return next
}
/**
* Check whether a part is already queued
* @param {string} partName the name of the part
* @return {boolean} whether the part is in the queue
*/
PatternDraftQueue.prototype.contains = function (partName) {
return this.queue.indexOf(partName) !== -1
}
/**
* Resolves the draft order based on the configuation
* @private
* @return A list of parts in the order they should be drafted
*/
PatternDraftQueue.prototype.__resolveDraftOrder = function () {
const partDistances = this.__configResolver.__mutated.partDistance
return Object.keys(this.__configResolver.parts).sort(
(p1, p2) => partDistances[p2] - partDistances[p1]
)
}

View file

@ -1,5 +1,5 @@
import { Attributes } from './attributes.mjs'
import pack from 'bin-pack'
import pack from 'bin-pack-with-constraints'
import { __addNonEnumProp, __macroName } from './utils.mjs'
import { Part } from './part.mjs'
import { Stack } from './stack.mjs'
@ -11,10 +11,10 @@ import { Store } from './store.mjs'
import { Hooks } from './hooks.mjs'
import { version } from '../data.mjs'
import { __loadPatternDefaults } from './config.mjs'
import { PatternConfig, getPluginName } from './pattern-config.mjs'
import { PatternDraftQueue } from './pattern-draft-queue.mjs'
import cloneDeep from 'lodash.clonedeep'
const DISTANCE_DEBUG = false
//////////////////////////////////////////////
// CONSTRUCTOR //
//////////////////////////////////////////////
@ -26,7 +26,13 @@ const DISTANCE_DEBUG = false
* @param {object} config - The Design config
* @return {object} this - The Pattern instance
*/
export function Pattern(designConfig) {
export function Pattern(designConfig = {}) {
// Enumerable properties
this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved)
this.config = {} // Will hold the resolved pattern after calling __init()
this.store = new Store() // Pattern-wide store
this.setStores = [] // Per-set stores
// Non-enumerable properties
__addNonEnumProp(this, 'plugins', {})
__addNonEnumProp(this, 'width', 0)
@ -40,26 +46,10 @@ export function Pattern(designConfig) {
__addNonEnumProp(this, 'Attributes', Attributes)
__addNonEnumProp(this, 'macros', {})
__addNonEnumProp(this, '__initialized', false)
__addNonEnumProp(this, '__designParts', {})
__addNonEnumProp(this, '__inject', {})
__addNonEnumProp(this, '__dependencies', {})
__addNonEnumProp(this, '__resolvedDependencies', {})
__addNonEnumProp(this, '__resolvedParts', [])
__addNonEnumProp(this, 'config.parts', {})
__addNonEnumProp(this, 'config.resolvedDependencies', {})
__addNonEnumProp(this, '__storeMethods', new Set())
__addNonEnumProp(this, '__mutated', {
optionDistance: {},
partDistance: {},
partHide: {},
partHideAll: {},
})
__addNonEnumProp(this, '__draftOrder', [])
__addNonEnumProp(this, '__hide', {})
// Enumerable properties
this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved)
this.config = {} // Will hold the resolved pattern after calling __init()
this.store = new Store() // Pattern-wide store
this.setStores = [] // Per-set stores
__addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding
return this
}
@ -69,19 +59,26 @@ export function Pattern(designConfig) {
//////////////////////////////////////////////
/**
* FIXME: Allows adding parts to the config at runtime
* Allows adding parts to the config at runtime
*
* @param {object} part - The part to add
* @param {boolean} resolveImmediately - Should the part be resolved now, or wait until the next call to {@link __init()}?
* It is useful to resolve immediately if one part is being added at runtime
* It might be useful to not resolve immediately if a number of parts will be added over multiple calls
* @return {object} this - The Pattern instance
*/
Pattern.prototype.addPart = function (part) {
if (typeof part?.draft === 'function') {
if (part.name) {
this.designConfig.parts.push(part)
this.__initialized = false
} else this.store.log.error(`Part must have a name`)
} else this.store.log.error(`Part must have a draft() method`)
Pattern.prototype.addPart = function (part, resolveImmediately = true) {
if (
this.__configResolver.isPartValid(part) &&
!this.designConfig.parts.find((p) => p.name == part.name)
) {
this.store.log.debug(`Adding Part \`${part.name}\` at runtime`)
this.designConfig.parts.push(part)
if (resolveImmediately) {
if (this.__configResolver.addPart(part) && typeof this.draftQueue !== 'undefined')
this.draftQueue.addPart(part.name)
} else this.__initialized = false
}
return this
}
@ -92,6 +89,7 @@ Pattern.prototype.addPart = function (part) {
*/
Pattern.prototype.draft = function () {
this.__init()
this.draftQueue = new PatternDraftQueue(this)
this.__runHooks('preDraft')
// Keep container for drafted parts fresh
this.parts = []
@ -110,8 +108,9 @@ Pattern.prototype.draft = function () {
// Handle snap for pct options
this.__loadAbsoluteOptionsSet(set)
for (const partName of this.config.draftOrder) {
this.createPartForSet(partName, set)
this.draftQueue.start()
while (this.draftQueue.hasNext()) {
this.createPartForSet(this.draftQueue.next(), set)
}
this.__runHooks('postSetDraft')
}
@ -130,15 +129,15 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) {
this.parts[set][partName] = this.__createPartWithContext(partName, set)
// Handle inject/inheritance
if (typeof this.__inject[partName] === 'string') {
if (typeof this.config.inject[partName] === 'string') {
this.setStores[set].log.debug(
`Creating part \`${partName}\` from part \`${this.__inject[partName]}\``
`Creating part \`${partName}\` from part \`${this.config.inject[partName]}\``
)
try {
this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]])
this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]])
} catch (err) {
this.setStores[set].log.error([
`Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``,
`Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``,
err,
])
}
@ -159,11 +158,11 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) {
}
Pattern.prototype.draftPartForSet = function (partName, set) {
if (typeof this.__designParts?.[partName]?.draft === 'function') {
if (typeof this.config.parts?.[partName]?.draft === 'function') {
this.activePart = partName
try {
this.__runHooks('prePartDraft')
const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand())
const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand())
this.__runHooks('postPartDraft')
if (typeof result === 'undefined') {
this.setStores[set].log.error(
@ -359,227 +358,6 @@ Pattern.prototype.use = function (plugin, data) {
// PRIVATE METHODS //
//////////////////////////////////////////////
/**
* Adds a part as a simple dependency
*
* @private
* @param {string} name - The name of the dependency
* @param {object} part - The part configuration
* @param {object} dep - The dependency configuration
* @return {object} this - The Pattern instance
*/
Pattern.prototype.__addDependency = function (name, part, dep) {
this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name])
if (typeof this.__designParts[dep.name] === 'undefined') {
this.config = this.__addPartConfig(this.__designParts[dep.name])
}
return this
}
/**
* Resolves/Adds a part's design configuration to the pattern config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {onject} config - The global config
* @param {Store} store - The store, used for logging
* @return {object} config - The mutated global config
*/
Pattern.prototype.__addPartConfig = function (part) {
if (this.__resolvedParts.includes(part.name)) return this
// Add parts, using set to keep them unique in the array
this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)]
return this.__addPartOptions(part)
.__addPartMeasurements(part)
.__addPartOptionalMeasurements(part)
.__addPartPlugins(part)
}
/**
* Resolves/Adds a part's configured measurements to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {array} list - The list of resolved measurements
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartMeasurements = function (part, list = false) {
if (!this.config.measurements) this.config.measurements = []
if (!list) list = this.config.measurements
if (part.measurements) {
for (const m of part.measurements) {
if (list.indexOf(m) === -1) {
list.push(m)
this.store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``)
}
}
}
if (part.from) this.__addPartMeasurements(part.from, list)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) this.__addPartMeasurements(dep, list)
} else this.__addPartMeasurements(part.after, list)
}
// Weed out duplicates
this.config.measurements = [...new Set(list)]
return this
}
/**
* Resolves/Adds a part's configured optional measurements to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {array} list - The list of resolved optional measurements
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) {
if (!this.config.optionalMeasurements) this.config.optionalMeasurements = []
if (!list) list = this.config.optionalMeasurements
if (part.optionalMeasurements) {
for (const m of part.optionalMeasurements) {
// Don't add it's a required measurement for another part
if (this.config.measurements.indexOf(m) === -1) {
if (list.indexOf(m) === -1) {
list.push(m)
this.store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``)
}
}
}
}
if (part.from) this.__addPartOptionalMeasurements(part.from, list)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) this.__addPartOptionalMeasurements(dep, list)
} else this.__addPartOptionalMeasurements(part.after, list)
}
// Weed out duplicates
if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)]
return this
}
/**
* Resolves/Adds a part's configured options to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartOptions = function (part) {
if (!this.config.options) this.config.options = {}
if (part.options) {
for (const optionName in part.options) {
if (!this.__mutated.optionDistance[optionName]) {
this.__mutated.optionDistance[optionName] = this.__mutated.partDistance?.[part.name] || 0
// Keep design parts immutable in the pattern or risk subtle bugs
this.config.options[optionName] = Object.freeze(part.options[optionName])
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
} else {
if (DISTANCE_DEBUG)
this.store.log.debug(
'optionDistance for ' +
optionName +
' is ' +
this.__mutated.optionDistance[optionName] +
', and partDistance for ' +
part.name +
' is ' +
this.__mutated.partDistance[part.name]
)
if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) {
this.config.options[optionName] = part.options[optionName]
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
}
}
}
}
if (part.from) this.__addPartOptions(part.from)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) this.__addPartOptions(dep)
} else this.__addPartOptions(part.after)
}
return this
}
function getPluginName(plugin) {
if (Array.isArray(plugin)) {
if (plugin[0].name) return plugin[0].name
if (plugin[0].plugin.name) return plugin[0].plugin.name
} else {
if (plugin.name) return plugin.name
if (plugin.plugin?.name) return plugin.plugin.name
}
return false
}
/**
* Resolves/Adds a part's configured plugins to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartPlugins = function (part) {
if (!part.plugins) return this
if (!this.config.plugins) this.config.plugins = {}
const plugins = { ...this.config.plugins }
// Side-step immutability of the part object to ensure plugins is an array
let partPlugins = part.plugins
if (!Array.isArray(partPlugins)) partPlugins = [partPlugins]
// Go through list of part plugins
for (let plugin of partPlugins) {
const name = getPluginName(plugin)
this.store.log.debug(
plugin.plugin
? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\``
: `🔌 Resolved __${name}__ plugin in \`${part.name}\``
)
// Handle [plugin, data] scenario
if (Array.isArray(plugin)) {
const pluginObj = { ...plugin[0], data: plugin[1] }
plugin = pluginObj
}
if (!plugins[name]) {
// New plugin, so we load it
plugins[name] = plugin
this.store.log.info(
plugin.condition
? `New plugin conditionally added: \`${name}\``
: `New plugin added: \`${name}\``
)
} else {
// Existing plugin, takes some more work
if (plugin.plugin && plugin.condition) {
// Multiple instances of the same plugin with different conditions
// will all be added, so we need to change the name.
if (plugins[name]?.condition) {
plugins[name + '_'] = plugin
this.store.log.info(
`Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.`
)
} else
this.store.log.info(
`Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.`
)
}
}
}
this.config.plugins = { ...plugins }
return this
}
/**
* Creates a store for a set (of settings)
*
@ -636,7 +414,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) {
const part = new Part()
part.name = name
part.set = set
part.stack = this.__designParts[name]?.stack || name
part.stack = this.config.parts[name]?.stack || name
part.context = {
parts: this.parts[set],
config: this.config,
@ -676,24 +454,6 @@ Pattern.prototype.__createStackWithContext = function (name) {
return stack
}
/**
* Filter optional measurements out id they are also required measurments
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__filterOptionalMeasurements = function () {
if (!this.config.optionalMeasurements) {
this.config.optionalMeasurements = []
return this
}
this.config.optionalMeasurements = this.config.optionalMeasurements.filter(
(m) => this.config.measurements.indexOf(m) === -1
)
return this
}
/**
* Initializes the pattern coniguration and settings
*
@ -716,14 +476,12 @@ Pattern.prototype.__init = function () {
* This methods does that, and resolves the design config + user settings
*/
this.__resolveParts() // Resolves parts
.__resolveDependencies() // Resolves dependencies
.__resolveDraftOrder() // Resolves draft order
.__resolveConfig() // Gets the config from the resolver
.__loadOptionDefaults() // Merges default options with user provided ones
.__loadPlugins() // Loads plugins
.__loadConfigData() // Makes config data available in store
.__filterOptionalMeasurements() // Removes required m's from optional list
.__loadOptionDefaults() // Merges default options with user provided ones
this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`)
this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`)
this.__runHooks('postInit')
this.__initialized = true
@ -739,16 +497,13 @@ Pattern.prototype.__init = function () {
* @return {bool} hidden - true if the part is hidden, or false if not
*/
Pattern.prototype.__isPartHidden = function (partName) {
const partHidden = this.parts?.[this.activeSet]?.[partName]?.hidden || false
if (Array.isArray(this.settings[this.activeSet || 0].only)) {
if (this.settings[this.activeSet || 0].only.includes(partName)) return false
if (this.settings[this.activeSet || 0].only.includes(partName)) return partHidden
}
if (this.__designParts?.[partName]?.hide) return true
if (this.__designParts?.[partName]?.hideAll) return true
if (this.__mutated.partHide?.[partName]) return true
if (this.__mutated.partHideAll?.[partName]) return true
if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true
if (this.config.partHide?.[partName]) return true
return false
return partHidden
}
/**
@ -761,21 +516,11 @@ Pattern.prototype.__isPartHidden = function (partName) {
Pattern.prototype.__isStackHidden = function (stackName) {
if (!this.stacks[stackName]) return true
const parts = this.stacks[stackName].getPartNames()
if (Array.isArray(this.settings[this.activeStack || 0].only)) {
for (const partName of parts) {
if (this.settings[this.activeStack || 0].only.includes(partName)) return false
}
return true
}
for (const partName of parts) {
if (this.__designParts?.[partName]?.hide) return true
if (this.__designParts?.[partName]?.hideAll) return true
if (this.__mutated.partHide?.[partName]) return true
if (this.__mutated.partHideAll?.[partName]) return true
if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true
if (!this.__isPartHidden(partName)) return false
}
return false
return true
}
/**
@ -1075,11 +820,7 @@ Pattern.prototype.__needs = function (partName, set = 0) {
// Walk the only parts, checking each one for a match in its dependencies
for (const part of only) {
if (part === partName) return true
if (this.__resolvedDependencies[part]) {
for (const dependency of this.__resolvedDependencies[part]) {
if (dependency === partName) return true
}
}
if (this.config.resolvedDependencies[part]?.indexOf(partName) !== -1) return true
}
return false
@ -1179,7 +920,8 @@ Pattern.prototype.__pack = function () {
}
}
if (this.settings[0].layout === true) {
let size = pack(bins, { inPlace: true })
// some plugins will add a width constraint to the settings, but we can safely pass undefined if not
let size = pack(bins, { inPlace: true, maxWidth: this.settings[0].maxWidth })
for (let bin of bins) {
this.autoLayout.stacks[bin.id] = { move: {} }
let stack = this.stacks[bin.id]
@ -1210,65 +952,12 @@ Pattern.prototype.__pack = function () {
}
/**
* Recursively solves part dependencies for a part
*
* Gets the configuration for the config resolver and sets it on the pattern
* @private
* @param {object} seen - Object to keep track of seen dependencies
* @param {string} part - Name of the part
* @param {object} graph - Dependency graph, used to call itself recursively
* @param {array} deps - List of dependencies
* @return {Array} deps - The list of dependencies
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveDependency = function (
seen,
part,
graph = this.dependencies,
deps = []
) {
if (typeof seen[part] === 'undefined') seen[part] = true
if (typeof graph[part] === 'string') graph[part] = [graph[part]]
if (Array.isArray(graph[part])) {
if (graph[part].length === 0) return []
else {
if (deps.indexOf(graph[part]) === -1) deps.push(...graph[part])
for (let apart of graph[part]) deps.concat(this.__resolveDependency(seen, apart, graph, deps))
}
}
return deps
}
/**
* Resolves the draft order based on the configuation
*
* @private
* @param {object} graph - The object of resolved dependencies, used to call itself recursively
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) {
let sorted = []
let visited = {}
Object.keys(graph).forEach(function visit(name, ancestors) {
if (!Array.isArray(ancestors)) ancestors = []
ancestors.push(name)
visited[name] = true
if (typeof graph[name] !== 'undefined') {
graph[name].forEach(function (dep) {
if (visited[dep]) return
visit(dep, ancestors.slice(0))
})
}
if (sorted.indexOf(name) < 0) sorted.push(name)
})
// Don't forget about parts without dependencies
for (const part in this.__designParts) {
if (sorted.indexOf(part) === -1) sorted.push(part)
}
this.__draftOrder = sorted
this.config.draftOrder = sorted
Pattern.prototype.__resolveConfig = function () {
this.config = this.__configResolver.asConfig()
return this
}
@ -1276,154 +965,13 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende
* Resolves parts and their dependencies
*
* @private
* @param {int} count - The count is used to call itself recursively
* @param {int} distance - Keeps track of how far the dependency is from the pattern
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
if (count === 0) {
for (const part of this.designConfig.parts) {
// Keep design parts immutable in the pattern or risk subtle bugs
this.__designParts[part.name] = Object.freeze(part)
}
}
distance++
if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance)
for (const part of this.designConfig.parts) {
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
this.__mutated.partDistance[part.name] = distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'Base partDistance for ' + part.name + ' is ' + this.__mutated.partDistance[part.name]
)
}
}
for (const [name, part] of Object.entries(this.__designParts)) {
const current_part_distance = this.__mutated.partDistance[part.name]
const proposed_dependent_part_distance = current_part_distance + 1
// Hide when hideAll is set
if (part.hideAll) this.__mutated.partHide[part.name] = true
// Inject (from)
if (part.from) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"')
this.__setFromHide(part, name, part.from.name)
this.__designParts[part.from.name] = part.from
this.__inject[name] = part.from.name
if (
typeof this.__mutated.partDistance[part.from.name] === 'undefined' ||
this.__mutated.partDistance[part.from.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[part.from.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"from:" partDistance for ' +
part.from.name +
' is ' +
this.__mutated.partDistance[part.from.name]
)
}
}
// Simple dependency (after)
if (part.after) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"')
if (Array.isArray(part.after)) {
for (const dep of part.after) {
this.__setAfterHide(part, name, dep.name)
this.__designParts[dep.name] = dep
this.__addDependency(name, part, dep)
if (
typeof this.__mutated.partDistance[dep.name] === 'undefined' ||
this.__mutated.partDistance[dep.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[dep.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"after:" partDistance for ' +
dep.name +
' is ' +
this.__mutated.partDistance[dep.name]
)
}
}
} else {
this.__setAfterHide(part, name, part.after.name)
this.__designParts[part.after.name] = part.after
this.__addDependency(name, part, part.after)
if (
typeof this.__mutated.partDistance[part.after.name] === 'undefined' ||
this.__mutated.partDistance[part.after.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[part.after.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"after:" partDistance for ' +
part.after.name +
' is ' +
this.__mutated.partDistance[part.after.name]
)
}
}
}
}
// Did we discover any new dependencies?
const len = Object.keys(this.__designParts).length
// If so, resolve recursively
if (len > count) {
if (DISTANCE_DEBUG) this.store.log.debug('Recursing...')
return this.__resolveParts(len, distance)
}
Pattern.prototype.__resolveParts = function () {
this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p))
// Print final part distances.
for (const part of this.designConfig.parts) {
let qualifier = ''
if (DISTANCE_DEBUG) qualifier = 'final '
this.store.log.debug(
'⚪️ `' +
part.name +
'` ' +
qualifier +
'options priority is __' +
this.__mutated.partDistance[part.name] +
'__'
)
}
for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)
return this
}
/**
* Resolves parts depdendencies into a flat array
*
* @private
* @param {object} graph - The graph is used to call itsels recursively
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveDependencies = function (graph = false) {
if (!graph) graph = this.__dependencies
for (const i in this.__inject) {
const dependency = this.__inject[i]
if (typeof this.__dependencies[i] === 'undefined') this.__dependencies[i] = dependency
else if (this.__dependencies[i] !== dependency) {
if (typeof this.__dependencies[i] === 'string') {
this.__dependencies[i] = [this.__dependencies[i], dependency]
} else if (Array.isArray(this.__dependencies[i])) {
if (this.__dependencies[i].indexOf(dependency) === -1)
this.__dependencies[i].push(dependency)
} else {
this.store.log.error('Part dependencies should be a string or an array of strings')
throw new Error('Part dependencies should be a string or an array of strings')
}
}
}
let resolved = {}
let seen = {}
for (let part in graph) resolved[part] = this.__resolveDependency(seen, part, graph)
for (let part in seen) if (typeof resolved[part] === 'undefined') resolved[part] = []
this.__resolvedDependencies = resolved
this.config.resolvedDependencies = resolved
this.__configResolver.logPartDistances()
return this
}
@ -1461,49 +1009,6 @@ Pattern.prototype.__setBase = function () {
}
}
/**
* Sets visibility of a dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__setFromHide = function (part, name, depName) {
if (
part.hideDependencies ||
part.hideAll ||
this.__mutated.partHide[name] ||
this.__mutated.partHideAll[name]
) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}
/**
* Sets visibility of an 'after' dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__setAfterHide = function (part, name, depName) {
if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}
/**
* Returns the absolute value of a snapped percentage option
*
@ -1590,34 +1095,3 @@ Pattern.prototype.__wants = function (partName, set = 0) {
return true
}
//////////////////////////////////////////////
// STATIC PRIVATE FUNCTIONS //
//////////////////////////////////////////////
/**
* Merges dependencies into a flat list
*
* @private
* @param {array} dep - New dependencies
* @param {array} current - Current dependencies
* @return {array} deps - Merged dependencies
*/
function mergeDependencies(dep = [], current = []) {
// Current dependencies
const list = []
if (Array.isArray(current)) list.push(...current)
else if (typeof current === 'string') list.push(current)
if (Array.isArray(dep)) list.push(...dep)
else if (typeof dep === 'string') list.push(dep)
// Dependencies should be parts names (string) not the object
const deps = []
for (const part of [...new Set(list)]) {
if (typeof part === 'object') deps.push(part.name)
else deps.push(part)
}
return deps
}

View file

@ -38,6 +38,7 @@ export function Store(methods = []) {
logs.warning.push(...data)
},
error: function (...data) {
if (typeof window !== 'undefined') console.error(...data)
logs.error.push(...data)
},
}

View file

@ -32,12 +32,12 @@ describe('Pattern', () => {
expect(typeof pattern.Snippet).to.equal('function')
expect(typeof pattern.Attributes).to.equal('function')
expect(typeof pattern.macros).to.equal('object')
expect(typeof pattern.__designParts).to.equal('object')
expect(typeof pattern.__inject).to.equal('object')
expect(typeof pattern.__dependencies).to.equal('object')
expect(typeof pattern.__resolvedDependencies).to.equal('object')
expect(typeof pattern.__hide).to.equal('object')
expect(Array.isArray(pattern.__draftOrder)).to.equal(true)
// expect(typeof pattern.__designParts).to.equal('object')
// expect(typeof pattern.config.inject).to.equal('object')
// expect(typeof pattern.config.directDependencies).to.equal('object')
// expect(typeof pattern.__resolvedDependencies).to.equal('object')
// expect(typeof pattern.__hide).to.equal('object')
// expect(Array.isArray(pattern.__draftOrder)).to.equal(true)
expect(pattern.width).to.equal(0)
expect(pattern.height).to.equal(0)
expect(pattern.is).to.equal('')
@ -111,7 +111,7 @@ describe('Pattern', () => {
parts: [partC],
})
const pattern = new Pattern()
pattern.draft()
pattern.__init()
it('Pattern.__init() should resolve all measurements', () => {
expect(
@ -145,7 +145,7 @@ describe('Pattern', () => {
})
it('Pattern.__init() should resolve parts', () => {
expect(pattern.designConfig.parts.length).to.equal(3)
expect(Object.keys(pattern.config.parts)).to.have.lengthOf(3)
})
it('Pattern.__init() should resolve plugins', () => {
@ -153,8 +153,8 @@ describe('Pattern', () => {
})
it('Pattern.__init() should set config data in the store', () => {
expect(pattern.setStores[0].get('data.name')).to.equal('test')
expect(pattern.setStores[0].get('data.version')).to.equal('1.2.3')
expect(pattern.store.get('data.name')).to.equal('test')
expect(pattern.store.get('data.version')).to.equal('1.2.3')
})
it('Pattern.__init() should resolve dependencies', () => {
@ -181,6 +181,89 @@ describe('Pattern', () => {
expect(pattern.config.draftOrder[2]).to.equal('test.partC')
})
it('Pattern.__init() should overwrite options from dependencies', () => {
const partD = {
name: 'test.partD',
from: partB,
options: {
optB: { deg: 25, min: 15, max: 45 },
},
draft: ({ part }) => part,
}
const Pattern = new Design({
data: {
name: 'test',
version: '1.2.3',
},
parts: [partD],
})
const pattern = new Pattern()
pattern.__init()
for (const [key, value] of Object.entries(partD.options.optB)) {
expect(pattern.config.options.optB[key]).to.equal(value)
}
})
it('Pattern.__init() should overwrite options from complex dependencies', () => {
const partD = {
name: 'test.partD',
from: partB,
options: {
optB: { deg: 25, min: 15, max: 45 },
},
draft: ({ part }) => part,
}
const partE = {
name: 'test.partE',
from: partD,
options: {
optB: { deg: 10, min: 15, max: 50 },
},
draft: ({ part }) => part,
}
const Pattern = new Design({
data: {
name: 'test',
version: '1.2.3',
},
parts: [partC, partE],
})
const pattern = new Pattern()
pattern.__init()
for (const [key, value] of Object.entries(partE.options.optB)) {
expect(pattern.config.options.optB[key]).to.equal(value)
}
})
it('Pattern.__init() should resolve nested dependencies for multiple parts that depend on the same part', () => {
const partD = {
name: 'test.partD',
from: partB,
draft: ({ part }) => part,
}
const Pattern = new Design({
data: {
name: 'test',
version: '1.2.3',
},
parts: [partC, partD],
})
const pattern = new Pattern()
pattern.__init()
expect(pattern.config.resolvedDependencies['test.partD']).to.have.members([
'test.partA',
'test.partB',
])
expect(pattern.config.resolvedDependencies['test.partC']).to.have.members([
'test.partA',
'test.partB',
])
})
// I am aware this does too much for one unit test, but this is to simplify TDD
// we can split it up later
it('Pattern.__init() should resolve nested injections', () => {
@ -267,14 +350,14 @@ describe('Pattern', () => {
expect(pattern.config.options.optionR.list[1]).to.equal('green')
expect(pattern.config.options.optionR.list[2]).to.equal('blue')
// Dependencies
expect(pattern.__dependencies.partB[0]).to.equal('partA')
expect(pattern.__dependencies.partC[0]).to.equal('partB')
expect(pattern.__dependencies.partR[0]).to.equal('partC')
expect(pattern.__dependencies.partR[1]).to.equal('partA')
expect(pattern.config.directDependencies.partB).to.include('partA')
expect(pattern.config.directDependencies.partC).to.include('partB')
expect(pattern.config.directDependencies.partR).to.include('partC')
expect(pattern.config.directDependencies.partR).to.include('partA')
// Inject
expect(pattern.__inject.partB).to.equal('partA')
expect(pattern.__inject.partC).to.equal('partB')
expect(pattern.__inject.partR).to.equal('partA')
expect(pattern.config.inject.partB).to.equal('partA')
expect(pattern.config.inject.partC).to.equal('partB')
expect(pattern.config.inject.partR).to.equal('partA')
// Draft order
expect(pattern.config.draftOrder[0]).to.equal('partA')
expect(pattern.config.draftOrder[1]).to.equal('partB')
@ -415,12 +498,12 @@ describe('Pattern', () => {
expect(pattern.config.options.optionD.list[1]).to.equal('green')
expect(pattern.config.options.optionD.list[2]).to.equal('blue')
// Dependencies
expect(pattern.__dependencies.partB[0]).to.equal('partA')
expect(pattern.__dependencies.partC[0]).to.equal('partB')
expect(pattern.__dependencies.partD[0]).to.equal('partC')
expect(pattern.config.directDependencies.partB[0]).to.equal('partA')
expect(pattern.config.directDependencies.partC[0]).to.equal('partB')
expect(pattern.config.directDependencies.partD[0]).to.equal('partC')
// Inject
expect(pattern.__inject.partB).to.equal('partA')
expect(pattern.__inject.partC).to.equal('partB')
expect(pattern.config.inject.partB).to.equal('partA')
expect(pattern.config.inject.partC).to.equal('partB')
// Draft order
expect(pattern.config.draftOrder[0]).to.equal('partA')
expect(pattern.config.draftOrder[1]).to.equal('partB')
@ -528,7 +611,7 @@ describe('Pattern', () => {
expect(pattern.hooks.preRender.length).to.equal(2)
})
it('Pattern.__init() should load conditional plugin', () => {
it('Pattern.__init() should load conditional plugin if condition is met', () => {
const plugin = {
name: 'example',
version: 1,
@ -550,7 +633,7 @@ describe('Pattern', () => {
expect(pattern.hooks.preRender.length).to.equal(1)
})
it('Pattern.__init() should not load conditional plugin', () => {
it('Pattern.__init() should not load conditional plugin if condition is not mett', () => {
const plugin = {
name: 'example',
version: 1,
@ -606,6 +689,38 @@ describe('Pattern', () => {
expect(pattern.hooks.preRender.length).to.equal(1)
})
it('Pattern.__init() should load a conditional plugin multiple times with different conditions', () => {
const plugin1 = {
name: 'example1',
version: 1,
hooks: {
preRender: function (svg) {
svg.attributes.add('freesewing:plugin-example', 1)
},
},
}
const condition1 = () => true
const condition2 = () => false
const part = {
name: 'test.part',
plugins: [{ plugin: plugin1, condition: condition1 }],
draft: (part) => part,
}
const part2 = {
name: 'test.part2',
plugins: [{ plugin: plugin1, condition: condition2 }],
draft: (part) => part,
}
const design = new Design({ parts: [part, part2] })
const pattern = new design()
pattern.__init()
expect(pattern.config.plugins).to.be.an('object').that.has.all.keys('example1', 'example1_')
expect(pattern.config.plugins.example1.plugin).to.deep.equal(plugin1)
expect(pattern.config.plugins.example1_.plugin).to.deep.equal(plugin1)
expect(pattern.hooks.preRender.length).to.equal(1)
})
it('Load conditional plugins that are also passing data', () => {
const plugin1 = {
name: 'example1',
@ -699,6 +814,191 @@ describe('Pattern', () => {
expect(count).to.equal(2)
})
describe('Hiding parts', () => {
const blankDraft = ({ part }) => part
const afterPart = {
name: 'afterPart',
draft: blankDraft,
}
const fromPart = {
name: 'fromPart',
draft: blankDraft,
}
describe('{hide: true}', () => {
const mainPart = {
name: 'mainPart',
after: afterPart,
from: fromPart,
hide: true,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [mainPart],
})
const pattern = new Test()
pattern.__init()
it('Should hide the part', () => {
expect(pattern.__isPartHidden('mainPart')).to.be.true
})
it("Should not hide the part's dependencies", () => {
expect(pattern.__isPartHidden('fromPart')).to.be.false
expect(pattern.__isPartHidden('afterPart')).to.be.false
})
describe('Nested Parts', () => {
const mainPart = {
name: 'mainPart',
after: afterPart,
from: fromPart,
draft: blankDraft,
}
const grandChild = {
name: 'grandChild',
from: mainPart,
hide: true,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [grandChild],
})
const pattern = new Test()
pattern.__init()
it('should not hide nested `from` dependencies', () => {
expect(pattern.__isPartHidden('fromPart')).to.be.false
expect(pattern.__isPartHidden('mainPart')).to.be.false
})
it('should not hide nested `after` dependencies', () => {
expect(pattern.__isPartHidden('afterPart')).to.be.false
})
})
})
describe('{hideDependencies: true}', () => {
const mainPart = {
name: 'mainPart',
hideDependencies: true,
after: afterPart,
from: fromPart,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [mainPart],
})
const pattern = new Test()
pattern.__init()
it('Should not hide the part', () => {
expect(pattern.__isPartHidden('mainPart')).to.be.false
})
it("Should hide the part's `from` dependencies", () => {
expect(pattern.__isPartHidden('fromPart')).to.be.true
})
it("Should not hide the part's `after` dependencies", () => {
expect(pattern.__isPartHidden('afterPart')).to.be.false
})
describe('Nested Parts', () => {
const mainPart = {
name: 'mainPart',
after: afterPart,
from: fromPart,
draft: blankDraft,
}
const grandChild = {
name: 'grandChild',
from: mainPart,
hideDependencies: true,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [grandChild],
})
const pattern = new Test()
pattern.__init()
it('should hide nested `from` dependencies', () => {
expect(pattern.__isPartHidden('fromPart')).to.be.true
expect(pattern.__isPartHidden('mainPart')).to.be.true
})
it('should not hide nested `after` dependencies', () => {
expect(pattern.__isPartHidden('afterPart')).to.be.false
})
})
})
describe('{hideAll: true}', () => {
const mainPart = {
name: 'mainPart',
hideAll: true,
after: afterPart,
from: fromPart,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [mainPart],
})
const pattern = new Test()
pattern.__init()
it('Should hide the part', () => {
expect(pattern.__isPartHidden('mainPart')).to.be.true
})
it("Should hide the part's `from` dependencies", () => {
expect(pattern.__isPartHidden('fromPart')).to.be.true
})
it("Should hide the part's `after` dependencies", () => {
expect(pattern.__isPartHidden('afterPart')).to.be.true
})
describe('Nested Parts', () => {
const mainPart = {
name: 'mainPart',
after: afterPart,
from: fromPart,
draft: blankDraft,
}
const grandChild = {
name: 'grandChild',
from: mainPart,
hideAll: true,
draft: blankDraft,
}
const Test = new Design({
name: 'test',
parts: [grandChild],
})
const pattern = new Test()
pattern.__init()
it('should hide nested `from` dependencies', () => {
expect(pattern.__isPartHidden('fromPart')).to.be.true
expect(pattern.__isPartHidden('mainPart')).to.be.true
})
it('should hide nested `after` dependencies', () => {
expect(pattern.__isPartHidden('afterPart')).to.be.true
})
})
})
})
it('Should check whether created parts get the pattern context', () => {
let partContext
const plugin = {

View file

@ -54,11 +54,14 @@ describe('Pattern', () => {
name: 'test',
draft: ({ part }) => part,
}
const design = new Design({ parts: [test] })
const you = {
name: 'you',
draft: ({ part }) => part,
}
const design = new Design({ parts: [test, you] })
const pattern = new design({ only: ['you'] })
pattern.draft()
expect(pattern.setStores[0].logs.debug.length).to.equal(4)
expect(pattern.setStores[0].logs.debug[3]).to.equal(
expect(pattern.setStores[0].logs.debug).to.include(
'Part `test` is not needed. Skipping draft and setting hidden to `true`'
)
})

View file

@ -0,0 +1,202 @@
import chai from 'chai'
import { Design } from '../src/index.mjs'
import sinon from 'sinon'
const expect = chai.expect
describe('Pattern', () => {
describe('.addPart()', () => {
const part1 = {
name: 'test',
draft: ({ part }) => part,
}
const part2 = {
name: 'test2',
after: part1,
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
from: part2,
draft: ({ part }) => part,
}
describe('with resolveImmediately: true', () => {
it('Should add the part to parts object', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
pattern.addPart(part2, true)
expect(pattern.config.parts.test2).to.equal(part2)
})
it('Should resolve injected dependencies for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
pattern.addPart(part3, true)
expect(pattern.config.inject.test3).to.equal('test2')
})
it('Should resolve all dependencies for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
pattern.addPart(part3, true)
expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2'])
expect(pattern.config.parts.test2).to.equal(part2)
})
it('Should add a the measurements for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const part2 = {
name: 'test2',
measurements: ['neck'],
draft: ({ part }) => part,
}
pattern.addPart(part2, true)
expect(pattern.config.measurements).to.include('neck')
})
it('Should add the plugins for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const plugin = { name: 'testPlugin' }
const part2 = {
name: 'test2',
plugins: [plugin],
draft: ({ part }) => part,
}
pattern.addPart(part2, true)
expect(pattern.config.plugins.testPlugin).to.equal(plugin)
})
it('Should resolve the options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const opt1 = { pct: 10, min: 0, max: 50 }
const part2 = {
name: 'test2',
options: {
opt1,
},
draft: ({ part }) => part,
}
pattern.addPart(part2, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
it('Should resolve the dependency options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const opt1 = { pct: 10, min: 0, max: 50 }
const part2 = {
name: 'test2',
options: {
opt1,
},
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
from: part2,
draft: ({ part }) => part,
}
pattern.addPart(part3, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
it('Should resolve the overwritten options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const opt1 = { pct: 10, min: 0, max: 50 }
const part2 = {
name: 'test2',
options: {
opt1: { pct: 15, min: 10, max: 55 },
},
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
from: part2,
options: {
opt1,
},
draft: ({ part }) => part,
}
pattern.addPart(part3, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
describe('during drafting', () => {
it('adds the part to the draft queue', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.use({
name: 'draftTimePartPlugin',
hooks: {
postPartDraft: (pattern) => {
const newPart = {
name: 'newPartTest',
draft: ({ part }) => part,
}
pattern.addPart(newPart)
},
},
})
pattern.draft()
expect(pattern.draftQueue.contains('newPartTest')).to.be.true
})
it('drafts the part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
const part2Draft = ({ part }) => part
const draftSpy = sinon.spy(part2Draft)
pattern.use({
name: 'draftTimePartPlugin',
hooks: {
postPartDraft: (pattern) => {
const newPart = {
name: 'newPartTest',
draft: draftSpy,
}
pattern.addPart(newPart)
},
},
})
pattern.draft()
expect(draftSpy.calledOnce).to.be.true
})
})
})
describe('with resolveImmediately: false', () => {
it('does not create duplications in the configuration')
})
})
})

View file

@ -42,6 +42,7 @@ describe('Snapped options', () => {
snap: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144],
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({ options: { test: 0.13 }, measurements }).draft()
@ -67,6 +68,7 @@ describe('Snapped options', () => {
},
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({ options: { test: 0.13 }, measurements, units: 'metric' }).draft()
@ -94,6 +96,7 @@ describe('Snapped options', () => {
},
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({

View file

@ -4750,6 +4750,34 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
"@sinonjs/commons@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@10.0.2", "@sinonjs/fake-timers@^10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c"
integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==
dependencies:
"@sinonjs/commons" "^2.0.0"
"@sinonjs/samsam@^7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f"
integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==
dependencies:
"@sinonjs/commons" "^2.0.0"
lodash.get "^4.4.2"
type-detect "^4.0.8"
"@sinonjs/text-encoding@^0.7.1":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
@ -6226,10 +6254,10 @@ bin-links@^3.0.0:
rimraf "^3.0.0"
write-file-atomic "^4.0.0"
bin-pack@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bin-pack/-/bin-pack-1.0.2.tgz#c2a014edbf0bed70a3292062ed46577b96120679"
integrity sha512-aOk0SxEon5LF9cMxQFViSKb4qccG6rs7XKyMXIb1J8f8LA2acTIWnHdT0IOTe4gYBbqgjdbuTZ5f+UP+vlh4Mw==
bin-pack-with-constraints@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bin-pack-with-constraints/-/bin-pack-with-constraints-1.0.1.tgz#47e67b481724a8a5a3644ec9c9ccf881c2725096"
integrity sha512-fiPxvZAWuIqLpK79Ov58PztT/ZiGbpDB7usifleilJ/4aqhSx0ivY5kRifESJnR2rq51pScbUJ0LgCebufGJnA==
binary-extensions@^2.0.0:
version "2.2.0"
@ -12259,6 +12287,11 @@ just-diff@^5.0.1:
resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.0.3.tgz#4c9c514dec5526b25ab977590e3c39a0cf271554"
integrity sha512-a8p80xcpJ6sdurk5PxDKb4mav9MeKjA3zFKZpCWBIfvg8mznfnmb13MKZvlrwJ+Lhis0wM3uGAzE0ArhFHvIcg==
just-extend@^4.0.2:
version "4.2.1"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@ -12933,7 +12966,7 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4"
integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==
luxon@3.2.1:
luxon@latest:
version "3.2.1"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f"
integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==
@ -14337,6 +14370,17 @@ next@13.1.6:
"@next/swc-win32-ia32-msvc" "13.1.6"
"@next/swc-win32-x64-msvc" "13.1.6"
nise@^5.1.2:
version "5.1.4"
resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
dependencies:
"@sinonjs/commons" "^2.0.0"
"@sinonjs/fake-timers" "^10.0.2"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
nlcst-to-string@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz#9315dfab80882bbfd86ddf1b706f53622dc400cc"
@ -15535,6 +15579,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@ -18346,6 +18397,18 @@ simple-wcswidth@^1.0.1:
resolved "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2"
integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==
sinon@^15.0.1:
version "15.0.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.1.tgz#ce062611a0b131892e2c18f03055b8eb6e8dc234"
integrity sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg==
dependencies:
"@sinonjs/commons" "^2.0.0"
"@sinonjs/fake-timers" "10.0.2"
"@sinonjs/samsam" "^7.0.1"
diff "^5.0.0"
nise "^5.1.2"
supports-color "^7.2.0"
sirv@^1.0.7:
version "1.0.19"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
@ -19131,7 +19194,7 @@ supports-color@^5.3.0, supports-color@^5.5.0:
dependencies:
has-flag "^3.0.0"
supports-color@^7.0.0, supports-color@^7.1.0:
supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
@ -19650,7 +19713,7 @@ type-component@0.0.1:
resolved "https://registry.yarnpkg.com/type-component/-/type-component-0.0.1.tgz#952a6c81c21efd24d13d811d0c8498cb860e1956"
integrity sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw==
type-detect@^4.0.0, type-detect@^4.0.5:
type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==