orionplace-react 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -152
- package/bin/orionplace-react.mjs +37 -2
- package/package.json +1 -1
- package/src/install.mjs +355 -0
- package/src/project-readme.mjs +7 -0
package/README.md
CHANGED
|
@@ -1,155 +1,28 @@
|
|
|
1
1
|
# orionplace-react
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
[
|
|
25
|
-
|
|
26
|
-
[`ProfileDetailsForm`](templates/components/orionplace/account/ProfileDetailsForm.README.md)
|
|
27
|
-
edits the customer's name and date of birth while keeping the sign-in phone
|
|
28
|
-
read-only.
|
|
29
|
-
[`ProfilePasswordForm`](templates/components/orionplace/account/ProfilePasswordForm.README.md)
|
|
30
|
-
creates or changes the customer's password with matching confirmation.
|
|
31
|
-
[`ProfileDeleteAccount`](templates/components/orionplace/account/ProfileDeleteAccount.README.md)
|
|
32
|
-
lets the customer delete their own account after confirming it cannot be restored.
|
|
33
|
-
[`ProfileEmailSettings`](templates/components/orionplace/account/ProfileEmailSettings.README.md)
|
|
34
|
-
handles the email verification steps, optional notification choice and notification
|
|
35
|
-
language.
|
|
36
|
-
[`AccountSection`](templates/components/orionplace/account/AccountSection.README.md)
|
|
37
|
-
provides authenticated account navigation plus guest and initialization failure
|
|
38
|
-
states.
|
|
39
|
-
[`StorefrontLogout`](templates/components/orionplace/account/StorefrontLogout.README.md)
|
|
40
|
-
presents pending, failed and completed sign-out states while the route owns the
|
|
41
|
-
request and navigation.
|
|
42
|
-
[`StorefrontProfile`](templates/components/orionplace/account/StorefrontProfile.README.md)
|
|
43
|
-
assembles the authenticated profile summary and all four profile settings
|
|
44
|
-
sections, and shows a sign-in explanation to guests.
|
|
45
|
-
[`ProfileSignIn`](templates/components/orionplace/account/ProfileSignIn.README.md)
|
|
46
|
-
renders every phone, password, one-time-code and registration step and reports
|
|
47
|
-
typed authentication actions to the host.
|
|
48
|
-
[`SignInHistory`](templates/components/orionplace/account/SignInHistory.README.md)
|
|
49
|
-
reports initial, filtered and paginated sign-in-history requests and renders the
|
|
50
|
-
returned devices, browsers and locations.
|
|
51
|
-
[`AccountPromocodes`](templates/components/orionplace/account/AccountPromocodes.README.md)
|
|
52
|
-
shows issued promo-code conditions and statuses and reports load and apply
|
|
53
|
-
requests to the host.
|
|
54
|
-
[`AccountActivity`](templates/components/orionplace/account/AccountActivity.README.md)
|
|
55
|
-
switches between promo codes, the customer's reviews and sign-in history while
|
|
56
|
-
mounting only the active section.
|
|
57
|
-
[`AddressForm`](templates/components/orionplace/address/AddressForm.README.md)
|
|
58
|
-
creates or edits a delivery address with region search, address suggestions and
|
|
59
|
-
optional coordinate lookup.
|
|
60
|
-
[`StorefrontAddresses`](templates/components/orionplace/address/StorefrontAddresses.README.md)
|
|
61
|
-
loads saved addresses and provides add, edit, default and confirmed delete
|
|
62
|
-
actions.
|
|
63
|
-
[`BlogPostCard`](templates/components/orionplace/blog/BlogPostCard.README.md)
|
|
64
|
-
renders a localized article preview as a link or selection button.
|
|
65
|
-
[`BlogPostGroup`](templates/components/orionplace/blog/BlogPostGroup.README.md)
|
|
66
|
-
arranges the browse store's article previews using the configured journal view.
|
|
67
|
-
[`BlogCommentForm`](templates/components/orionplace/blog/BlogCommentForm.README.md)
|
|
68
|
-
collects validated text for a new comment, reply or edit.
|
|
69
|
-
[`BlogComments`](templates/components/orionplace/blog/BlogComments.README.md)
|
|
70
|
-
renders nested article discussions, the customer's comments and all comment
|
|
71
|
-
write actions.
|
|
72
|
-
[`BlogArticle`](templates/components/orionplace/blog/BlogArticle.README.md)
|
|
73
|
-
renders an SSR-readable article, subscription access, safe editor HTML,
|
|
74
|
-
reading accounting, related product cards and its discussion.
|
|
75
|
-
[`StorefrontJournal`](templates/components/orionplace/blog/StorefrontJournal.README.md)
|
|
76
|
-
renders the SSR-ready article list with category and author filters,
|
|
77
|
-
pagination, linked cards and an optional inline article.
|
|
78
|
-
[`CartCheckoutDetails`](templates/components/orionplace/cart/CartCheckoutDetails.README.md)
|
|
79
|
-
connects the confirmed cart address, payment, recipient and comment fields to
|
|
80
|
-
their typed mutation actions.
|
|
81
|
-
[`MainSearch`](templates/components/orionplace/search/MainSearch.README.md)
|
|
82
|
-
connects the localized search field and hints to the separate browse store.
|
|
83
|
-
The first storefront data components are
|
|
84
|
-
[`BrandLogos`](templates/components/orionplace/branding/BrandLogos.README.md)
|
|
85
|
-
and [`SocialLinks`](templates/components/orionplace/social/SocialLinks.README.md).
|
|
86
|
-
[`MainBanner`](templates/components/orionplace/banners/MainBanner.README.md)
|
|
87
|
-
renders localized banner images with manual navigation and category actions.
|
|
88
|
-
[`EventsBlock`](templates/components/orionplace/events/EventsBlock.README.md)
|
|
89
|
-
renders page events for the selected language and reports the selected event.
|
|
90
|
-
[`CategoryGroup`](templates/components/orionplace/categories/CategoryGroup.README.md)
|
|
91
|
-
renders ordered public categories or one parent's immediate public children.
|
|
92
|
-
[`StorefrontHome`](templates/components/orionplace/storefront/StorefrontHome.README.md)
|
|
93
|
-
assembles banners, categories, product sections and page events in storefront
|
|
94
|
-
order.
|
|
95
|
-
[`CatalogRepresentative`](templates/components/orionplace/catalog/CatalogRepresentative.README.md)
|
|
96
|
-
adapts the verified compact product card for category highlights.
|
|
97
|
-
[`CatalogHeader`](templates/components/orionplace/catalog/CatalogHeader.README.md)
|
|
98
|
-
provides the focus-managed category panel, shortcuts, trails and representative
|
|
99
|
-
product actions used by the storefront header.
|
|
100
|
-
[`StorefrontCartButton`](templates/components/orionplace/storefront/StorefrontCartButton.README.md)
|
|
101
|
-
shows the shared cart-position count and reports a cart selection.
|
|
102
|
-
[`StorefrontWishlistButton`](templates/components/orionplace/storefront/StorefrontWishlistButton.README.md)
|
|
103
|
-
shows the shared wishlist count with plain-click application handling and
|
|
104
|
-
native modifier-click navigation.
|
|
105
|
-
[`WalletBalance`](templates/components/orionplace/wallet/WalletBalance.README.md)
|
|
106
|
-
formats the selected currency's Application Load wallet amount in full or
|
|
107
|
-
compact form.
|
|
108
|
-
[`StorefrontAccount`](templates/components/orionplace/storefront/StorefrontAccount.README.md)
|
|
109
|
-
provides the guest login action and the authenticated profile, wallet and logout
|
|
110
|
-
menu with keyboard navigation.
|
|
111
|
-
[`StorefrontFooter`](templates/components/orionplace/storefront/StorefrontFooter.README.md)
|
|
112
|
-
combines branding, social links, public categories and locale-preserving
|
|
113
|
-
storefront links.
|
|
114
|
-
[`StorefrontHeader`](templates/components/orionplace/storefront/StorefrontHeader.README.md)
|
|
115
|
-
assembles search, selections, account actions and the interactive catalog with
|
|
116
|
-
localized navigation.
|
|
117
|
-
[`OrionplaceThemeProvider`](templates/components/orionplace/theme/OrionplaceThemeProvider.README.md)
|
|
118
|
-
sets their shared color tokens.
|
|
119
|
-
[`MainCurrencySwitcher`](templates/components/orionplace/currencies/MainCurrencySwitcher.README.md)
|
|
120
|
-
[`UnitSwitcher`](templates/components/orionplace/units/UnitSwitcher.README.md)
|
|
121
|
-
and [`MainLanguageSwitcher`](templates/components/orionplace/locale/MainLanguageSwitcher.README.md)
|
|
122
|
-
use the separate current-selection store.
|
|
123
|
-
[`HoverImageViewer`](templates/components/orionplace/products/HoverImageViewer.README.md)
|
|
124
|
-
shows a product's first image in server HTML and starts loading its other
|
|
125
|
-
images after hover or keyboard focus.
|
|
126
|
-
The first product-card parts cover the
|
|
127
|
-
[`title`](templates/components/orionplace/products/ProductCardTitle.README.md),
|
|
128
|
-
[`description`](templates/components/orionplace/products/ProductCardDescription.README.md),
|
|
129
|
-
[`price`](templates/components/orionplace/products/ProductCardPrice.README.md),
|
|
130
|
-
[`stock notice`](templates/components/orionplace/products/ProductCardStockNotice.README.md),
|
|
131
|
-
[`review rating`](templates/components/orionplace/products/ProductCardRating.README.md),
|
|
132
|
-
[`seller`](templates/components/orionplace/products/ProductCardSeller.README.md),
|
|
133
|
-
[`badges`](templates/components/orionplace/products/ProductCardBadges.README.md),
|
|
134
|
-
[`favorite`](templates/components/orionplace/products/ProductCardFavorite.README.md),
|
|
135
|
-
[`purchase actions`](templates/components/orionplace/products/ProductCardActions.README.md),
|
|
136
|
-
[`variant choices`](templates/components/orionplace/products/ProductCardVariants.README.md)
|
|
137
|
-
and the [`secondary-choice dialog`](templates/components/orionplace/products/ProductSecondaryVariantModal.README.md).
|
|
138
|
-
[`ProductCard`](templates/components/orionplace/products/ProductCard.README.md)
|
|
139
|
-
assembles these parts and delays each card's secondary images until hover
|
|
140
|
-
or keyboard focus. [`ProductGroup`](templates/components/orionplace/products/ProductGroup.README.md)
|
|
141
|
-
renders responsive rows of cards.
|
|
142
|
-
[`ProductReviewImageViewer`](templates/components/orionplace/products/ProductReviewImageViewer.README.md)
|
|
143
|
-
opens review photos with keyboard navigation.
|
|
144
|
-
[`ProductReviewForm`](templates/components/orionplace/products/ProductReviewForm.README.md)
|
|
145
|
-
collects validated review, reply and edit values.
|
|
146
|
-
[`ProductReviewGallery`](templates/components/orionplace/products/ProductReviewGallery.README.md)
|
|
147
|
-
loads scoped review photos and reports the owning review selection.
|
|
148
|
-
[`ProductReviews`](templates/components/orionplace/products/ProductReviews.README.md)
|
|
149
|
-
assembles review statistics, filters, photos, replies, reactions and writing
|
|
150
|
-
controls for product, seller and account views.
|
|
151
|
-
Their contracts are documented in English and Russian beside the maintained
|
|
152
|
-
React source.
|
|
3
|
+
**A complete Next.js 16 storefront for your Orionplace marketplace, set up with one command.**
|
|
4
|
+
|
|
5
|
+
`orionplace-react` creates a server-rendered shop connected to your personal
|
|
6
|
+
marketplace API: catalog and search, product pages, cart and checkout, customer
|
|
7
|
+
account, journal and support, in English and Russian. The package includes a
|
|
8
|
+
setup CLI and the full storefront source, so every page and component in your
|
|
9
|
+
project is yours to change.
|
|
10
|
+
|
|
11
|
+
- **Setup CLI:** `init`, `set backend`, `add frontend`, `set mimic` and `update`.
|
|
12
|
+
- **Server install:** `install` builds the storefront on your Linux server and
|
|
13
|
+
runs it as a system service behind nginx with HTTPS.
|
|
14
|
+
- **API gateway:** the browser calls `/api/...` on your own domain; the server
|
|
15
|
+
forwards it to your marketplace backend with cookies bound to the storefront.
|
|
16
|
+
- **Typed store:** one Application Load request fills it before the first render.
|
|
17
|
+
- **124 storefront components**, each with an English and Russian README
|
|
18
|
+
([list](#components)).
|
|
19
|
+
- **Several storefront domains** on one backend, each with its own mapping.
|
|
20
|
+
- **Android app** from the same project with Capacitor 8.
|
|
21
|
+
|
|
22
|
+
**Contents:** [Quick start](#quick-start-from-zero) ·
|
|
23
|
+
[Existing project](#existing-nextjs-16-project) · [Components](#components) ·
|
|
24
|
+
[Commands](#commands) · [Server install](#install-on-your-server) ·
|
|
25
|
+
[Android app](#android-app) · [Update](#update-an-existing-project)
|
|
153
26
|
|
|
154
27
|
Requires Node.js 20.9+ and a Node server deployment. Static export and edge-only
|
|
155
28
|
hosting are not supported by this filesystem-based integration; only the Android
|
|
@@ -251,6 +124,188 @@ these mappings for new projects and updates unchanged generated configuration.
|
|
|
251
124
|
If you already own `tsconfig.json`, it is preserved; merge these aliases before
|
|
252
125
|
starting the application. Imports then work at any component directory depth.
|
|
253
126
|
|
|
127
|
+
<!-- components:start (generated by scripts/generate-package-readme-components.mjs) -->
|
|
128
|
+
## Components
|
|
129
|
+
|
|
130
|
+
The starter includes **124 storefront components**, copied into
|
|
131
|
+
`components/orionplace/` of your project. Each one has a README in English and Russian
|
|
132
|
+
next to its source that describes its inputs, data and actions.
|
|
133
|
+
|
|
134
|
+
### Storefront structure
|
|
135
|
+
|
|
136
|
+
Main pages and the shared storefront frame.
|
|
137
|
+
|
|
138
|
+
| Component | Name | What it does |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| **Header** | `StorefrontHeader` | The storefront header and its search, catalog, manufacturer and journal links, locale, currency, account, wishlist and cart controls. |
|
|
141
|
+
| **Storefront logo** | `BrandLogos` | Displays one logo from store.init.logos. |
|
|
142
|
+
| **Product search** | `MainSearch` | A search input with suggestions in the storefront language. |
|
|
143
|
+
| **Language selector** | `MainLanguageSwitcher` | Selects the storefront language and its associated currency and measurement units. |
|
|
144
|
+
| **Currency selector** | `MainCurrencySwitcher` | Selects the current storefront currency from the available options. |
|
|
145
|
+
| **Storefront account** | `StorefrontAccount` | Guest sign-in button and signed-in account menu with profile and wallet actions. |
|
|
146
|
+
| **Wallet balance** | `WalletBalance` | Displays the wallet balance in the selected storefront currency. |
|
|
147
|
+
| **Sign-out page** | `StorefrontLogout` | Session sign-out, failure retry and return to the storefront. |
|
|
148
|
+
| **Wishlist button** | `StorefrontWishlistButton` | Displays the wishlist count and links to the wishlist page. |
|
|
149
|
+
| **Cart button** | `StorefrontCartButton` | Displays the cart count and emits a request to open the cart page. |
|
|
150
|
+
| **Cart preview** | `CartPreview` | A compact dropdown under the header's cart button with cart products, quantities and amounts in the selected currency. |
|
|
151
|
+
| **Catalog menu** | `CatalogHeader` | Category navigation with nested sections and representative product cards. |
|
|
152
|
+
| **Catalog representative** | `CatalogRepresentative` | A compact product card used in the catalog menu, with the same navigation, variant, wishlist and purchase actions. |
|
|
153
|
+
| **Storefront home** | `StorefrontHome` | Banners, the optional Popular categories block, product sections, storefront events and recommendations. |
|
|
154
|
+
| **Storefront banners** | `MainBanner` | A carousel of banner images for the current storefront language, with responsive image formats. |
|
|
155
|
+
| **Popular categories block** | `CategoryGroup` | Optional home-page block of public category tiles. |
|
|
156
|
+
| **Product group** | `ProductGroup` | Displays a collection of product cards with images, prices, variant selection and purchase controls. |
|
|
157
|
+
| **Product card** | `ProductCard` | Combines a product’s image, name, price, variants, wishlist button and purchase controls in one card. |
|
|
158
|
+
| **Product photo viewer** | `HoverImageViewer` | A separate product photo viewer with dot buttons: it switches photos with the pointer, with a swipe and from the keyboard, and a click passes the chosen photo's address to the application. |
|
|
159
|
+
| **Product card title** | `ProductCardTitle` | Displays the localized product name in the card. |
|
|
160
|
+
| **Product card description** | `ProductCardDescription` | Displays the localized short product description as plain text, with HTML tags removed. |
|
|
161
|
+
| **Product card badges** | `ProductCardBadges` | Marks new, popular, good-deal and low-stock products with badges in the card. |
|
|
162
|
+
| **Product card wishlist button** | `ProductCardFavorite` | Requests adding or removing the product from the wishlist; the supplied storefront sends guests to sign-in. |
|
|
163
|
+
| **Seller in the product card** | `ProductCardSeller` | Displays the product seller’s name and rating in the card. |
|
|
164
|
+
| **Product card rating** | `ProductCardRating` | Displays the product’s average rating and review count in the card. |
|
|
165
|
+
| **Product card variants** | `ProductCardVariants` | Selects a primary or secondary product variant. |
|
|
166
|
+
| **Secondary variant selector** | `ProductSecondaryVariantModal` | Shows primary and secondary product choices in a modal and can continue the selected purchase action. |
|
|
167
|
+
| **Product card price** | `ProductCardPrice` | Displays the price in the selected currency and the original price when the product is discounted. |
|
|
168
|
+
| **Low-stock notice** | `ProductCardStockNotice` | Warns that the product will soon run out of stock. |
|
|
169
|
+
| **Product card purchase controls** | `ProductCardActions` | Product-card controls for the cart, quantity, availability notifications and preorder when available. |
|
|
170
|
+
| **Preorder dialog** | `ProductPreorderDialog` | The selected product and contact details for a preorder request. |
|
|
171
|
+
| **Storefront events** | `EventsBlock` | A full-width carousel of layout-event images for the current storefront language, with responsive image formats. |
|
|
172
|
+
| **Event block and promotion enrollment** | `EventEnrollmentDialog` | Displaying the event block and opening promotion enrollment from the selected event. |
|
|
173
|
+
| **Footer** | `StorefrontFooter` | Store identity, navigation, a contact-page link and social links. |
|
|
174
|
+
| **Social links** | `SocialLinks` | Display the store’s published social links and icons. |
|
|
175
|
+
| **Error page** | `StorefrontErrorPage` | Page-not-found (404) and failure (500) page with Home, Catalog and Retry actions. |
|
|
176
|
+
| **Storefront theme** | `OrionplaceThemeProvider` | Applies the selected light or dark storefront theme and optional color overrides to its content. |
|
|
177
|
+
|
|
178
|
+
### Catalog and discovery
|
|
179
|
+
|
|
180
|
+
Browse categories, search results and product sources.
|
|
181
|
+
|
|
182
|
+
| Component | Name | What it does |
|
|
183
|
+
| --- | --- | --- |
|
|
184
|
+
| **Catalog component** | `StorefrontCatalog` | The component shows store categories, subcategories and product links. |
|
|
185
|
+
| **Category page** | `StorefrontCategory` | A category with its breadcrumb path, subcategories, filters and paginated product cards. |
|
|
186
|
+
| **Search results** | `ProductSearchResults` | Filters, products, loading and empty states for search, category, seller and recommendation results. |
|
|
187
|
+
| **Search filters** | `SearchFilters` | Price, discount, rating, review, category, seller, manufacturer and variant facets with selectable values and optional result counts. |
|
|
188
|
+
| **Sellers** | `StorefrontSellers` | Seller directory with filters and sorting, and a seller profile with its products. |
|
|
189
|
+
| **Seller filters** | `SellerFilters` | Seller name, minimum rating and category filters for the seller directory. |
|
|
190
|
+
| **Seller summary** | `SellerSummary` | Seller logo, name, rating, selling-since badge, product count and description. |
|
|
191
|
+
| **Seller photos** | `SellerPhotoGallery` | The seller’s approved photos as a square grid with a photo viewer. |
|
|
192
|
+
| **Stores** | `StorefrontStores` | Store addresses, map, phone numbers, opening hours and products. |
|
|
193
|
+
| **Manufacturers and their products** | `StorefrontManufacturers` | Alphabetical index, letter-based lists and manufacturer details. |
|
|
194
|
+
| **Manufacturer index** | `ManufacturerIndex` | Manufacturer logos and letter groups. |
|
|
195
|
+
|
|
196
|
+
### Products and shopping
|
|
197
|
+
|
|
198
|
+
Product details, saved items and the cart.
|
|
199
|
+
|
|
200
|
+
| Component | Name | What it does |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| **Product details** | `StorefrontProduct` | Loads and displays a published product or a secret-key preview with images, purchase choices, details and reviews. |
|
|
203
|
+
| **Page path** | `OrionplaceBreadcrumbs` | Linked path from the storefront home page through parent categories to the current page. |
|
|
204
|
+
| **Product characteristics** | `ProductSpecificationsDialog` | Every characteristic of the product in a dialog over the page. |
|
|
205
|
+
| **Product reviews** | `ProductReviews` | Reviews, replies, ratings and the user’s submissions. |
|
|
206
|
+
| **Review form** | `ProductReviewForm` | Rating, text and photos for a review or reply. |
|
|
207
|
+
| **Customer review photos** | `ProductReviewGallery` | Review photos for the selected product. |
|
|
208
|
+
| **Review photo viewer** | `ProductReviewImageViewer` | The photos of one review, shown large on the same page. |
|
|
209
|
+
| **Promo code products** | `PromoProducts` | The products a promo code offer applies to, with the offer’s conditions. |
|
|
210
|
+
| **Wishlist and availability notices** | `StorefrontWishlist` | Saved products and products with availability notifications, shown as standard product cards. |
|
|
211
|
+
| **Cart and checkout** | `StorefrontCart` | Cart items, promo codes, delivery, payment and order creation. |
|
|
212
|
+
| **Cart item** | `CartLineItem` | Item name, image, price, quantity and removal. |
|
|
213
|
+
| **Cart item options** | `CartItemOptions` | Option products as product cards, with text and file attachment for an item. |
|
|
214
|
+
| **Checkout details** | `CartCheckoutDetails` | Delivery address, payment method, recipient and comment. |
|
|
215
|
+
| **Shipment delivery selection** | `CartDeliverySelector` | Delivery methods with icons, pickup points and their map for one shipment. |
|
|
216
|
+
| **Delivery map** | `DeliveryMap` | Selectable locations on Yandex Maps for delivery pickup and the store directory. |
|
|
217
|
+
|
|
218
|
+
### Customer account
|
|
219
|
+
|
|
220
|
+
Profile, orders, addresses and account activity.
|
|
221
|
+
|
|
222
|
+
| Component | Name | What it does |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| **Account sections** | `AccountSection` | Account navigation and sign-in for private storefront sections. |
|
|
225
|
+
| **Personal profile** | `StorefrontProfile` | Account overview and forms for personal details, verified email, notification preferences, avatar, password and account deletion. |
|
|
226
|
+
| **Profile photo** | `ProfileAvatar` | Upload or remove the current account’s avatar. |
|
|
227
|
+
| **Personal details** | `ProfileDetailsForm` | Edit the account holder’s name and date of birth. |
|
|
228
|
+
| **Email and notifications** | `ProfileEmailSettings` | Verify an email address and choose optional email-notification and email-language preferences. |
|
|
229
|
+
| **Account password** | `ProfilePasswordForm` | Create or change the account password. |
|
|
230
|
+
| **Password field** | `OrionplacePasswordInput` | Password input with a button that shows or hides the entered password. |
|
|
231
|
+
| **Delete account** | `ProfileDeleteAccount` | Delete the signed-in customer's own account. |
|
|
232
|
+
| **Account sign-in** | `ProfileSignIn` | Phone-based account sign-in and registration. |
|
|
233
|
+
| **User promo codes** | `AccountPromocodes` | Available promo codes and applying a selected code to the cart. |
|
|
234
|
+
| **My activity** | `AccountActivity` | The user’s promo codes, reviews and sign-in history. |
|
|
235
|
+
| **Sign-in history** | `SignInHistory` | The user’s sign-in dates, devices, browsers and IP addresses. |
|
|
236
|
+
| **User orders** | `StorefrontOrders` | Order history, purchased items, payment, cancellation and repeat. |
|
|
237
|
+
| **Order details** | `OrderDetails` | Order items, shipments, tracking, purchased content and attachments. |
|
|
238
|
+
| **Order payment selection** | `OrderPaymentForm` | Available payment-method selection for an existing order. |
|
|
239
|
+
| **User addresses** | `StorefrontAddresses` | Saved addresses, address editing and default delivery selection. |
|
|
240
|
+
| **Address form** | `AddressForm` | Address creation and editing with country selection, region search, text suggestions and location lookup. |
|
|
241
|
+
| **Value selector** | `OrionplaceSelect` | Selects one value from a labeled list, with keyboard controls, country flags and option icons. |
|
|
242
|
+
| **Search selector** | `OrionplaceSearchSelect` | Selects one value from options searched as the user types, with a loading spinner and retry. |
|
|
243
|
+
| **Wallet and payments** | `StorefrontWallet` | Payment history, balance top-ups and cancellation of unpaid invoices. |
|
|
244
|
+
| **Wallet top-up form** | `WalletTopUpForm` | Amount, currency and payment-method selection for a wallet top-up. |
|
|
245
|
+
| **Payment panel** | `PaymentPanel` | Opens the payment link and lets the user refresh its status. |
|
|
246
|
+
| **Pagination** | `StorefrontPagination` | Previous and next controls with the current page for a returned list. |
|
|
247
|
+
|
|
248
|
+
### Subscriptions and support
|
|
249
|
+
|
|
250
|
+
Plans, active subscriptions and support requests.
|
|
251
|
+
|
|
252
|
+
| Component | Name | What it does |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| **Subscription plans** | `StorefrontTariffs` | Plans grouped by subscription, with search, sign-in for guests and subscription purchase. |
|
|
255
|
+
| **Subscription plan details** | `StorefrontTariff` | One public plan with its media, description, features, periods, options and subscription purchase. |
|
|
256
|
+
| **Subscription plans** | `TariffCards` | Plan cards in a responsive grid. |
|
|
257
|
+
| **Subscription plan card** | `TariffCard` | A plan card with its image, trial badge, name, description, the modules and limits of the plan, the option chooser, period prices and subscribe actions. |
|
|
258
|
+
| **Plan image** | `TariffCardImage` | The first small plan image. |
|
|
259
|
+
| **Plan name** | `TariffCardTitle` | The tariff name in its card, in the selected language. |
|
|
260
|
+
| **Plan description** | `TariffCardDescription` | The short plan description without HTML tags, and the full description under Details. |
|
|
261
|
+
| **Plan modules, limits and options** | `TariffCardFeatures` | The modules and limits of a plan from `tariff_variables_json`, each with its explanation, and the chooser of the plan options. |
|
|
262
|
+
| **Information hint** | `OrionplaceInfoHint` | Small information button that explains the term beside it. |
|
|
263
|
+
| **Plan prices** | `TariffCardPrices` | The period switcher, the price for the chosen period and the Subscribe and Try actions. |
|
|
264
|
+
| **Plan purchase** | `TariffSubscriptionModal` | Select an option, enter a promotion code and confirm a subscription purchase. |
|
|
265
|
+
| **My subscriptions** | `StorefrontSubscriptions` | Subscription dates, plan, usage, service details, automatic renewal, renewal payment and support. |
|
|
266
|
+
| **Renewal payment** | `SubscriptionExtendDialog` | The period, the promo code and the amount for extending a subscription. |
|
|
267
|
+
| **Automatic renewal** | `SubscriptionRenewalDialog` | The period that is paid automatically, or turning it off. |
|
|
268
|
+
| **Plan and option change** | `SubscriptionChangeDialog` | The new plan or service option, its period and the amount that is charged. |
|
|
269
|
+
| **Subscription cancellation** | `SubscriptionCancelDialog` | The confirmation that stops automatic renewal and keeps the paid period. |
|
|
270
|
+
| **Plan and usage** | `SubscriptionPlanSummary` | The plan picture, description and parameters with the usage the subscription service reported. |
|
|
271
|
+
| **Subscription service details** | `SubscriptionRemoteDetails` | The identifier, confirmation time and host a subscription service returned. |
|
|
272
|
+
| **Support conversations** | `StorefrontSupport` | Customer support conversations for general questions, sellers, order items and subscriptions. |
|
|
273
|
+
| **Conversation messages** | `TicketConversation` | Messages, attachments, conversation closing and support rating. |
|
|
274
|
+
| **Conversation item** | `TicketItemCard` | The purchased item a conversation is about, with its image, variant or option, quantity and paid amount. |
|
|
275
|
+
| **Subscription of a conversation** | `TicketSubscriptionCard` | The subscription a support conversation is about: plan, number, state, paid-until date and automatic renewal. |
|
|
276
|
+
| **Conversation image** | `TicketImage` | Loading and viewing an image from the user’s conversation. |
|
|
277
|
+
| **Support message form** | `TicketMessageForm` | Message text and images for a new conversation or reply. |
|
|
278
|
+
|
|
279
|
+
### Content and contact
|
|
280
|
+
|
|
281
|
+
Journal content and customer contact flows.
|
|
282
|
+
|
|
283
|
+
| Component | Name | What it does |
|
|
284
|
+
| --- | --- | --- |
|
|
285
|
+
| **Journal** | `StorefrontJournal` | Paged journal of store articles with category and author filters. |
|
|
286
|
+
| **Journal article** | `BlogArticle` | An article, subscription access and its discussion. |
|
|
287
|
+
| **Article comments** | `BlogComments` | Discussion, replies and the user’s own comments. |
|
|
288
|
+
| **Comment form** | `BlogCommentForm` | Text for a new comment, reply or edit. |
|
|
289
|
+
| **Blog posts** | `BlogPostGroup` | Publication previews in a grid, list or layout with a lead card. |
|
|
290
|
+
| **Blog post card** | `BlogPostCard` | Publication image, localized title, plain-text preview and reading time. |
|
|
291
|
+
| **Contact page** | `StorefrontContact` | Company contact and registration details, with a form for messaging the store. |
|
|
292
|
+
| **Contact details** | `AboutPage` | Display the store’s published company and contact information. |
|
|
293
|
+
| **Contact form** | `ContactForm` | Sending a question to the store. |
|
|
294
|
+
| **Unsubscribe from emails** | `EmailUnsubscribe` | Confirming an unsubscribe request from an email link. |
|
|
295
|
+
|
|
296
|
+
### Shared interface
|
|
297
|
+
|
|
298
|
+
Small reusable states and identity elements.
|
|
299
|
+
|
|
300
|
+
| Component | Name | What it does |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| **Empty state** | `OrionplaceEmptyState` | Line drawing and message shown when a list, history or search result has no items. |
|
|
303
|
+
| **Avatar** | `OrionplaceAvatar` | Round user photo with initials when no photo is available. |
|
|
304
|
+
| **Storefront image** | `OrionplaceImage` | Loads a safe public image lazily and shows an accessible placeholder when the image is missing or fails. |
|
|
305
|
+
| **Measurement unit selector** | `UnitSwitcher` | Selects a weight or length unit from those available in the storefront. |
|
|
306
|
+
|
|
307
|
+
<!-- components:end -->
|
|
308
|
+
|
|
254
309
|
## Commands
|
|
255
310
|
|
|
256
311
|
```bash
|
|
@@ -261,6 +316,7 @@ npx orionplace-react add frontend shop.example.com
|
|
|
261
316
|
npx orionplace-react add frontend www.shop.example.com
|
|
262
317
|
npx orionplace-react remove frontend www.shop.example.com
|
|
263
318
|
npx orionplace-react set mimic shop.example.com
|
|
319
|
+
sudo npx orionplace-react install
|
|
264
320
|
npx orionplace-react help
|
|
265
321
|
```
|
|
266
322
|
|
|
@@ -277,8 +333,38 @@ npm run orionplace:update
|
|
|
277
333
|
```
|
|
278
334
|
|
|
279
335
|
`npm set` belongs to npm's own configuration system; use the commands above.
|
|
280
|
-
|
|
281
|
-
change your account
|
|
336
|
+
The setup commands configure local integration files. They do not provision a
|
|
337
|
+
backend, change your account or create DNS records. `install` deploys the
|
|
338
|
+
storefront on your server; see [Install on your server](#install-on-your-server).
|
|
339
|
+
|
|
340
|
+
## Install on your server
|
|
341
|
+
|
|
342
|
+
Prepare a Linux server once with the OrionMarketplace server script: it installs
|
|
343
|
+
Node.js, nginx and certbot and adds the `orionplace` command
|
|
344
|
+
([server guide](https://orionmarket.place/en/sandbox/install)).
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
curl -fsSLo orionplace.sh https://orionmarket.place/downloads/orionplace.sh
|
|
348
|
+
sudo sh orionplace.sh setup
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Then create the storefront in `/srv/orionplace` and install it:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
cd /srv/orionplace
|
|
355
|
+
sudo npx --yes orionplace-react init shop
|
|
356
|
+
cd shop
|
|
357
|
+
sudo npm install
|
|
358
|
+
sudo npx orionplace-react install
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`install` asks for your backend and storefront domain when they are missing,
|
|
362
|
+
builds the project, runs it as the system service `orionplace-shop` on a free
|
|
363
|
+
local port and connects each storefront domain with nginx and a Let's Encrypt
|
|
364
|
+
certificate. Run it again after code or package updates; the service keeps its
|
|
365
|
+
port. Options: `--name <service>`, `--port <port>`, `--user <user>`,
|
|
366
|
+
`--no-domains`, `--yes`, `--dry-run`. Full guide:
|
|
367
|
+
[Install a storefront](https://orionmarket.place/en/sandbox/install/storefront?framework=react).
|
|
282
368
|
|
|
283
369
|
## Generated files
|
|
284
370
|
|
package/bin/orionplace-react.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setupProject } from '../src/setup.mjs';
|
|
3
3
|
import { updateProject } from '../src/update.mjs';
|
|
4
|
+
import { installService } from '../src/install.mjs';
|
|
4
5
|
import { readFile } from 'node:fs/promises';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -29,8 +30,16 @@ Commands for an existing project (run from its root):
|
|
|
29
30
|
npx orionplace-react set mimic <existing-frontend.example.com>
|
|
30
31
|
npx orionplace-react set frontend <shop.example.com>
|
|
31
32
|
|
|
33
|
+
On your Linux server (run from the project root):
|
|
34
|
+
sudo npx orionplace-react install
|
|
35
|
+
Builds the storefront, installs it as a system service (systemd or OpenRC)
|
|
36
|
+
and, when the orionplace server tool is set up, connects its domains with
|
|
37
|
+
nginx and HTTPS. Run it again after changes or an update to rebuild and restart.
|
|
38
|
+
Server tool: https://orionmarket.place/downloads/orionplace.sh
|
|
39
|
+
|
|
32
40
|
Options: --cwd <directory> --dry-run --help --version
|
|
33
41
|
Update option: --local (apply this CLI version instead of fetching the latest release)
|
|
42
|
+
Install options: --name <service> --port <port> --user <user> --no-domains --yes
|
|
34
43
|
|
|
35
44
|
Update downloads the latest release, refreshes unedited source, and installs dependencies.
|
|
36
45
|
It preserves edited source, domain mappings and MIMIC. --dry-run leaves project files unchanged.
|
|
@@ -53,10 +62,16 @@ try {
|
|
|
53
62
|
let cwd = process.cwd();
|
|
54
63
|
let dryRun = false;
|
|
55
64
|
let local = false;
|
|
65
|
+
const install = { domains: true, yes: false };
|
|
56
66
|
for (let index = 0; index < argv.length; index++) {
|
|
57
67
|
if (argv[index] === '--dry-run') dryRun = true;
|
|
58
68
|
else if (argv[index] === '--local') local = true;
|
|
59
|
-
else if (argv[index] === '--
|
|
69
|
+
else if (argv[index] === '--no-domains') install.domains = false;
|
|
70
|
+
else if (argv[index] === '--yes' || argv[index] === '-y') install.yes = true;
|
|
71
|
+
else if (['--name', '--port', '--user'].includes(argv[index])) {
|
|
72
|
+
if (!argv[index + 1]) throw new Error(`${argv[index]} needs a value`);
|
|
73
|
+
install[argv[index].slice(2)] = argv[++index];
|
|
74
|
+
} else if (argv[index] === '--cwd') {
|
|
60
75
|
if (!argv[index + 1]) throw new Error('--cwd needs a directory');
|
|
61
76
|
cwd = resolve(argv[++index]);
|
|
62
77
|
} else if (argv[index].startsWith('--')) throw new Error(`Unknown option: ${argv[index]}`);
|
|
@@ -64,8 +79,28 @@ try {
|
|
|
64
79
|
}
|
|
65
80
|
const [command, target] = positional;
|
|
66
81
|
if (local && command !== 'update') throw new Error('--local is only supported by update.');
|
|
82
|
+
if (command !== 'install' && (install.name || install.port || install.user || !install.domains || install.yes)) {
|
|
83
|
+
throw new Error('--name, --port, --user, --no-domains and --yes are install options.');
|
|
84
|
+
}
|
|
67
85
|
let result;
|
|
68
|
-
if (command === '
|
|
86
|
+
if (command === 'install' && positional.length === 1) {
|
|
87
|
+
const done = await installService({ cwd, dryRun, ...install });
|
|
88
|
+
if (done.dryRun) {
|
|
89
|
+
console.log(`Planned service orionplace-${done.name} on 127.0.0.1:${done.port} as ${done.user}`);
|
|
90
|
+
console.log(`Domains: ${done.frontends.join(', ') || 'none yet'} -> ${done.backend || 'no backend yet'}`);
|
|
91
|
+
console.log(`\n${done.unitPath}:\n${done.unit}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`\nInstalled orionplace-${done.name} (port ${done.port}, user ${done.user}).`);
|
|
94
|
+
for (const domain of done.frontends) {
|
|
95
|
+
if (!done.connected) console.log(` - ${domain}: not connected to nginx (see above)`);
|
|
96
|
+
else if (done.failed.includes(domain)) console.log(` ✗ ${domain}: not connected (see above)`);
|
|
97
|
+
else console.log(` ✓ https://${domain}`);
|
|
98
|
+
}
|
|
99
|
+
if (done.failed.length) console.log('Fix the problems above, then run: sudo orionplace domain add <domain>');
|
|
100
|
+
console.log(`Manage it with: orionplace app status ${done.name} | orionplace app logs ${done.name}`);
|
|
101
|
+
console.log('After code changes or npx orionplace-react update, run sudo npx orionplace-react install again.');
|
|
102
|
+
}
|
|
103
|
+
} else if (command === 'init' && positional.length <= 2) {
|
|
69
104
|
result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
|
|
70
105
|
} else if (command === 'update' && positional.length === 1) {
|
|
71
106
|
result = await updateProject({ cwd, dryRun, local });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Next.js 16 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and Russian, plus an Android app build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// Installs the built storefront as a system service on a Linux server and
|
|
2
|
+
// connects its domains through the "orionplace" server tool
|
|
3
|
+
// (https://orionmarket.place/downloads/orionplace.sh) when that tool is set up.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { request } from 'node:http';
|
|
8
|
+
import { createServer } from 'node:net';
|
|
9
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import { setupProject } from './setup.mjs';
|
|
12
|
+
|
|
13
|
+
export const framework = {
|
|
14
|
+
id: 'react',
|
|
15
|
+
title: 'Next.js',
|
|
16
|
+
dependency: 'next',
|
|
17
|
+
output: '.next/BUILD_ID',
|
|
18
|
+
start: port => ['node_modules/next/dist/bin/next', 'start', '--hostname', '127.0.0.1', '--port', String(port)],
|
|
19
|
+
environment: {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const defaultPaths = {
|
|
23
|
+
etc: process.env.ORIONPLACE_ETC || '/etc/orionplace',
|
|
24
|
+
systemd: '/etc/systemd/system',
|
|
25
|
+
openrc: '/etc/init.d',
|
|
26
|
+
logs: '/var/log/orionplace',
|
|
27
|
+
srv: '/srv/orionplace',
|
|
28
|
+
tool: ['/usr/local/bin/orionplace', '/usr/bin/orionplace'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SERVICE_USER = 'orionplace';
|
|
32
|
+
|
|
33
|
+
// run(command, args, { cwd, capture, env }) -> { code, output }
|
|
34
|
+
export function runCommand(command, args, { cwd, capture = false, env = process.env } = {}) {
|
|
35
|
+
return new Promise((done, reject) => {
|
|
36
|
+
const child = spawn(command, args, { cwd, env, stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit' });
|
|
37
|
+
let output = '';
|
|
38
|
+
if (capture) {
|
|
39
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
40
|
+
child.stderr.on('data', chunk => { output += chunk; });
|
|
41
|
+
}
|
|
42
|
+
child.once('error', reject);
|
|
43
|
+
child.once('exit', code => done({ code: code ?? 1, output }));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseConf(text) {
|
|
48
|
+
const values = {};
|
|
49
|
+
for (const line of text.split('\n')) {
|
|
50
|
+
const match = /^([A-Z_]+)=(.*)$/.exec(line);
|
|
51
|
+
if (match) values[match[1]] = match[2];
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readApps(paths = defaultPaths) {
|
|
57
|
+
const directory = join(paths.etc, 'apps');
|
|
58
|
+
let names = [];
|
|
59
|
+
try { names = (await readdir(directory)).filter(name => name.endsWith('.conf')); }
|
|
60
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
61
|
+
const apps = [];
|
|
62
|
+
for (const name of names) apps.push(parseConf(await readFile(join(directory, name), 'utf8')));
|
|
63
|
+
return apps;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function serviceName(value) {
|
|
67
|
+
const name = value.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
68
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error('Choose a service name with letters, digits and dashes: --name shop');
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function portIsFree(port) {
|
|
73
|
+
return new Promise(done => {
|
|
74
|
+
const server = createServer();
|
|
75
|
+
server.once('error', () => done(false));
|
|
76
|
+
server.listen({ port, host: '127.0.0.1', exclusive: true }, () => server.close(() => done(true)));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function choosePort(apps, wanted, isFree = portIsFree) {
|
|
81
|
+
const taken = new Set(apps.map(app => Number(app.PORT)));
|
|
82
|
+
if (wanted) {
|
|
83
|
+
if (!Number.isInteger(wanted) || wanted < 1024 || wanted > 65535) throw new Error('Use a port between 1024 and 65535.');
|
|
84
|
+
return wanted;
|
|
85
|
+
}
|
|
86
|
+
for (let port = 3000; port < 4000; port++) {
|
|
87
|
+
if (!taken.has(port) && await isFree(port)) return port;
|
|
88
|
+
}
|
|
89
|
+
throw new Error('No free port between 3000 and 3999. Choose one with --port.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function passwdEntry(user) {
|
|
93
|
+
const text = await readFile('/etc/passwd', 'utf8');
|
|
94
|
+
for (const line of text.split('\n')) {
|
|
95
|
+
const [name, , uid, gid, , home] = line.split(':');
|
|
96
|
+
if (name === user) return { name, uid: Number(uid), gid: Number(gid), home };
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function userByUid(uid) {
|
|
102
|
+
const text = await readFile('/etc/passwd', 'utf8');
|
|
103
|
+
for (const line of text.split('\n')) {
|
|
104
|
+
const [name, , id, gid, , home] = line.split(':');
|
|
105
|
+
if (Number(id) === uid && name) return { name, uid, gid: Number(gid), home };
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Every parent folder must let the service user through.
|
|
111
|
+
async function checkReachable(root, account) {
|
|
112
|
+
let cursor = dirname(root);
|
|
113
|
+
while (true) {
|
|
114
|
+
const info = await stat(cursor);
|
|
115
|
+
const open = info.uid === account.uid ? info.mode & 0o100 : info.gid === account.gid ? info.mode & 0o010 : info.mode & 0o001;
|
|
116
|
+
if (!open) {
|
|
117
|
+
throw new Error(`The folder ${cursor} is closed to the service user ${account.name}. Move the storefront to ${defaultPaths.srv}/ (for example ${defaultPaths.srv}/${basename(root)}) and run install there.`);
|
|
118
|
+
}
|
|
119
|
+
if (cursor === dirname(cursor)) return;
|
|
120
|
+
cursor = dirname(cursor);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function writeAtomic(path, content, mode = 0o644) {
|
|
125
|
+
await mkdir(dirname(path), { recursive: true });
|
|
126
|
+
const temporary = `${path}.orionplace-${process.pid}.tmp`;
|
|
127
|
+
await writeFile(temporary, content, { mode });
|
|
128
|
+
await rename(temporary, path);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function systemdUnit({ name, root, user, group, port, node, env }) {
|
|
132
|
+
const environment = Object.entries({ NODE_ENV: 'production', HOST: '127.0.0.1', PORT: String(port), SITEMAPS_DIR: join(root, 'sitemaps'), ...env })
|
|
133
|
+
.map(([key, value]) => `Environment=${key}=${value}`).join('\n');
|
|
134
|
+
const args = [...(existsSync(join(root, '.env')) ? ['--env-file=.env'] : []), ...framework.start(port)];
|
|
135
|
+
return `# Installed by "orionplace-${framework.id} install". Run it again after changes;
|
|
136
|
+
# manage with: orionplace app status|logs|restart ${name}
|
|
137
|
+
[Unit]
|
|
138
|
+
Description=OrionMarketplace storefront ${name} (${framework.title})
|
|
139
|
+
After=network-online.target
|
|
140
|
+
Wants=network-online.target
|
|
141
|
+
|
|
142
|
+
[Service]
|
|
143
|
+
Type=simple
|
|
144
|
+
User=${user}
|
|
145
|
+
Group=${group}
|
|
146
|
+
WorkingDirectory=${root}
|
|
147
|
+
${environment}
|
|
148
|
+
ExecStart=${node} ${args.join(' ')}
|
|
149
|
+
Restart=on-failure
|
|
150
|
+
RestartSec=3
|
|
151
|
+
LimitNOFILE=65535
|
|
152
|
+
NoNewPrivileges=true
|
|
153
|
+
PrivateTmp=true
|
|
154
|
+
ProtectSystem=full
|
|
155
|
+
|
|
156
|
+
[Install]
|
|
157
|
+
WantedBy=multi-user.target
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function openrcScript({ name, root, user, group, port, node, env, logs }) {
|
|
162
|
+
const environment = Object.entries({ NODE_ENV: 'production', HOST: '127.0.0.1', PORT: String(port), SITEMAPS_DIR: join(root, 'sitemaps'), ...env })
|
|
163
|
+
.map(([key, value]) => `export ${key}="${value}"`).join('\n');
|
|
164
|
+
const args = [...(existsSync(join(root, '.env')) ? ['--env-file=.env'] : []), ...framework.start(port)];
|
|
165
|
+
return `#!/sbin/openrc-run
|
|
166
|
+
# Installed by "orionplace-${framework.id} install". Run it again after changes;
|
|
167
|
+
# manage with: orionplace app status|logs|restart ${name}
|
|
168
|
+
name="orionplace-${name}"
|
|
169
|
+
description="OrionMarketplace storefront ${name} (${framework.title})"
|
|
170
|
+
supervisor=supervise-daemon
|
|
171
|
+
respawn_delay=3
|
|
172
|
+
command="${node}"
|
|
173
|
+
command_args="${args.join(' ')}"
|
|
174
|
+
command_user="${user}:${group}"
|
|
175
|
+
directory="${root}"
|
|
176
|
+
output_log="${logs}/${name}.log"
|
|
177
|
+
error_log="${logs}/${name}.log"
|
|
178
|
+
${environment}
|
|
179
|
+
|
|
180
|
+
depend() {
|
|
181
|
+
need net
|
|
182
|
+
after firewall
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
start_pre() {
|
|
186
|
+
checkpath --directory --owner ${user}:${group} --mode 0755 ${logs}
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function prompt(question) {
|
|
192
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
try { return (await terminal.question(question)).trim(); }
|
|
194
|
+
finally { terminal.close(); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function log(message) { console.log(message); }
|
|
198
|
+
|
|
199
|
+
// /usr/local/bin/node from the server tool follows Node.js updates; the
|
|
200
|
+
// versioned path of the running binary would not.
|
|
201
|
+
function stableNode() {
|
|
202
|
+
return ['/usr/local/bin/node', '/usr/bin/node'].find(path => existsSync(path)) || process.execPath;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fills in the backend and first storefront domain when the project has none.
|
|
206
|
+
async function ensureDomains(root, { yes }) {
|
|
207
|
+
const read = async () => JSON.parse(await readFile(join(root, 'orionplace/settings.json'), 'utf8'));
|
|
208
|
+
let settings = await read();
|
|
209
|
+
const ask = process.stdin.isTTY && !yes;
|
|
210
|
+
if (!settings.backend) {
|
|
211
|
+
if (!ask) throw new Error(`Set the personal backend first: npx orionplace-${framework.id} set backend my-shop.orionplace.app`);
|
|
212
|
+
log('Your personal backend is shown in your orionmarket.place profile, for example my-shop.orionplace.app.');
|
|
213
|
+
await setupProject({ cwd: root, field: 'backend', value: await prompt('Personal backend domain: ') });
|
|
214
|
+
settings = await read();
|
|
215
|
+
}
|
|
216
|
+
if (!settings.frontends?.length) {
|
|
217
|
+
if (!ask) throw new Error(`Add the storefront domain first: npx orionplace-${framework.id} add frontend shop.example.com`);
|
|
218
|
+
await setupProject({ cwd: root, action: 'add', field: 'frontend', value: await prompt('Storefront domain (for example shop.example.com): ') });
|
|
219
|
+
settings = await read();
|
|
220
|
+
}
|
|
221
|
+
return settings;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The storefront's home page as the public domain sees it -> HTTP status, 0 when down.
|
|
225
|
+
function homeStatus(port, host) {
|
|
226
|
+
return new Promise(done => {
|
|
227
|
+
const call = request({ host: '127.0.0.1', port, path: '/', headers: { host }, timeout: 15000 }, response => {
|
|
228
|
+
response.resume();
|
|
229
|
+
done(response.statusCode);
|
|
230
|
+
});
|
|
231
|
+
call.once('timeout', () => call.destroy());
|
|
232
|
+
call.once('error', () => done(0));
|
|
233
|
+
call.end();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function waitForHttp(port, host, seconds = 90) {
|
|
238
|
+
const until = Date.now() + seconds * 1000;
|
|
239
|
+
while (Date.now() < until) {
|
|
240
|
+
const status = await homeStatus(port, host);
|
|
241
|
+
if (status) return status;
|
|
242
|
+
await new Promise(done => setTimeout(done, 1000));
|
|
243
|
+
}
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function installService({
|
|
248
|
+
cwd = process.cwd(), name, port, user, build = true, domains = true, yes = false, dryRun = false,
|
|
249
|
+
paths = defaultPaths, run = runCommand, uid = process.getuid?.(), isFree = portIsFree,
|
|
250
|
+
systemd = existsSync('/run/systemd/system'), node = stableNode(),
|
|
251
|
+
} = {}) {
|
|
252
|
+
if (process.platform !== 'linux') throw new Error('install sets up a Linux server service. Run it on your VPS.');
|
|
253
|
+
if (uid !== 0 && !dryRun) throw new Error(`install needs root: sudo npx orionplace-${framework.id} install`);
|
|
254
|
+
const root = resolve(cwd);
|
|
255
|
+
if (!/^\/[A-Za-z0-9._/@+-]+$/.test(root)) throw new Error(`Use a storefront folder path without spaces or special characters (for example ${paths.srv}/shop), not ${root}.`);
|
|
256
|
+
let pkg;
|
|
257
|
+
try { pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); }
|
|
258
|
+
catch { throw new Error(`Run install in your storefront folder (created with: npx orionplace-${framework.id} init).`); }
|
|
259
|
+
if (!{ ...pkg.devDependencies, ...pkg.dependencies }[framework.dependency] || !existsSync(join(root, 'orionplace/settings.json'))) {
|
|
260
|
+
throw new Error(`This folder is not an Orionplace ${framework.title} storefront. Run install in the folder created with: npx orionplace-${framework.id} init`);
|
|
261
|
+
}
|
|
262
|
+
const openrc = !systemd && existsSync('/sbin/openrc-run');
|
|
263
|
+
if (!systemd && !openrc && !dryRun) throw new Error('Neither systemd nor OpenRC was found; start the storefront with your own process manager (see orionplace/README.md, Deploy on your VPS).');
|
|
264
|
+
|
|
265
|
+
const settings = dryRun ? JSON.parse(await readFile(join(root, 'orionplace/settings.json'), 'utf8')) : await ensureDomains(root, { yes });
|
|
266
|
+
const apps = await readApps(paths);
|
|
267
|
+
const previous = apps.find(app => app.DIR === root);
|
|
268
|
+
const service = serviceName(name || previous?.NAME || basename(root));
|
|
269
|
+
if (apps.some(app => app.NAME === service && app.DIR !== root)) {
|
|
270
|
+
throw new Error(`Another storefront already uses the name ${service} (${apps.find(app => app.NAME === service).DIR}). Choose one with --name.`);
|
|
271
|
+
}
|
|
272
|
+
const chosen = await choosePort(apps.filter(app => app.DIR !== root), port ? Number(port) : Number(previous?.PORT) || 0, isFree);
|
|
273
|
+
|
|
274
|
+
// The service runs as the folder's owner, or as "orionplace" for root-owned folders.
|
|
275
|
+
const owner = await stat(root);
|
|
276
|
+
let account = user ? await passwdEntry(user) : owner.uid === 0 ? await passwdEntry(SERVICE_USER) : await userByUid(owner.uid);
|
|
277
|
+
if (user && !account) throw new Error(`No system user ${user}.`);
|
|
278
|
+
if (!account && !dryRun) {
|
|
279
|
+
if (existsSync('/usr/sbin/useradd') || existsSync('/sbin/useradd')) {
|
|
280
|
+
await run('useradd', ['--system', '--home-dir', paths.srv, '--shell', '/sbin/nologin', '--user-group', SERVICE_USER], { capture: true });
|
|
281
|
+
} else {
|
|
282
|
+
await run('addgroup', ['-S', SERVICE_USER], { capture: true });
|
|
283
|
+
await run('adduser', ['-S', '-D', '-H', '-h', paths.srv, '-s', '/sbin/nologin', '-G', SERVICE_USER, SERVICE_USER], { capture: true });
|
|
284
|
+
}
|
|
285
|
+
account = await passwdEntry(SERVICE_USER);
|
|
286
|
+
if (!account) throw new Error(`Could not create the system user ${SERVICE_USER}.`);
|
|
287
|
+
}
|
|
288
|
+
account ||= { name: SERVICE_USER, uid: -1, gid: -1 };
|
|
289
|
+
const group = account.gid >= 0 ? (await readFile('/etc/group', 'utf8')).split('\n').map(line => line.split(':'))
|
|
290
|
+
.find(([, , gid]) => Number(gid) === account.gid)?.[0] || account.name : account.name;
|
|
291
|
+
if (account.uid >= 0) await checkReachable(root, account);
|
|
292
|
+
|
|
293
|
+
const values = { name: service, root, user: account.name, group, port: chosen, node, env: framework.environment, logs: paths.logs };
|
|
294
|
+
const unitPath = systemd ? join(paths.systemd, `orionplace-${service}.service`) : join(paths.openrc, `orionplace-${service}`);
|
|
295
|
+
const unit = systemd ? systemdUnit(values) : openrcScript(values);
|
|
296
|
+
const conf = [
|
|
297
|
+
`# Managed by orionplace-${framework.id} install. Use "orionplace app" commands to change it.`,
|
|
298
|
+
`NAME=${service}`, `FRAMEWORK=${framework.id}`, `DIR=${root}`, `PORT=${chosen}`,
|
|
299
|
+
`USER=${account.name}`, `NODE=${node}`, `SERVICE=orionplace-${service}`, '',
|
|
300
|
+
].join('\n');
|
|
301
|
+
if (dryRun) {
|
|
302
|
+
return { dryRun, name: service, port: chosen, user: account.name, unitPath, unit, conf, frontends: settings.frontends || [], backend: settings.backend };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
log(`\n==> Building the ${framework.title} storefront ${service}`);
|
|
306
|
+
// The website build; ORIONPLACE_APP_ORIGIN would make the Android app build.
|
|
307
|
+
const buildEnv = { ...process.env };
|
|
308
|
+
delete buildEnv.ORIONPLACE_APP_ORIGIN;
|
|
309
|
+
if (!existsSync(join(root, 'node_modules', framework.dependency))) {
|
|
310
|
+
const installed = await run('npm', ['install', '--no-audit', '--no-fund'], { cwd: root, env: buildEnv });
|
|
311
|
+
if (installed.code) throw new Error('npm install failed; see the messages above.');
|
|
312
|
+
}
|
|
313
|
+
if (build || !existsSync(join(root, framework.output))) {
|
|
314
|
+
const built = await run('npm', ['run', 'build'], { cwd: root, env: buildEnv });
|
|
315
|
+
if (built.code) throw new Error('The build failed; see the messages above. The running service (if any) was not changed.');
|
|
316
|
+
}
|
|
317
|
+
if (!existsSync(join(root, framework.output))) throw new Error(`The build did not produce ${framework.output}.`);
|
|
318
|
+
const owned = await run('chown', ['-R', `${account.uid}:${account.gid}`, root], { capture: true });
|
|
319
|
+
if (owned.code) throw new Error(`Could not give ${root} to ${account.name}:\n${owned.output}`);
|
|
320
|
+
|
|
321
|
+
log(`\n==> Installing the service orionplace-${service} on port ${chosen}`);
|
|
322
|
+
await writeAtomic(unitPath, unit, systemd ? 0o644 : 0o755);
|
|
323
|
+
await writeAtomic(join(paths.etc, 'apps', `${service}.conf`), conf);
|
|
324
|
+
const steps = systemd
|
|
325
|
+
? [['systemctl', ['daemon-reload']], ['systemctl', ['enable', `orionplace-${service}`]], ['systemctl', ['restart', `orionplace-${service}`]]]
|
|
326
|
+
: [['rc-update', ['add', `orionplace-${service}`, 'default']], ['rc-service', [`orionplace-${service}`, 'restart']]];
|
|
327
|
+
for (const [command, args] of steps) {
|
|
328
|
+
const result = await run(command, args, { capture: true });
|
|
329
|
+
if (result.code) throw new Error(`${command} ${args.join(' ')} failed:\n${result.output}`);
|
|
330
|
+
}
|
|
331
|
+
const status = await waitForHttp(chosen, settings.frontends[0]);
|
|
332
|
+
if (!status) {
|
|
333
|
+
const logs = systemd
|
|
334
|
+
? await run('journalctl', ['-u', `orionplace-${service}`, '-n', '40', '--no-pager'], { capture: true })
|
|
335
|
+
: await run('tail', ['-n', '40', join(paths.logs, `${service}.log`)], { capture: true });
|
|
336
|
+
throw new Error(`The storefront did not start on port ${chosen}. Last log lines:\n${logs.output}`);
|
|
337
|
+
}
|
|
338
|
+
log(` ✓ orionplace-${service} answers on 127.0.0.1:${chosen} (HTTP ${status})`);
|
|
339
|
+
|
|
340
|
+
const tool = paths.tool.find(path => existsSync(path));
|
|
341
|
+
const failed = [];
|
|
342
|
+
if (domains && tool) {
|
|
343
|
+
for (const domain of settings.frontends) {
|
|
344
|
+
log(`\n==> Connecting ${domain}`);
|
|
345
|
+
const backend = settings.backend.replace(/^https:\/\//, '').replace(/\/api$/, '');
|
|
346
|
+
const result = await run(tool, ['domain', 'add', domain, '--app', service, '--backend', backend, '--yes']);
|
|
347
|
+
if (result.code) failed.push(domain);
|
|
348
|
+
}
|
|
349
|
+
} else if (domains) {
|
|
350
|
+
log('\nThe orionplace server tool is not set up, so nginx and HTTPS were not configured. Prepare the server:');
|
|
351
|
+
log(' curl -fsSLo orionplace.sh https://orionmarket.place/downloads/orionplace.sh && sudo sh orionplace.sh setup');
|
|
352
|
+
log(`Then run this install again. Or proxy your HTTPS site to http://127.0.0.1:${chosen} as orionplace/README.md describes.`);
|
|
353
|
+
}
|
|
354
|
+
return { name: service, port: chosen, user: account.name, unitPath, frontends: settings.frontends, failed, connected: domains && Boolean(tool) };
|
|
355
|
+
}
|
package/src/project-readme.mjs
CHANGED
|
@@ -346,6 +346,13 @@ If installation fails after source generation, run \`npm install\` and retry.
|
|
|
346
346
|
|
|
347
347
|
## Deploy on your VPS
|
|
348
348
|
|
|
349
|
+
The quickest way: prepare the server with the OrionMarketplace server script,
|
|
350
|
+
then run \`sudo npx orionplace-react install\` in this folder. It builds the
|
|
351
|
+
project, runs it as a system service and connects your domains with nginx and
|
|
352
|
+
HTTPS. Steps: [Install a storefront](https://orionmarket.place/en/sandbox/install/storefront?framework=react).
|
|
353
|
+
|
|
354
|
+
To set the server up yourself instead:
|
|
355
|
+
|
|
349
356
|
This integration needs a Node.js server and its domain configuration files.
|
|
350
357
|
It does not support static export or edge-only hosting. Deploy the build,
|
|
351
358
|
dependencies, \`sitemaps\`, and your environment configuration together; use an
|