@medusajs/admin-shared
Advanced tools
Comparing version 0.0.2-snapshot-20230612144720 to 0.0.2-snapshot-20240523143641
import { ComponentType } from 'react'; | ||
import { NavigateFunction } from 'react-router-dom'; | ||
declare const extensionTypes: readonly ["widget", "route", "nested-route"]; | ||
declare const injectionZones: readonly ["order.details.before", "order.details.after", "order.list.before", "order.list.after", "draft_order.list", "draft_order.details", "customer.details", "customer.list", "customer_group.details", "customer_group.list", "product.details.before", "product.details.after", "product.list.before", "product.list.after", "product_collection.details", "product_collection.list", "price_list.details", "price_list.list", "discount.details", "discount.list", "gift_card.details", "gift_card.list", "custom_gift_card", "login.before", "login.after"]; | ||
/** | ||
* All valid injection zones in the admin panel. An injection zone is a specific place | ||
* in the admin panel where a plugin can inject custom widgets. | ||
*/ | ||
declare const INJECTION_ZONES: readonly ["order.details.before", "order.details.after", "order.list.before", "order.list.after", "draft_order.list.before", "draft_order.list.after", "draft_order.details.before", "draft_order.details.after", "customer.details.before", "customer.details.after", "customer.list.before", "customer.list.after", "customer_group.details.before", "customer_group.details.after", "customer_group.list.before", "customer_group.list.after", "product.details.before", "product.details.after", "product.list.before", "product.list.after", "product.details.side.before", "product.details.side.after", "product_collection.details.before", "product_collection.details.after", "product_collection.list.before", "product_collection.list.after", "product_category.details.before", "product_category.details.after", "product_category.list.before", "product_category.list.after", "price_list.details.before", "price_list.details.after", "price_list.list.before", "price_list.list.after", "discount.details.before", "discount.details.after", "discount.list.before", "discount.list.after", "promotion.details.before", "promotion.details.after", "promotion.list.before", "promotion.list.after", "gift_card.details.before", "gift_card.details.after", "gift_card.list.before", "gift_card.list.after", "custom_gift_card.before", "custom_gift_card.after", "login.before", "login.after"]; | ||
type InjectionZone = typeof injectionZones[number]; | ||
type ExtensionType = typeof extensionTypes[number]; | ||
type InjectionZone = (typeof INJECTION_ZONES)[number]; | ||
/** | ||
* Validates that the provided zone is a valid injection zone for a widget. | ||
*/ | ||
declare function isValidInjectionZone(zone: any): zone is InjectionZone; | ||
type WidgetConfig = { | ||
type: "widget"; | ||
zone: InjectionZone | InjectionZone[]; | ||
}; | ||
type RouteConfig = { | ||
type: "route"; | ||
path: string; | ||
title: string; | ||
label?: string; | ||
icon?: ComponentType; | ||
}; | ||
type NestedRouteConfig = { | ||
type: "nested-route"; | ||
path: string; | ||
parent: string; | ||
}; | ||
type ExtensionConfig = WidgetConfig | RouteConfig | NestedRouteConfig; | ||
type WidgetExtension = { | ||
Component: React.ComponentType<any>; | ||
config: WidgetConfig; | ||
}; | ||
type RouteExtension = { | ||
Component: React.ComponentType<any>; | ||
config: RouteConfig; | ||
}; | ||
type NestedRouteExtension = { | ||
Component: React.ComponentType<any>; | ||
config: NestedRouteConfig; | ||
}; | ||
type Extension = WidgetExtension | RouteExtension | NestedRouteExtension; | ||
type ExtensionsEntry = { | ||
identifier: string; | ||
extensions: Extension[]; | ||
}; | ||
type Widget = { | ||
origin: string; | ||
Widget: ComponentType<any>; | ||
}; | ||
type Route = { | ||
origin: string; | ||
path: string; | ||
Page: ComponentType<any>; | ||
}; | ||
type NestedRoute = { | ||
origin: string; | ||
parent: string; | ||
path: string; | ||
Page: ComponentType<any>; | ||
}; | ||
type Link = Pick<RouteConfig, "path" | "title" | "icon">; | ||
type Notify = { | ||
success: (title: string, message: string) => void; | ||
error: (title: string, message: string) => void; | ||
info: (title: string, message: string) => void; | ||
warning: (title: string, message: string) => void; | ||
}; | ||
interface ExtensionProps { | ||
navigate: NavigateFunction; | ||
notify: Notify; | ||
} | ||
declare function isValidExtensionType(val: any): val is ExtensionType; | ||
declare function isValidInjectionZone(val: any): val is InjectionZone; | ||
declare function isWidgetExtension(extension: Extension): extension is WidgetExtension; | ||
declare function isRouteExtension(extension: Extension): extension is RouteExtension; | ||
declare function isNestedRouteExtension(extension: Extension): extension is NestedRouteExtension; | ||
/** | ||
* Define a widget configuration. | ||
* | ||
* @param config The widget configuration. | ||
* @returns The widget configuration. | ||
*/ | ||
declare function defineWidgetConfig(config: WidgetConfig): WidgetConfig; | ||
/** | ||
* Define a route configuration. | ||
* | ||
* @param config The route configuration. | ||
* @returns The route configuration. | ||
*/ | ||
declare function defineRouteConfig(config: RouteConfig): RouteConfig; | ||
declare class WidgetRegistry { | ||
private widgets; | ||
registerWidget(origin: string, widget: WidgetExtension): void; | ||
getWidgets(zone: InjectionZone): Widget[]; | ||
} | ||
/** | ||
* All virtual modules that are used in the admin panel. Virtual modules are used | ||
* to inject custom widgets, routes and settings. A virtual module is imported using | ||
* a string that corresponds to the id of the virtual module. | ||
* | ||
* @example | ||
* ```ts | ||
* import ProductDetailsBefore from "virtual:medusa/widgets/product/details/before" | ||
* ``` | ||
*/ | ||
declare const VIRTUAL_MODULES: string[]; | ||
/** | ||
* Reolved paths to all virtual widget modules. | ||
*/ | ||
declare const RESOLVED_WIDGET_MODULES: string[]; | ||
/** | ||
* Reolved paths to all virtual route modules. | ||
*/ | ||
declare const RESOLVED_ROUTE_MODULES: string[]; | ||
declare class RouteRegistry { | ||
links: Link[]; | ||
routes: Route[]; | ||
nestedRoutes: Map<string, Route[]>; | ||
private formatPath_; | ||
registerRoute(origin: string, route: RouteExtension): void; | ||
registerNestedRoute(origin: string, route: NestedRouteExtension): void; | ||
getLinks(): Link[]; | ||
getRoutes(): Route[]; | ||
getNestedRoutes(parent: string): Route[]; | ||
} | ||
declare const getVirtualId: (name: string) => string; | ||
declare const resolveVirtualId: (id: string) => string; | ||
declare const getWidgetImport: (zone: InjectionZone) => string; | ||
declare const getWidgetZone: (resolvedId: string) => InjectionZone; | ||
export { Extension, ExtensionConfig, ExtensionProps, ExtensionType, ExtensionsEntry, InjectionZone, Link, NestedRoute, NestedRouteConfig, NestedRouteExtension, Route, RouteConfig, RouteExtension, RouteRegistry, Widget, WidgetConfig, WidgetExtension, WidgetRegistry, extensionTypes, injectionZones, isNestedRouteExtension, isRouteExtension, isValidExtensionType, isValidInjectionZone, isWidgetExtension }; | ||
export { INJECTION_ZONES, type InjectionZone, RESOLVED_ROUTE_MODULES, RESOLVED_WIDGET_MODULES, type RouteConfig, VIRTUAL_MODULES, type WidgetConfig, defineRouteConfig, defineWidgetConfig, getVirtualId, getWidgetImport, getWidgetZone, isValidInjectionZone, resolveVirtualId }; |
@@ -0,1 +1,2 @@ | ||
"use strict"; | ||
var __defProp = Object.defineProperty; | ||
@@ -5,3 +6,2 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); | ||
var __export = (target, all) => { | ||
@@ -24,36 +24,66 @@ for (var name in all) | ||
__export(src_exports, { | ||
RouteRegistry: () => route_registry_default, | ||
WidgetRegistry: () => widget_registry_default, | ||
extensionTypes: () => extensionTypes, | ||
injectionZones: () => injectionZones, | ||
isNestedRouteExtension: () => isNestedRouteExtension, | ||
isRouteExtension: () => isRouteExtension, | ||
isValidExtensionType: () => isValidExtensionType, | ||
INJECTION_ZONES: () => INJECTION_ZONES, | ||
RESOLVED_ROUTE_MODULES: () => RESOLVED_ROUTE_MODULES, | ||
RESOLVED_WIDGET_MODULES: () => RESOLVED_WIDGET_MODULES, | ||
VIRTUAL_MODULES: () => VIRTUAL_MODULES, | ||
defineRouteConfig: () => defineRouteConfig, | ||
defineWidgetConfig: () => defineWidgetConfig, | ||
getVirtualId: () => getVirtualId, | ||
getWidgetImport: () => getWidgetImport, | ||
getWidgetZone: () => getWidgetZone, | ||
isValidInjectionZone: () => isValidInjectionZone, | ||
isWidgetExtension: () => isWidgetExtension | ||
resolveVirtualId: () => resolveVirtualId | ||
}); | ||
module.exports = __toCommonJS(src_exports); | ||
// src/constants.ts | ||
var extensionTypes = [ | ||
"widget", | ||
"route", | ||
"nested-route" | ||
]; | ||
var injectionZones = [ | ||
// Order injection zones | ||
// src/extensions/config/utils.ts | ||
function createConfigHelper(config) { | ||
return { | ||
...config, | ||
/** | ||
* This property is required to allow the config to be exported, | ||
* while still allowing HMR to work correctly. | ||
* | ||
* It tricks Fast Refresh into thinking that the config is a React component, | ||
* which allows it to be updated without a full page reload. | ||
*/ | ||
$$typeof: Symbol.for("react.memo") | ||
}; | ||
} | ||
function defineWidgetConfig(config) { | ||
return createConfigHelper(config); | ||
} | ||
function defineRouteConfig(config) { | ||
return createConfigHelper(config); | ||
} | ||
// src/extensions/routes/constants.ts | ||
var ROUTE_IMPORTS = ["routes/pages", "routes/links"]; | ||
// src/extensions/widgets/constants.ts | ||
var ORDER_INJECTION_ZONES = [ | ||
"order.details.before", | ||
"order.details.after", | ||
"order.list.before", | ||
"order.list.after", | ||
// Draft order injection zones | ||
"draft_order.list", | ||
"draft_order.details", | ||
// Customer injection zones | ||
"customer.details", | ||
"customer.list", | ||
// Customer group injection zones | ||
"customer_group.details", | ||
"customer_group.list", | ||
// Product injection zones | ||
"order.list.after" | ||
]; | ||
var DRAFT_ORDER_INJECTION_ZONES = [ | ||
"draft_order.list.before", | ||
"draft_order.list.after", | ||
"draft_order.details.before", | ||
"draft_order.details.after" | ||
]; | ||
var CUSTOMER_INJECTION_ZONES = [ | ||
"customer.details.before", | ||
"customer.details.after", | ||
"customer.list.before", | ||
"customer.list.after" | ||
]; | ||
var CUSTOMER_GROUP_INJECTION_ZONES = [ | ||
"customer_group.details.before", | ||
"customer_group.details.after", | ||
"customer_group.list.before", | ||
"customer_group.list.after" | ||
]; | ||
var PRODUCT_INJECTION_ZONES = [ | ||
"product.details.before", | ||
@@ -63,134 +93,111 @@ "product.details.after", | ||
"product.list.after", | ||
// Product collection injection zones | ||
"product_collection.details", | ||
"product_collection.list", | ||
// Price list injection zones | ||
"price_list.details", | ||
"price_list.list", | ||
// Discount injection zones | ||
"discount.details", | ||
"discount.list", | ||
// Gift card injection zones | ||
"gift_card.details", | ||
"gift_card.list", | ||
"custom_gift_card", | ||
// Login | ||
"login.before", | ||
"login.after" | ||
"product.details.side.before", | ||
"product.details.side.after" | ||
]; | ||
var PRODUCT_COLLECTION_INJECTION_ZONES = [ | ||
"product_collection.details.before", | ||
"product_collection.details.after", | ||
"product_collection.list.before", | ||
"product_collection.list.after" | ||
]; | ||
var PRODUCT_CATEGORY_INJECTION_ZONES = [ | ||
"product_category.details.before", | ||
"product_category.details.after", | ||
"product_category.list.before", | ||
"product_category.list.after" | ||
]; | ||
var PRICE_LIST_INJECTION_ZONES = [ | ||
"price_list.details.before", | ||
"price_list.details.after", | ||
"price_list.list.before", | ||
"price_list.list.after" | ||
]; | ||
var DISCOUNT_INJECTION_ZONES = [ | ||
"discount.details.before", | ||
"discount.details.after", | ||
"discount.list.before", | ||
"discount.list.after" | ||
]; | ||
var PROMOTION_INJECTION_ZONES = [ | ||
"promotion.details.before", | ||
"promotion.details.after", | ||
"promotion.list.before", | ||
"promotion.list.after" | ||
]; | ||
var GIFT_CARD_INJECTION_ZONES = [ | ||
"gift_card.details.before", | ||
"gift_card.details.after", | ||
"gift_card.list.before", | ||
"gift_card.list.after", | ||
"custom_gift_card.before", | ||
"custom_gift_card.after" | ||
]; | ||
var LOGIN_INJECTION_ZONES = ["login.before", "login.after"]; | ||
var INJECTION_ZONES = [ | ||
...ORDER_INJECTION_ZONES, | ||
...DRAFT_ORDER_INJECTION_ZONES, | ||
...CUSTOMER_INJECTION_ZONES, | ||
...CUSTOMER_GROUP_INJECTION_ZONES, | ||
...PRODUCT_INJECTION_ZONES, | ||
...PRODUCT_COLLECTION_INJECTION_ZONES, | ||
...PRODUCT_CATEGORY_INJECTION_ZONES, | ||
...PRICE_LIST_INJECTION_ZONES, | ||
...DISCOUNT_INJECTION_ZONES, | ||
...PROMOTION_INJECTION_ZONES, | ||
...GIFT_CARD_INJECTION_ZONES, | ||
...LOGIN_INJECTION_ZONES | ||
]; | ||
// src/utils.ts | ||
function isValidExtensionType(val) { | ||
return extensionTypes.includes(val); | ||
// src/extensions/widgets/utils.ts | ||
function isValidInjectionZone(zone) { | ||
return INJECTION_ZONES.includes(zone); | ||
} | ||
__name(isValidExtensionType, "isValidExtensionType"); | ||
function isValidInjectionZone(val) { | ||
return injectionZones.includes(val); | ||
} | ||
__name(isValidInjectionZone, "isValidInjectionZone"); | ||
function isWidgetExtension(extension) { | ||
return extension.config.type === "widget"; | ||
} | ||
__name(isWidgetExtension, "isWidgetExtension"); | ||
function isRouteExtension(extension) { | ||
return extension.config.type === "route"; | ||
} | ||
__name(isRouteExtension, "isRouteExtension"); | ||
function isNestedRouteExtension(extension) { | ||
return extension.config.type === "nested-route"; | ||
} | ||
__name(isNestedRouteExtension, "isNestedRouteExtension"); | ||
// src/widget-registry.tsx | ||
var WidgetRegistry = /* @__PURE__ */ __name(class WidgetRegistry2 { | ||
widgets = /* @__PURE__ */ new Map(); | ||
registerWidget(origin, widget) { | ||
const { zone } = widget.config; | ||
const zones = Array.isArray(zone) ? zone : [ | ||
zone | ||
]; | ||
for (let widgetZone of zones) { | ||
const widgets = this.widgets.get(widgetZone) || []; | ||
widgets.push({ | ||
origin, | ||
Widget: widget.Component | ||
}); | ||
this.widgets.set(widgetZone, widgets); | ||
} | ||
} | ||
getWidgets(zone) { | ||
return this.widgets.get(zone) || []; | ||
} | ||
}, "WidgetRegistry"); | ||
var widget_registry_default = WidgetRegistry; | ||
// src/extensions/virtual/utils.ts | ||
var PREFIX = "virtual:medusa/"; | ||
var getVirtualId = (name) => { | ||
return `${PREFIX}${name}`; | ||
}; | ||
var resolveVirtualId = (id) => { | ||
return `\0${id}`; | ||
}; | ||
var getWidgetImport = (zone) => { | ||
return `widgets/${zone.replace(/\./g, "/")}`; | ||
}; | ||
var getWidgetZone = (resolvedId) => { | ||
const virtualPrefix = `\0${PREFIX}widgets/`; | ||
const zone = resolvedId.replace(virtualPrefix, "").replace(/\//g, "."); | ||
return zone; | ||
}; | ||
// src/route-registry.tsx | ||
var RouteRegistry = /* @__PURE__ */ __name(class RouteRegistry2 { | ||
links = []; | ||
routes = []; | ||
nestedRoutes = /* @__PURE__ */ new Map(); | ||
formatPath_(path) { | ||
if (path.startsWith("a/")) { | ||
path = path.substring(2); | ||
} | ||
if (path.startsWith("/a/")) { | ||
path = path.substring(3); | ||
} | ||
if (path.startsWith("/")) { | ||
path = path.substring(1); | ||
} | ||
if (path.endsWith("/")) { | ||
path = path.substring(0, path.length - 1); | ||
} | ||
return path; | ||
} | ||
registerRoute(origin, route) { | ||
const { path, title, icon } = route.config; | ||
const formattedPath = this.formatPath_(path); | ||
this.routes.push({ | ||
origin, | ||
path: formattedPath, | ||
Page: route.Component | ||
}); | ||
this.links.push({ | ||
path: formattedPath, | ||
title, | ||
icon | ||
}); | ||
} | ||
registerNestedRoute(origin, route) { | ||
const { path, parent } = route.config; | ||
const formattedPath = this.formatPath_(path); | ||
const formattedParent = this.formatPath_(parent); | ||
const routes = this.nestedRoutes.get(formattedParent) || []; | ||
routes.push({ | ||
origin, | ||
path: formattedPath, | ||
Page: route.Component | ||
}); | ||
this.nestedRoutes.set(formattedParent, routes); | ||
} | ||
getLinks() { | ||
return this.links; | ||
} | ||
getRoutes() { | ||
return this.routes; | ||
} | ||
getNestedRoutes(parent) { | ||
return this.nestedRoutes.get(parent) || []; | ||
} | ||
}, "RouteRegistry"); | ||
var route_registry_default = RouteRegistry; | ||
// src/extensions/virtual/constants.ts | ||
var VIRTUAL_WIDGET_MODULES = INJECTION_ZONES.map((zone) => { | ||
return getVirtualId(getWidgetImport(zone)); | ||
}); | ||
var VIRTUAL_ROUTE_MODULES = ROUTE_IMPORTS.map((route) => { | ||
return getVirtualId(route); | ||
}); | ||
var VIRTUAL_MODULES = [ | ||
...VIRTUAL_WIDGET_MODULES, | ||
...VIRTUAL_ROUTE_MODULES | ||
]; | ||
var RESOLVED_WIDGET_MODULES = VIRTUAL_WIDGET_MODULES.map((id) => { | ||
return resolveVirtualId(id); | ||
}); | ||
var RESOLVED_ROUTE_MODULES = VIRTUAL_ROUTE_MODULES.map((id) => { | ||
return resolveVirtualId(id); | ||
}); | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
RouteRegistry, | ||
WidgetRegistry, | ||
extensionTypes, | ||
injectionZones, | ||
isNestedRouteExtension, | ||
isRouteExtension, | ||
isValidExtensionType, | ||
INJECTION_ZONES, | ||
RESOLVED_ROUTE_MODULES, | ||
RESOLVED_WIDGET_MODULES, | ||
VIRTUAL_MODULES, | ||
defineRouteConfig, | ||
defineWidgetConfig, | ||
getVirtualId, | ||
getWidgetImport, | ||
getWidgetZone, | ||
isValidInjectionZone, | ||
isWidgetExtension | ||
resolveVirtualId | ||
}); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@medusajs/admin-shared", | ||
"version": "0.0.2-snapshot-20230612144720", | ||
"description": "Shared code for Medusa admin packages.", | ||
"version": "0.0.2-snapshot-20240523143641", | ||
"author": "Kasper Kristensen <kasper@medusajs.com>", | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"files": [ | ||
"dist" | ||
"dist", | ||
"package.json" | ||
], | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https:www.github.com/medusajs/medusa.git", | ||
"directory": "packages/admin-shared" | ||
}, | ||
"bugs": { | ||
"url": "https:www.github.com/medusajs/medusa/issues" | ||
}, | ||
"scripts": { | ||
"build": "tsup" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=18.2.0" | ||
}, | ||
"devDependencies": { | ||
"@swc/core": "1.3.60", | ||
"@types/react": "18.2.7", | ||
"react": "18.2.0", | ||
"react-router-dom": "6.8.0", | ||
"tsup": "6.7.0", | ||
"typescript": "5.0.4" | ||
"@types/react": "^18.3.2", | ||
"tsup": "^8.0.2", | ||
"typescript": "^5.3.3" | ||
}, | ||
"packageManager": "yarn@3.2.1" | ||
} |
@@ -1,40 +0,1 @@ | ||
<p align="center"> | ||
<a href="https://www.medusajs.com"> | ||
<img alt="Medusa" src="https://user-images.githubusercontent.com/7554214/153162406-bf8fd16f-aa98-4604-b87b-e13ab4baf604.png" width="100" /> | ||
</a> | ||
</p> | ||
<h1 align="center"> | ||
@medusajs/admin-shared | ||
</h1> | ||
<h4 align="center"> | ||
<a href="https://docs.medusajs.com">Documentation</a> | | ||
<a href="https://demo.medusajs.com/">Medusa Admin Demo</a> | | ||
<a href="https://www.medusajs.com">Website</a> | ||
</h4> | ||
<p align="center"> | ||
An open source composable commerce engine built for developers. | ||
</p> | ||
<p align="center"> | ||
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE"> | ||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." /> | ||
</a> | ||
<a href="https://circleci.com/gh/medusajs/medusa"> | ||
<img src="https://circleci.com/gh/medusajs/medusa.svg?style=shield" alt="Current CircleCI build status." /> | ||
</a> | ||
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md"> | ||
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" /> | ||
</a> | ||
<a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a> | ||
<a href="https://discord.gg/xpCwq3Kfn8"> | ||
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" /> | ||
</a> | ||
<a href="https://twitter.com/intent/follow?screen_name=medusajs"> | ||
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" /> | ||
</a> | ||
</p> | ||
## Introduction | ||
This package contains shared logic and types for the Medusa Admin. The package is for internal usage, and should not be installed directly. | ||
# shared |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
0
3
17681
6
412
1
2
1
1