orionplace-nuxt 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 -5
- package/bin/orionplace-nuxt.mjs +37 -2
- package/package.json +1 -1
- package/src/install.mjs +355 -0
- package/src/project-readme.mjs +7 -0
- package/templates/app/utils/orionplace/api.ts +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# orionplace-nuxt
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**A complete Nuxt 4 storefront for your Orionplace marketplace, set up with one command.**
|
|
4
|
+
|
|
5
|
+
`orionplace-nuxt` 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 Nuxt module, 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-nuxt-4-project) · [Components](#components) ·
|
|
24
|
+
[Commands](#commands) · [Server install](#install-on-your-server) ·
|
|
25
|
+
[Android app](#android-app) · [Update](#update-an-existing-project)
|
|
6
26
|
|
|
7
27
|
Requires Node.js 22.12+ and a Node server deployment. Static-only and edge
|
|
8
28
|
hosting are not supported by this filesystem-based integration.
|
|
@@ -106,6 +126,188 @@ Use `@/` for application imports. Nuxt resolves it to `app/`, so an import such
|
|
|
106
126
|
as `@/stores/orionplace` works from any page, composable or component folder.
|
|
107
127
|
Framework imports such as `#imports` keep their own names.
|
|
108
128
|
|
|
129
|
+
<!-- components:start (generated by scripts/generate-package-readme-components.mjs) -->
|
|
130
|
+
## Components
|
|
131
|
+
|
|
132
|
+
The starter includes **124 storefront components**, copied into
|
|
133
|
+
`app/components/orionplace/` of your project. Each one has a README in English and Russian
|
|
134
|
+
next to its source that describes its inputs, data and actions.
|
|
135
|
+
|
|
136
|
+
### Storefront structure
|
|
137
|
+
|
|
138
|
+
Main pages and the shared storefront frame.
|
|
139
|
+
|
|
140
|
+
| Component | Name | What it does |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| **Header** | `StorefrontHeader` | The storefront header and its search, catalog, manufacturer and journal links, locale, currency, account, wishlist and cart controls. |
|
|
143
|
+
| **Storefront logo** | `BrandLogos` | Displays one logo from store.init.logos. |
|
|
144
|
+
| **Product search** | `MainSearch` | A search input with suggestions in the storefront language. |
|
|
145
|
+
| **Language selector** | `MainLanguageSwitcher` | Selects the storefront language and its associated currency and measurement units. |
|
|
146
|
+
| **Currency selector** | `MainCurrencySwitcher` | Selects the current storefront currency from the available options. |
|
|
147
|
+
| **Storefront account** | `StorefrontAccount` | Guest sign-in button and signed-in account menu with profile and wallet actions. |
|
|
148
|
+
| **Wallet balance** | `WalletBalance` | Displays the wallet balance in the selected storefront currency. |
|
|
149
|
+
| **Sign-out page** | `StorefrontLogout` | Session sign-out, failure retry and return to the storefront. |
|
|
150
|
+
| **Wishlist button** | `StorefrontWishlistButton` | Displays the wishlist count and links to the wishlist page. |
|
|
151
|
+
| **Cart button** | `StorefrontCartButton` | Displays the cart count and emits a request to open the cart page. |
|
|
152
|
+
| **Cart preview** | `CartPreview` | A compact dropdown under the header's cart button with cart products, quantities and amounts in the selected currency. |
|
|
153
|
+
| **Catalog menu** | `CatalogHeader` | Category navigation with nested sections and representative product cards. |
|
|
154
|
+
| **Catalog representative** | `CatalogRepresentative` | A compact product card used in the catalog menu, with the same navigation, variant, wishlist and purchase actions. |
|
|
155
|
+
| **Storefront home** | `StorefrontHome` | Banners, the optional Popular categories block, product sections, storefront events and recommendations. |
|
|
156
|
+
| **Storefront banners** | `MainBanner` | A carousel of banner images for the current storefront language, with responsive image formats. |
|
|
157
|
+
| **Popular categories block** | `CategoryGroup` | Optional home-page block of public category tiles. |
|
|
158
|
+
| **Product group** | `ProductGroup` | Displays a collection of product cards with images, prices, variant selection and purchase controls. |
|
|
159
|
+
| **Product card** | `ProductCard` | Combines a product’s image, name, price, variants, wishlist button and purchase controls in one card. |
|
|
160
|
+
| **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. |
|
|
161
|
+
| **Product card title** | `ProductCardTitle` | Displays the localized product name in the card. |
|
|
162
|
+
| **Product card description** | `ProductCardDescription` | Displays the localized short product description as plain text, with HTML tags removed. |
|
|
163
|
+
| **Product card badges** | `ProductCardBadges` | Marks new, popular, good-deal and low-stock products with badges in the card. |
|
|
164
|
+
| **Product card wishlist button** | `ProductCardFavorite` | Requests adding or removing the product from the wishlist; the supplied storefront sends guests to sign-in. |
|
|
165
|
+
| **Seller in the product card** | `ProductCardSeller` | Displays the product seller’s name and rating in the card. |
|
|
166
|
+
| **Product card rating** | `ProductCardRating` | Displays the product’s average rating and review count in the card. |
|
|
167
|
+
| **Product card variants** | `ProductCardVariants` | Selects a primary or secondary product variant. |
|
|
168
|
+
| **Secondary variant selector** | `ProductSecondaryVariantModal` | Shows primary and secondary product choices in a modal and can continue the selected purchase action. |
|
|
169
|
+
| **Product card price** | `ProductCardPrice` | Displays the price in the selected currency and the original price when the product is discounted. |
|
|
170
|
+
| **Low-stock notice** | `ProductCardStockNotice` | Warns that the product will soon run out of stock. |
|
|
171
|
+
| **Product card purchase controls** | `ProductCardActions` | Product-card controls for the cart, quantity, availability notifications and preorder when available. |
|
|
172
|
+
| **Preorder dialog** | `ProductPreorderDialog` | The selected product and contact details for a preorder request. |
|
|
173
|
+
| **Storefront events** | `EventsBlock` | A full-width carousel of layout-event images for the current storefront language, with responsive image formats. |
|
|
174
|
+
| **Event block and promotion enrollment** | `EventEnrollmentDialog` | Displaying the event block and opening promotion enrollment from the selected event. |
|
|
175
|
+
| **Footer** | `StorefrontFooter` | Store identity, navigation, a contact-page link and social links. |
|
|
176
|
+
| **Social links** | `SocialLinks` | Display the store’s published social links and icons. |
|
|
177
|
+
| **Error page** | `StorefrontErrorPage` | Page-not-found (404) and failure (500) page with Home, Catalog and Retry actions. |
|
|
178
|
+
| **Storefront theme** | `OrionplaceThemeProvider` | Applies the selected light or dark storefront theme and optional color overrides to its content. |
|
|
179
|
+
|
|
180
|
+
### Catalog and discovery
|
|
181
|
+
|
|
182
|
+
Browse categories, search results and product sources.
|
|
183
|
+
|
|
184
|
+
| Component | Name | What it does |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| **Catalog component** | `StorefrontCatalog` | The component shows store categories, subcategories and product links. |
|
|
187
|
+
| **Category page** | `StorefrontCategory` | A category with its breadcrumb path, subcategories, filters and paginated product cards. |
|
|
188
|
+
| **Search results** | `ProductSearchResults` | Filters, products, loading and empty states for search, category, seller and recommendation results. |
|
|
189
|
+
| **Search filters** | `SearchFilters` | Price, discount, rating, review, category, seller, manufacturer and variant facets with selectable values and optional result counts. |
|
|
190
|
+
| **Sellers** | `StorefrontSellers` | Seller directory with filters and sorting, and a seller profile with its products. |
|
|
191
|
+
| **Seller filters** | `SellerFilters` | Seller name, minimum rating and category filters for the seller directory. |
|
|
192
|
+
| **Seller summary** | `SellerSummary` | Seller logo, name, rating, selling-since badge, product count and description. |
|
|
193
|
+
| **Seller photos** | `SellerPhotoGallery` | The seller’s approved photos as a square grid with a photo viewer. |
|
|
194
|
+
| **Stores** | `StorefrontStores` | Store addresses, map, phone numbers, opening hours and products. |
|
|
195
|
+
| **Manufacturers and their products** | `StorefrontManufacturers` | Alphabetical index, letter-based lists and manufacturer details. |
|
|
196
|
+
| **Manufacturer index** | `ManufacturerIndex` | Manufacturer logos and letter groups. |
|
|
197
|
+
|
|
198
|
+
### Products and shopping
|
|
199
|
+
|
|
200
|
+
Product details, saved items and the cart.
|
|
201
|
+
|
|
202
|
+
| Component | Name | What it does |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| **Product details** | `StorefrontProduct` | Loads and displays a published product or a secret-key preview with images, purchase choices, details and reviews. |
|
|
205
|
+
| **Page path** | `OrionplaceBreadcrumbs` | Linked path from the storefront home page through parent categories to the current page. |
|
|
206
|
+
| **Product characteristics** | `ProductSpecificationsDialog` | Every characteristic of the product in a dialog over the page. |
|
|
207
|
+
| **Product reviews** | `ProductReviews` | Reviews, replies, ratings and the user’s submissions. |
|
|
208
|
+
| **Review form** | `ProductReviewForm` | Rating, text and photos for a review or reply. |
|
|
209
|
+
| **Customer review photos** | `ProductReviewGallery` | Review photos for the selected product. |
|
|
210
|
+
| **Review photo viewer** | `ProductReviewImageViewer` | The photos of one review, shown large on the same page. |
|
|
211
|
+
| **Promo code products** | `PromoProducts` | The products a promo code offer applies to, with the offer’s conditions. |
|
|
212
|
+
| **Wishlist and availability notices** | `StorefrontWishlist` | Saved products and products with availability notifications, shown as standard product cards. |
|
|
213
|
+
| **Cart and checkout** | `StorefrontCart` | Cart items, promo codes, delivery, payment and order creation. |
|
|
214
|
+
| **Cart item** | `CartLineItem` | Item name, image, price, quantity and removal. |
|
|
215
|
+
| **Cart item options** | `CartItemOptions` | Option products as product cards, with text and file attachment for an item. |
|
|
216
|
+
| **Checkout details** | `CartCheckoutDetails` | Delivery address, payment method, recipient and comment. |
|
|
217
|
+
| **Shipment delivery selection** | `CartDeliverySelector` | Delivery methods with icons, pickup points and their map for one shipment. |
|
|
218
|
+
| **Delivery map** | `DeliveryMap` | Selectable locations on Yandex Maps for delivery pickup and the store directory. |
|
|
219
|
+
|
|
220
|
+
### Customer account
|
|
221
|
+
|
|
222
|
+
Profile, orders, addresses and account activity.
|
|
223
|
+
|
|
224
|
+
| Component | Name | What it does |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| **Account sections** | `AccountSection` | Account navigation and sign-in for private storefront sections. |
|
|
227
|
+
| **Personal profile** | `StorefrontProfile` | Account overview and forms for personal details, verified email, notification preferences, avatar, password and account deletion. |
|
|
228
|
+
| **Profile photo** | `ProfileAvatar` | Upload or remove the current account’s avatar. |
|
|
229
|
+
| **Personal details** | `ProfileDetailsForm` | Edit the account holder’s name and date of birth. |
|
|
230
|
+
| **Email and notifications** | `ProfileEmailSettings` | Verify an email address and choose optional email-notification and email-language preferences. |
|
|
231
|
+
| **Account password** | `ProfilePasswordForm` | Create or change the account password. |
|
|
232
|
+
| **Password field** | `OrionplacePasswordInput` | Password input with a button that shows or hides the entered password. |
|
|
233
|
+
| **Delete account** | `ProfileDeleteAccount` | Delete the signed-in customer's own account. |
|
|
234
|
+
| **Account sign-in** | `ProfileSignIn` | Phone-based account sign-in and registration. |
|
|
235
|
+
| **User promo codes** | `AccountPromocodes` | Available promo codes and applying a selected code to the cart. |
|
|
236
|
+
| **My activity** | `AccountActivity` | The user’s promo codes, reviews and sign-in history. |
|
|
237
|
+
| **Sign-in history** | `SignInHistory` | The user’s sign-in dates, devices, browsers and IP addresses. |
|
|
238
|
+
| **User orders** | `StorefrontOrders` | Order history, purchased items, payment, cancellation and repeat. |
|
|
239
|
+
| **Order details** | `OrderDetails` | Order items, shipments, tracking, purchased content and attachments. |
|
|
240
|
+
| **Order payment selection** | `OrderPaymentForm` | Available payment-method selection for an existing order. |
|
|
241
|
+
| **User addresses** | `StorefrontAddresses` | Saved addresses, address editing and default delivery selection. |
|
|
242
|
+
| **Address form** | `AddressForm` | Address creation and editing with country selection, region search, text suggestions and location lookup. |
|
|
243
|
+
| **Value selector** | `OrionplaceSelect` | Selects one value from a labeled list, with keyboard controls, country flags and option icons. |
|
|
244
|
+
| **Search selector** | `OrionplaceSearchSelect` | Selects one value from options searched as the user types, with a loading spinner and retry. |
|
|
245
|
+
| **Wallet and payments** | `StorefrontWallet` | Payment history, balance top-ups and cancellation of unpaid invoices. |
|
|
246
|
+
| **Wallet top-up form** | `WalletTopUpForm` | Amount, currency and payment-method selection for a wallet top-up. |
|
|
247
|
+
| **Payment panel** | `PaymentPanel` | Opens the payment link and lets the user refresh its status. |
|
|
248
|
+
| **Pagination** | `StorefrontPagination` | Previous and next controls with the current page for a returned list. |
|
|
249
|
+
|
|
250
|
+
### Subscriptions and support
|
|
251
|
+
|
|
252
|
+
Plans, active subscriptions and support requests.
|
|
253
|
+
|
|
254
|
+
| Component | Name | What it does |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| **Subscription plans** | `StorefrontTariffs` | Plans grouped by subscription, with search, sign-in for guests and subscription purchase. |
|
|
257
|
+
| **Subscription plan details** | `StorefrontTariff` | One public plan with its media, description, features, periods, options and subscription purchase. |
|
|
258
|
+
| **Subscription plans** | `TariffCards` | Plan cards in a responsive grid. |
|
|
259
|
+
| **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. |
|
|
260
|
+
| **Plan image** | `TariffCardImage` | The first small plan image. |
|
|
261
|
+
| **Plan name** | `TariffCardTitle` | The tariff name in its card, in the selected language. |
|
|
262
|
+
| **Plan description** | `TariffCardDescription` | The short plan description without HTML tags, and the full description under Details. |
|
|
263
|
+
| **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. |
|
|
264
|
+
| **Information hint** | `OrionplaceInfoHint` | Small information button that explains the term beside it. |
|
|
265
|
+
| **Plan prices** | `TariffCardPrices` | The period switcher, the price for the chosen period and the Subscribe and Try actions. |
|
|
266
|
+
| **Plan purchase** | `TariffSubscriptionModal` | Select an option, enter a promotion code and confirm a subscription purchase. |
|
|
267
|
+
| **My subscriptions** | `StorefrontSubscriptions` | Subscription dates, plan, usage, service details, automatic renewal, renewal payment and support. |
|
|
268
|
+
| **Renewal payment** | `SubscriptionExtendDialog` | The period, the promo code and the amount for extending a subscription. |
|
|
269
|
+
| **Automatic renewal** | `SubscriptionRenewalDialog` | The period that is paid automatically, or turning it off. |
|
|
270
|
+
| **Plan and option change** | `SubscriptionChangeDialog` | The new plan or service option, its period and the amount that is charged. |
|
|
271
|
+
| **Subscription cancellation** | `SubscriptionCancelDialog` | The confirmation that stops automatic renewal and keeps the paid period. |
|
|
272
|
+
| **Plan and usage** | `SubscriptionPlanSummary` | The plan picture, description and parameters with the usage the subscription service reported. |
|
|
273
|
+
| **Subscription service details** | `SubscriptionRemoteDetails` | The identifier, confirmation time and host a subscription service returned. |
|
|
274
|
+
| **Support conversations** | `StorefrontSupport` | Customer support conversations for general questions, sellers, order items and subscriptions. |
|
|
275
|
+
| **Conversation messages** | `TicketConversation` | Messages, attachments, conversation closing and support rating. |
|
|
276
|
+
| **Conversation item** | `TicketItemCard` | The purchased item a conversation is about, with its image, variant or option, quantity and paid amount. |
|
|
277
|
+
| **Subscription of a conversation** | `TicketSubscriptionCard` | The subscription a support conversation is about: plan, number, state, paid-until date and automatic renewal. |
|
|
278
|
+
| **Conversation image** | `TicketImage` | Loading and viewing an image from the user’s conversation. |
|
|
279
|
+
| **Support message form** | `TicketMessageForm` | Message text and images for a new conversation or reply. |
|
|
280
|
+
|
|
281
|
+
### Content and contact
|
|
282
|
+
|
|
283
|
+
Journal content and customer contact flows.
|
|
284
|
+
|
|
285
|
+
| Component | Name | What it does |
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| **Journal** | `StorefrontJournal` | Paged journal of store articles with category and author filters. |
|
|
288
|
+
| **Journal article** | `BlogArticle` | An article, subscription access and its discussion. |
|
|
289
|
+
| **Article comments** | `BlogComments` | Discussion, replies and the user’s own comments. |
|
|
290
|
+
| **Comment form** | `BlogCommentForm` | Text for a new comment, reply or edit. |
|
|
291
|
+
| **Blog posts** | `BlogPostGroup` | Publication previews in a grid, list or layout with a lead card. |
|
|
292
|
+
| **Blog post card** | `BlogPostCard` | Publication image, localized title, plain-text preview and reading time. |
|
|
293
|
+
| **Contact page** | `StorefrontContact` | Company contact and registration details, with a form for messaging the store. |
|
|
294
|
+
| **Contact details** | `AboutPage` | Display the store’s published company and contact information. |
|
|
295
|
+
| **Contact form** | `ContactForm` | Sending a question to the store. |
|
|
296
|
+
| **Unsubscribe from emails** | `EmailUnsubscribe` | Confirming an unsubscribe request from an email link. |
|
|
297
|
+
|
|
298
|
+
### Shared interface
|
|
299
|
+
|
|
300
|
+
Small reusable states and identity elements.
|
|
301
|
+
|
|
302
|
+
| Component | Name | What it does |
|
|
303
|
+
| --- | --- | --- |
|
|
304
|
+
| **Empty state** | `OrionplaceEmptyState` | Line drawing and message shown when a list, history or search result has no items. |
|
|
305
|
+
| **Avatar** | `OrionplaceAvatar` | Round user photo with initials when no photo is available. |
|
|
306
|
+
| **Storefront image** | `OrionplaceImage` | Loads a safe public image lazily and shows an accessible placeholder when the image is missing or fails. |
|
|
307
|
+
| **Measurement unit selector** | `UnitSwitcher` | Selects a weight or length unit from those available in the storefront. |
|
|
308
|
+
|
|
309
|
+
<!-- components:end -->
|
|
310
|
+
|
|
109
311
|
## Commands
|
|
110
312
|
|
|
111
313
|
```bash
|
|
@@ -116,6 +318,7 @@ npx orionplace-nuxt add frontend shop.example.com
|
|
|
116
318
|
npx orionplace-nuxt add frontend www.shop.example.com
|
|
117
319
|
npx orionplace-nuxt remove frontend www.shop.example.com
|
|
118
320
|
npx orionplace-nuxt set mimic shop.example.com
|
|
321
|
+
sudo npx orionplace-nuxt install
|
|
119
322
|
npx orionplace-nuxt help
|
|
120
323
|
```
|
|
121
324
|
|
|
@@ -132,8 +335,38 @@ npm run orionplace:update
|
|
|
132
335
|
```
|
|
133
336
|
|
|
134
337
|
`npm set` belongs to npm's own configuration system; use the commands above.
|
|
135
|
-
|
|
136
|
-
change your account
|
|
338
|
+
The setup commands configure local integration files. They do not provision a
|
|
339
|
+
backend, change your account or create DNS records. `install` deploys the
|
|
340
|
+
storefront on your server; see [Install on your server](#install-on-your-server).
|
|
341
|
+
|
|
342
|
+
## Install on your server
|
|
343
|
+
|
|
344
|
+
Prepare a Linux server once with the OrionMarketplace server script: it installs
|
|
345
|
+
Node.js, nginx and certbot and adds the `orionplace` command
|
|
346
|
+
([server guide](https://orionmarket.place/en/sandbox/install)).
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
curl -fsSLo orionplace.sh https://orionmarket.place/downloads/orionplace.sh
|
|
350
|
+
sudo sh orionplace.sh setup
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Then create the storefront in `/srv/orionplace` and install it:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
cd /srv/orionplace
|
|
357
|
+
sudo npx --yes orionplace-nuxt init shop
|
|
358
|
+
cd shop
|
|
359
|
+
sudo npm install
|
|
360
|
+
sudo npx orionplace-nuxt install
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`install` asks for your backend and storefront domain when they are missing,
|
|
364
|
+
builds the project, runs it as the system service `orionplace-shop` on a free
|
|
365
|
+
local port and connects each storefront domain with nginx and a Let's Encrypt
|
|
366
|
+
certificate. Run it again after code or package updates; the service keeps its
|
|
367
|
+
port. Options: `--name <service>`, `--port <port>`, `--user <user>`,
|
|
368
|
+
`--no-domains`, `--yes`, `--dry-run`. Full guide:
|
|
369
|
+
[Install a storefront](https://orionmarket.place/en/sandbox/install/storefront).
|
|
137
370
|
|
|
138
371
|
## Generated files
|
|
139
372
|
|
package/bin/orionplace-nuxt.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-nuxt set mimic <existing-frontend.example.com>
|
|
30
31
|
npx orionplace-nuxt set frontend <shop.example.com>
|
|
31
32
|
|
|
33
|
+
On your Linux server (run from the project root):
|
|
34
|
+
sudo npx orionplace-nuxt 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.
|
|
@@ -52,10 +61,16 @@ try {
|
|
|
52
61
|
let cwd = process.cwd();
|
|
53
62
|
let dryRun = false;
|
|
54
63
|
let local = false;
|
|
64
|
+
const install = { domains: true, yes: false };
|
|
55
65
|
for (let index = 0; index < argv.length; index++) {
|
|
56
66
|
if (argv[index] === '--dry-run') dryRun = true;
|
|
57
67
|
else if (argv[index] === '--local') local = true;
|
|
58
|
-
else if (argv[index] === '--
|
|
68
|
+
else if (argv[index] === '--no-domains') install.domains = false;
|
|
69
|
+
else if (argv[index] === '--yes' || argv[index] === '-y') install.yes = true;
|
|
70
|
+
else if (['--name', '--port', '--user'].includes(argv[index])) {
|
|
71
|
+
if (!argv[index + 1]) throw new Error(`${argv[index]} needs a value`);
|
|
72
|
+
install[argv[index].slice(2)] = argv[++index];
|
|
73
|
+
} else if (argv[index] === '--cwd') {
|
|
59
74
|
if (!argv[index + 1]) throw new Error('--cwd needs a directory');
|
|
60
75
|
cwd = resolve(argv[++index]);
|
|
61
76
|
} else if (argv[index].startsWith('--')) throw new Error(`Unknown option: ${argv[index]}`);
|
|
@@ -63,8 +78,28 @@ try {
|
|
|
63
78
|
}
|
|
64
79
|
const [command, target] = positional;
|
|
65
80
|
if (local && command !== 'update') throw new Error('--local is only supported by update.');
|
|
81
|
+
if (command !== 'install' && (install.name || install.port || install.user || !install.domains || install.yes)) {
|
|
82
|
+
throw new Error('--name, --port, --user, --no-domains and --yes are install options.');
|
|
83
|
+
}
|
|
66
84
|
let result;
|
|
67
|
-
if (command === '
|
|
85
|
+
if (command === 'install' && positional.length === 1) {
|
|
86
|
+
const done = await installService({ cwd, dryRun, ...install });
|
|
87
|
+
if (done.dryRun) {
|
|
88
|
+
console.log(`Planned service orionplace-${done.name} on 127.0.0.1:${done.port} as ${done.user}`);
|
|
89
|
+
console.log(`Domains: ${done.frontends.join(', ') || 'none yet'} -> ${done.backend || 'no backend yet'}`);
|
|
90
|
+
console.log(`\n${done.unitPath}:\n${done.unit}`);
|
|
91
|
+
} else {
|
|
92
|
+
console.log(`\nInstalled orionplace-${done.name} (port ${done.port}, user ${done.user}).`);
|
|
93
|
+
for (const domain of done.frontends) {
|
|
94
|
+
if (!done.connected) console.log(` - ${domain}: not connected to nginx (see above)`);
|
|
95
|
+
else if (done.failed.includes(domain)) console.log(` ✗ ${domain}: not connected (see above)`);
|
|
96
|
+
else console.log(` ✓ https://${domain}`);
|
|
97
|
+
}
|
|
98
|
+
if (done.failed.length) console.log('Fix the problems above, then run: sudo orionplace domain add <domain>');
|
|
99
|
+
console.log(`Manage it with: orionplace app status ${done.name} | orionplace app logs ${done.name}`);
|
|
100
|
+
console.log('After code changes or npx orionplace-nuxt update, run sudo npx orionplace-nuxt install again.');
|
|
101
|
+
}
|
|
102
|
+
} else if (command === 'init' && positional.length <= 2) {
|
|
68
103
|
result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
|
|
69
104
|
} else if (command === 'update' && positional.length === 1) {
|
|
70
105
|
result = await updateProject({ cwd, dryRun, local });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Nuxt 4 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: 'nuxt',
|
|
15
|
+
title: 'Nuxt',
|
|
16
|
+
dependency: 'nuxt',
|
|
17
|
+
output: '.output/server/index.mjs',
|
|
18
|
+
start: () => ['.output/server/index.mjs'],
|
|
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
|
@@ -513,6 +513,13 @@ If installation fails after source generation, run \`npm install\` and retry.
|
|
|
513
513
|
|
|
514
514
|
## Deploy on your VPS
|
|
515
515
|
|
|
516
|
+
The quickest way: prepare the server with the OrionMarketplace server script,
|
|
517
|
+
then run \`sudo npx orionplace-nuxt install\` in this folder. It builds the
|
|
518
|
+
project, runs it as a system service and connects your domains with nginx and
|
|
519
|
+
HTTPS. Steps: [Install a storefront](https://orionmarket.place/en/sandbox/install/storefront).
|
|
520
|
+
|
|
521
|
+
To set the server up yourself instead:
|
|
522
|
+
|
|
516
523
|
This integration needs a Node.js server and its domain configuration files.
|
|
517
524
|
It does not support static-only or edge hosting. Deploy the build, dependencies,
|
|
518
525
|
\`sitemaps\`, and your environment configuration together; use an absolute
|
|
@@ -190,8 +190,8 @@ export class OrionplaceApi {
|
|
|
190
190
|
const body = response.body as {
|
|
191
191
|
page_data?: { csrf?: boolean };
|
|
192
192
|
} | null;
|
|
193
|
-
//
|
|
194
|
-
//
|
|
193
|
+
// A CSRF rejection: Django views answer 419, API endpoints answer 403
|
|
194
|
+
// with page_data.csrf set to false. The request is not repeated.
|
|
195
195
|
const csrfRejected =
|
|
196
196
|
response.status === 419 ||
|
|
197
197
|
(response.status === 403 && body?.page_data?.csrf === false);
|