@stacksjs/defaults 0.75.38 → 0.75.41
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/ai/skills/stacks-ai/SKILL.md +24 -1
- package/ai/skills/stacks-commerce/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-dependencies/SKILL.md +35 -3
- package/ai/skills/stacks-desktop/SKILL.md +12 -0
- package/ai/skills/stacks-env/SKILL.md +4 -1
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-models/SKILL.md +2 -2
- package/app/Actions/Commerce/Product/ProductStoreAction.ts +9 -1
- package/app/Actions/Commerce/Product/ProductUpdateAction.ts +4 -1
- package/app/Actions/Commerce/commerce-action.test.ts +30 -1
- package/app/Actions/Commerce/commerce-action.ts +44 -0
- package/app/Actions/Dashboard/Analytics/sales-analytics.test.ts +19 -0
- package/app/Actions/Dashboard/Analytics/sales-analytics.ts +2 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +3 -2
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.test.ts +29 -9
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +5 -12
- package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +15 -0
- package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +8 -10
- package/app/Actions/Dashboard/Commerce/commerce-pos.test.ts +26 -6
- package/app/Actions/Dashboard/Commerce/commerce-pos.ts +8 -3
- package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +69 -0
- package/app/Actions/Dashboard/Marketing/abandoned-cart-records.test.ts +39 -0
- package/app/Actions/Dashboard/Marketing/abandoned-cart-records.ts +9 -3
- package/app/Actions/Dashboard/Marketing/campaign-records.ts +17 -3
- package/app/Actions/Payment/CreatePaymentIntentAction.ts +1 -1
- package/app/Actions/Payment/CreateSetupIntentAction.ts +1 -1
- package/app/Actions/Storefront/AddToCartAction.ts +4 -1
- package/app/Actions/Storefront/GetCartAction.ts +25 -10
- package/app/Actions/Storefront/PlaceOrderAction.ts +6 -1
- package/app/Actions/Storefront/_shipping.ts +63 -37
- package/app/Broadcasts/OrderShipped.ts +1 -1
- package/app/Mail/OrderConfirmation.ts +14 -7
- package/app/Models/Campaign.ts +9 -4
- package/app/Models/commerce/Cart.ts +5 -4
- package/app/Models/commerce/CartItem.ts +6 -4
- package/app/Models/commerce/Coupon.ts +9 -3
- package/app/Models/commerce/Customer.ts +2 -1
- package/app/Models/commerce/GiftCard.ts +5 -2
- package/app/Models/commerce/Order.ts +6 -5
- package/app/Models/commerce/OrderItem.ts +2 -1
- package/app/Models/commerce/ProductVariant.ts +71 -4
- package/app/Models/commerce/Transaction.ts +3 -2
- package/app/Storefront/StorefrontMoney.ts +54 -0
- package/ide/vscode/.vscode/settings.json +1 -1
- package/ide/vscode/README.md +65 -20
- package/ide/vscode/package.json +102 -12
- package/ide/vscode/src/extension.ts +59 -0
- package/ide/vscode/src/project.ts +440 -0
- package/ide/vscode/src/stacks.ts +475 -0
- package/ide/vscode/src/vscode.d.ts +181 -0
- package/ide/vscode/tests/extension.test.ts +420 -0
- package/ide/vscode/tests/project.test.ts +268 -0
- package/ide/vscode/tsconfig.json +19 -0
- package/package.json +4 -4
- package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +8 -8
- package/resources/components/Dashboard/Analytics/SalesAnalytics.stx +4 -3
- package/resources/components/Dashboard/Billing/BillingPlanSummary.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +34 -10
- package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +3 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +6 -6
- package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +3 -11
- package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardBreakdown.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +20 -17
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +3 -2
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +28 -20
- package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +7 -15
- package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePaymentsBreakdown.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCart.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +5 -7
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +2 -8
- package/resources/components/Dashboard/Commerce/CommerceProductDetailDashboard.stx +3 -20
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +27 -19
- package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +4 -15
- package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +12 -10
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +3 -4
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +7 -5
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +3 -2
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +3 -4
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +12 -10
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +3 -10
- package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +3 -8
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +26 -8
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +4 -2
- package/resources/components/Dashboard/Marketing/RecoveryCampaignDialog.stx +25 -13
- package/resources/components/Dashboard/Transaction/index.stx +6 -3
- package/resources/components/Storefront/CartDrawer.stx +11 -7
- package/resources/views/cart.stx +19 -23
- package/resources/views/checkout/contact.stx +12 -14
- package/resources/views/checkout/payment.stx +13 -13
- package/resources/views/checkout/shipping.stx +13 -13
- package/resources/views/cms/blocks/form.stx +50 -1
- package/resources/views/orders/[id].stx +13 -4
- package/vcs/github/workflows/buddy-bot.yml +5 -5
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
|
@@ -52,7 +52,8 @@ export default defineModel({
|
|
|
52
52
|
validation: {
|
|
53
53
|
rule: schema.number().required().min(0),
|
|
54
54
|
},
|
|
55
|
-
|
|
55
|
+
// Integer minor units, in the same range as Product.price.
|
|
56
|
+
factory: faker => faker.number.int({ min: 100, max: 10000 }),
|
|
56
57
|
},
|
|
57
58
|
|
|
58
59
|
totalPrice: {
|
|
@@ -61,7 +62,7 @@ export default defineModel({
|
|
|
61
62
|
validation: {
|
|
62
63
|
rule: schema.number().required().min(0),
|
|
63
64
|
},
|
|
64
|
-
factory: faker => faker.number.int({ min:
|
|
65
|
+
factory: faker => faker.number.int({ min: 100, max: 50000 }),
|
|
65
66
|
},
|
|
66
67
|
|
|
67
68
|
taxRate: {
|
|
@@ -79,7 +80,7 @@ export default defineModel({
|
|
|
79
80
|
validation: {
|
|
80
81
|
rule: schema.number().min(0),
|
|
81
82
|
},
|
|
82
|
-
factory: faker => faker.number.int({ min: 0, max:
|
|
83
|
+
factory: faker => faker.number.int({ min: 0, max: 1000 }),
|
|
83
84
|
},
|
|
84
85
|
|
|
85
86
|
discountPercentage: {
|
|
@@ -97,7 +98,8 @@ export default defineModel({
|
|
|
97
98
|
validation: {
|
|
98
99
|
rule: schema.number().min(0),
|
|
99
100
|
},
|
|
100
|
-
|
|
101
|
+
// At most $1.00, so it never exceeds the smallest unit price above.
|
|
102
|
+
factory: faker => faker.number.int({ min: 0, max: 100 }),
|
|
101
103
|
},
|
|
102
104
|
|
|
103
105
|
productName: {
|
|
@@ -106,7 +106,11 @@ export default defineModel({
|
|
|
106
106
|
min: 'Discount value must be greater than 0.01',
|
|
107
107
|
},
|
|
108
108
|
},
|
|
109
|
-
|
|
109
|
+
// A percentage stays a plain number; a fixed amount is integer minor units
|
|
110
|
+
// ($5 to $50). discountType is declared above, so the seeder has drawn it.
|
|
111
|
+
factory: (faker, { discountType }) => discountType === 'fixed_amount'
|
|
112
|
+
? faker.helpers.arrayElement([500, 1000, 1500, 2000, 2500, 5000])
|
|
113
|
+
: faker.number.int({ min: 5, max: 50 }),
|
|
110
114
|
},
|
|
111
115
|
|
|
112
116
|
minOrderAmount: {
|
|
@@ -118,7 +122,8 @@ export default defineModel({
|
|
|
118
122
|
min: 'Min order amount must be greater than 0',
|
|
119
123
|
},
|
|
120
124
|
},
|
|
121
|
-
|
|
125
|
+
// Integer minor units: no minimum, or $25 to $100.
|
|
126
|
+
factory: faker => faker.helpers.arrayElement([0, 2500, 5000, 7500, 10000]),
|
|
122
127
|
},
|
|
123
128
|
|
|
124
129
|
maxDiscountAmount: {
|
|
@@ -130,7 +135,8 @@ export default defineModel({
|
|
|
130
135
|
min: 'Max discount amount must be greater than 0',
|
|
131
136
|
},
|
|
132
137
|
},
|
|
133
|
-
|
|
138
|
+
// Integer minor units: $10 to $100.
|
|
139
|
+
factory: faker => faker.number.int({ min: 1000, max: 10000 }),
|
|
134
140
|
},
|
|
135
141
|
|
|
136
142
|
freeProductId: {
|
|
@@ -82,7 +82,8 @@ export default defineModel({
|
|
|
82
82
|
min: 'Total spent cannot be negative',
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
|
-
|
|
85
|
+
// Integer minor units: $0 to $5,000.
|
|
86
|
+
factory: faker => faker.number.int({ min: 0, max: 500000 }),
|
|
86
87
|
},
|
|
87
88
|
|
|
88
89
|
lastOrder: {
|
|
@@ -57,7 +57,8 @@ export default defineModel({
|
|
|
57
57
|
validation: {
|
|
58
58
|
rule: schema.number().required().min(1),
|
|
59
59
|
},
|
|
60
|
-
|
|
60
|
+
// Integer minor units, in the denominations cards are actually sold in: $25 to $250.
|
|
61
|
+
factory: faker => faker.helpers.arrayElement([2500, 5000, 10000, 15000, 20000, 25000]),
|
|
61
62
|
},
|
|
62
63
|
|
|
63
64
|
currentBalance: {
|
|
@@ -66,7 +67,9 @@ export default defineModel({
|
|
|
66
67
|
validation: {
|
|
67
68
|
rule: schema.number().required().min(0),
|
|
68
69
|
},
|
|
69
|
-
|
|
70
|
+
// Drawn below the smallest initial balance above, so a seeded card never
|
|
71
|
+
// holds more than it was issued with.
|
|
72
|
+
factory: faker => faker.number.int({ min: 0, max: 2500 }),
|
|
70
73
|
},
|
|
71
74
|
|
|
72
75
|
currency: {
|
|
@@ -64,7 +64,8 @@ export default defineModel({
|
|
|
64
64
|
validation: {
|
|
65
65
|
rule: schema.number().required().min(0),
|
|
66
66
|
},
|
|
67
|
-
|
|
67
|
+
// Integer minor units, like every commerce amount: 1999 is $19.99.
|
|
68
|
+
factory: faker => faker.number.int({ min: 1000, max: 50000 }),
|
|
68
69
|
},
|
|
69
70
|
|
|
70
71
|
currency: {
|
|
@@ -84,7 +85,7 @@ export default defineModel({
|
|
|
84
85
|
validation: {
|
|
85
86
|
rule: schema.number().min(0),
|
|
86
87
|
},
|
|
87
|
-
factory: faker => faker.number.int({ min:
|
|
88
|
+
factory: faker => faker.number.int({ min: 0, max: 4000 }),
|
|
88
89
|
},
|
|
89
90
|
|
|
90
91
|
discountAmount: {
|
|
@@ -94,7 +95,7 @@ export default defineModel({
|
|
|
94
95
|
validation: {
|
|
95
96
|
rule: schema.number().min(0),
|
|
96
97
|
},
|
|
97
|
-
factory: faker => faker.number.int({ min: 0, max:
|
|
98
|
+
factory: faker => faker.number.int({ min: 0, max: 2000 }),
|
|
98
99
|
},
|
|
99
100
|
|
|
100
101
|
deliveryFee: {
|
|
@@ -104,7 +105,7 @@ export default defineModel({
|
|
|
104
105
|
validation: {
|
|
105
106
|
rule: schema.number().min(0),
|
|
106
107
|
},
|
|
107
|
-
factory: faker => faker.
|
|
108
|
+
factory: faker => faker.helpers.arrayElement([0, 299, 499, 799]),
|
|
108
109
|
},
|
|
109
110
|
|
|
110
111
|
tipAmount: {
|
|
@@ -114,7 +115,7 @@ export default defineModel({
|
|
|
114
115
|
validation: {
|
|
115
116
|
rule: schema.number().min(0),
|
|
116
117
|
},
|
|
117
|
-
factory: faker => faker.number.int({ min: 0, max:
|
|
118
|
+
factory: faker => faker.number.int({ min: 0, max: 1500 }),
|
|
118
119
|
},
|
|
119
120
|
|
|
120
121
|
orderType: {
|
|
@@ -36,7 +36,8 @@ export default defineModel({
|
|
|
36
36
|
min: 'Price cannot be negative',
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
|
-
|
|
39
|
+
// Integer minor units, in the same range as Product.price.
|
|
40
|
+
factory: faker => faker.number.int({ min: 100, max: 10000 }),
|
|
40
41
|
},
|
|
41
42
|
|
|
42
43
|
special_instructions: {
|
|
@@ -13,14 +13,22 @@ export default defineModel({
|
|
|
13
13
|
// tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
|
|
14
14
|
ownership: false,
|
|
15
15
|
|
|
16
|
+
// A SKU identifies a variant within its product. Unique per product rather
|
|
17
|
+
// than globally, because two imported stores (or two products of one) can
|
|
18
|
+
// legitimately reuse a code. NULLs never collide, so variants without a SKU
|
|
19
|
+
// are unconstrained on every dialect.
|
|
20
|
+
indexes: [
|
|
21
|
+
{ name: 'product_variants_product_sku_unique', columns: ['product_id', 'sku'], unique: true },
|
|
22
|
+
],
|
|
23
|
+
|
|
16
24
|
traits: {
|
|
17
25
|
useUuid: true,
|
|
18
26
|
useTimestamps: true,
|
|
19
27
|
useSearch: {
|
|
20
|
-
displayable: ['id', 'productId', 'variant', 'type', 'description', 'options', 'status'],
|
|
21
|
-
searchable: ['variant', 'type', 'description', 'options'],
|
|
22
|
-
sortable: ['createdAt', 'updatedAt', 'variant', 'type', 'status'],
|
|
23
|
-
filterable: ['productId', 'type', 'status'],
|
|
28
|
+
displayable: ['id', 'productId', 'variant', 'type', 'sku', 'price', 'compareAtPrice', 'inventoryCount', 'description', 'options', 'status'],
|
|
29
|
+
searchable: ['variant', 'type', 'sku', 'description', 'options'],
|
|
30
|
+
sortable: ['createdAt', 'updatedAt', 'variant', 'type', 'status', 'price', 'inventoryCount'],
|
|
31
|
+
filterable: ['productId', 'type', 'status', 'sku'],
|
|
24
32
|
},
|
|
25
33
|
|
|
26
34
|
useSeeder: {
|
|
@@ -109,6 +117,65 @@ export default defineModel({
|
|
|
109
117
|
},
|
|
110
118
|
factory: faker => faker.helpers.arrayElement(['active', 'inactive', 'draft']),
|
|
111
119
|
},
|
|
120
|
+
|
|
121
|
+
sku: {
|
|
122
|
+
order: 8,
|
|
123
|
+
fillable: true,
|
|
124
|
+
nullable: true,
|
|
125
|
+
validation: {
|
|
126
|
+
rule: schema.string().max(100),
|
|
127
|
+
message: {
|
|
128
|
+
max: 'SKU must have a maximum of 100 characters',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
factory: faker => faker.string.alphanumeric(10).toUpperCase(),
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Money is integer minor units (cents), never a float. NULL means the
|
|
135
|
+
// variant has no price of its own and inherits `products.price`.
|
|
136
|
+
price: {
|
|
137
|
+
order: 9,
|
|
138
|
+
fillable: true,
|
|
139
|
+
nullable: true,
|
|
140
|
+
validation: {
|
|
141
|
+
rule: schema.number().integer().min(0),
|
|
142
|
+
message: {
|
|
143
|
+
min: 'Price must be at least 0',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
factory: faker => faker.number.int({ min: 100, max: 10000 }),
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// The "was" price shown struck through, in minor units. NULL when the
|
|
150
|
+
// variant is not on sale.
|
|
151
|
+
compareAtPrice: {
|
|
152
|
+
order: 10,
|
|
153
|
+
fillable: true,
|
|
154
|
+
nullable: true,
|
|
155
|
+
validation: {
|
|
156
|
+
rule: schema.number().integer().min(0),
|
|
157
|
+
message: {
|
|
158
|
+
min: 'Compare-at price must be at least 0',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
// Above the factory's highest `price`, so a seeded sale is always a discount.
|
|
162
|
+
factory: faker => faker.datatype.boolean() ? faker.number.int({ min: 10001, max: 20000 }) : null,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Units in stock. NULL means stock is not tracked for this variant,
|
|
166
|
+
// which is different from 0 (tracked and sold out).
|
|
167
|
+
inventoryCount: {
|
|
168
|
+
order: 11,
|
|
169
|
+
fillable: true,
|
|
170
|
+
nullable: true,
|
|
171
|
+
validation: {
|
|
172
|
+
rule: schema.number().integer().min(0),
|
|
173
|
+
message: {
|
|
174
|
+
min: 'Inventory count must be at least 0',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
factory: faker => faker.number.int({ min: 0, max: 100 }),
|
|
178
|
+
},
|
|
112
179
|
},
|
|
113
180
|
|
|
114
181
|
dashboard: {
|
|
@@ -41,9 +41,10 @@ export default defineModel({
|
|
|
41
41
|
order: 2,
|
|
42
42
|
fillable: true,
|
|
43
43
|
validation: {
|
|
44
|
-
|
|
44
|
+
// Integer minor units: the smallest amount is one cent, not 0.01 of one.
|
|
45
|
+
rule: schema.number().integer().required().min(1),
|
|
45
46
|
},
|
|
46
|
-
factory: faker => faker.number.int({ min:
|
|
47
|
+
factory: faker => faker.number.int({ min: 500, max: 50000 }),
|
|
47
48
|
},
|
|
48
49
|
|
|
49
50
|
status: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money on the storefront's server-rendered pages.
|
|
3
|
+
*
|
|
4
|
+
* `cart_items.unit_price`, `cart_items.total_price`, `order_items.price`
|
|
5
|
+
* and `orders.total_amount` hold integer minor units (1999 is $19.99), as
|
|
6
|
+
* every commerce amount does. The cart, checkout and order pages used to
|
|
7
|
+
* print them with a hardcoded `$` and `.toFixed(2)`, which showed a 1999
|
|
8
|
+
* cent product as $1999.00 and ignored the cart's currency. They format
|
|
9
|
+
* through `@stacksjs/commerce/money` here instead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
13
|
+
import { totalsFor } from '../Actions/Storefront/_shipping'
|
|
14
|
+
|
|
15
|
+
/** The currency a cart or order row was priced in, or USD when it has none. */
|
|
16
|
+
export function storefrontCurrency(stored: unknown): string {
|
|
17
|
+
const code = String(stored ?? '').trim().toUpperCase()
|
|
18
|
+
return /^[A-Z]{3}$/.test(code) ? code : 'USD'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A stored minor-unit amount, formatted for the shopper: `1999, 'USD'` is `"$19.99"`. */
|
|
22
|
+
export function storefrontMoney(minor: number, currency: string): string {
|
|
23
|
+
return formatCurrency(minor, currency)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StorefrontSummaryLabels {
|
|
27
|
+
subtotal: number
|
|
28
|
+
shipping: number
|
|
29
|
+
total: number
|
|
30
|
+
subtotalLabel: string
|
|
31
|
+
shippingLabel: string
|
|
32
|
+
totalLabel: string
|
|
33
|
+
freeShippingNote: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The running totals the cart and checkout pages show, from the lines'
|
|
38
|
+
* subtotal in minor units. Shipping comes from the same rule the
|
|
39
|
+
* place-order action charges with, so the two cannot disagree.
|
|
40
|
+
*/
|
|
41
|
+
export function storefrontSummary(subtotal: number, currency: string): StorefrontSummaryLabels {
|
|
42
|
+
const totals = totalsFor(subtotal, currency)
|
|
43
|
+
return {
|
|
44
|
+
subtotal: totals.subtotal,
|
|
45
|
+
shipping: totals.shipping,
|
|
46
|
+
total: totals.total,
|
|
47
|
+
subtotalLabel: storefrontMoney(totals.subtotal, currency),
|
|
48
|
+
shippingLabel: totals.shipping === 0 ? 'Free' : storefrontMoney(totals.shipping, currency),
|
|
49
|
+
totalLabel: storefrontMoney(totals.total, currency),
|
|
50
|
+
freeShippingNote: totals.remainingForFree > 0
|
|
51
|
+
? `Add ${storefrontMoney(totals.remainingForFree, currency)} more for free shipping.`
|
|
52
|
+
: '',
|
|
53
|
+
}
|
|
54
|
+
}
|
package/ide/vscode/README.md
CHANGED
|
@@ -1,36 +1,81 @@
|
|
|
1
|
-
# Stacks VS Code
|
|
1
|
+
# Stacks for VS Code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build a Stacks app without leaving the editor. Start the dev server, preview it in VS Code's built-in Simple Browser, and run any `buddy` command from the Command Palette. The extension also installs a small pack of extensions that Stacks projects are configured for.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The extension activates in workspaces that contain a `buddy` file or a `config/app.ts`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) and type `Stacks`.
|
|
10
|
+
|
|
11
|
+
| Command | What it does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Stacks: Open Preview** | Opens the running dev server in the Simple Browser, beside your editor. If nothing is running, it offers to start the dev server. |
|
|
14
|
+
| **Stacks: Start Dev Server** | Runs `./buddy dev` in a terminal named `Stacks Dev` at the project root. Once the server is up, the preview opens in the Simple Browser. |
|
|
15
|
+
| **Stacks: Run Buddy Command...** | Lists every command `./buddy list` knows, including your own in `app/Commands/`. Pick one, and if it takes arguments you are asked for them. You can also type a full command line, such as `migrate --diff` or `make:model Post`. The command runs in a terminal named `Stacks Buddy`. |
|
|
16
|
+
|
|
17
|
+
The **Stacks** item in the status bar opens the preview.
|
|
18
|
+
|
|
19
|
+
Every command runs the project's own `./buddy`, relative to the workspace root. A `buddy` on your `PATH` or in `node_modules/.bin` is never used, because that name can also belong to an unrelated tool, such as the `@buddysh/buddy` dependency bot.
|
|
20
|
+
|
|
21
|
+
## Finding the preview URL
|
|
22
|
+
|
|
23
|
+
When the extension starts the dev server, it reads the URL `buddy dev` prints in its ready banner (for example `App: https://stacks.localhost/dashboard`). That URL already accounts for a shifted port and the app's entry page.
|
|
24
|
+
|
|
25
|
+
Otherwise it works the URL out the same way `buddy dev` does, from `.env`, `.env.local`, `.env.development` and `.env.development.local`:
|
|
26
|
+
|
|
27
|
+
- The frontend listens on `PORT`, or `3000` if it is not set.
|
|
28
|
+
- If `APP_URL` (default `stacks.localhost`) is not a localhost address, the app is served at `https://<APP_URL>` through the local HTTPS proxy. `STACKS_DEV_LOCALHOST=1` turns this off.
|
|
29
|
+
- `APP_PATH`, or `appPath` in `config/app.ts`, is appended as the entry page.
|
|
30
|
+
|
|
31
|
+
The extension checks that the frontend port is accepting connections before it opens anything. It uses the `https://` URL only when the proxy answers too, and falls back to `http://localhost:<PORT>` otherwise. In a remote window (SSH, WSL, Codespaces) localhost URLs are port-forwarded automatically.
|
|
32
|
+
|
|
33
|
+
## Settings
|
|
34
|
+
|
|
35
|
+
| Setting | Default | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `stacks.preview.url` | `""` | Always open this URL, e.g. `http://localhost:3000/dashboard`. Leave empty to detect it. |
|
|
38
|
+
| `stacks.preview.preferLocalhost` | `false` | Preview `http://localhost:<PORT>` even when `APP_URL` is a pretty `https://` domain. Use this if the Simple Browser stays blank because the local certificate is not trusted. |
|
|
39
|
+
| `stacks.devServer.args` | `[]` | Extra arguments for `./buddy dev`, e.g. `["frontend"]` or `["--site"]`. |
|
|
40
|
+
| `stacks.devServer.openPreview` | `true` | Open the Simple Browser once the dev server is up, instead of letting `buddy dev` open your system browser. |
|
|
41
|
+
| `stacks.statusBar.enabled` | `true` | Show the Stacks status bar item. |
|
|
42
|
+
|
|
43
|
+
The extension also sets a few editor defaults for Stacks projects: `.stx` files use HTML language support, the `buddy` script is highlighted as a shell script, `npm.packageManager` is `bun`, Go to Definition prefers source files over `.d.ts`, and TypeScript uses the workspace version.
|
|
44
|
+
|
|
45
|
+
## Included extensions
|
|
8
46
|
|
|
9
|
-
- Built-in HTML language support for `.stx` templates
|
|
10
|
-
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Editor integration for the Pickier configuration
|
|
11
|
-
- [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) - Markdown linting
|
|
12
|
-
- [Goto Definition Alias](https://marketplace.visualstudio.com/items?itemName=antfu.goto-definition-alias) - Allows jumping to the definition of an aliased import
|
|
13
|
-
- [NPM Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - Auto-completes package imports
|
|
14
47
|
- [Bun for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=oven.bun-vscode) - Bun runtime and test integration
|
|
15
|
-
- [Dotenvx](https://marketplace.visualstudio.com/items?itemName=dotenv.dotenvx-vscode) -
|
|
16
|
-
- [
|
|
48
|
+
- [Dotenvx](https://marketplace.visualstudio.com/items?itemName=dotenv.dotenvx-vscode) - encrypted environment file support
|
|
49
|
+
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - editor integration for the project's lint rules
|
|
50
|
+
- [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) - Markdown linting
|
|
51
|
+
- [shell-format](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format) - shell script and Dockerfile formatting
|
|
52
|
+
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - spell checking with the Stacks dictionary
|
|
53
|
+
- [Total TypeScript Error Translator](https://marketplace.visualstudio.com/items?itemName=mattpocock.ts-error-translator) - plain-English TypeScript errors
|
|
54
|
+
- [Prettify TypeScript](https://marketplace.visualstudio.com/items?itemName=mylesmurphy.prettify-ts) - readable type hovers
|
|
55
|
+
- [Twoslash Query Comments](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-twoslash-queries) - inline `// ^?` type queries
|
|
56
|
+
- [AWS Toolkit](https://marketplace.visualstudio.com/items?itemName=amazonwebservices.aws-toolkit-vscode) - the AWS resources `buddy deploy` creates
|
|
17
57
|
|
|
18
|
-
|
|
58
|
+
Some extensions were dropped from the pack because VS Code now does the same thing itself:
|
|
19
59
|
|
|
20
|
-
|
|
60
|
+
- **Goto Definition Alias**: built-in **Go to Source Definition**, which this extension enables by default.
|
|
61
|
+
- **npm Intellisense**: TypeScript's built-in package import completions.
|
|
62
|
+
- **DotENV**: VS Code's built-in `dotenv` language highlights `.env` and `.env.*` files.
|
|
63
|
+
- **Gremlins**: built-in Unicode highlighting (`editor.unicodeHighlight.*`) flags invisible and confusable characters.
|
|
21
64
|
|
|
22
|
-
##
|
|
65
|
+
## Development
|
|
23
66
|
|
|
24
67
|
```bash
|
|
25
|
-
bun run
|
|
26
|
-
|
|
68
|
+
bun run build # bundle src/ into dist/extension.js
|
|
69
|
+
bun test ./tests # unit tests for URL detection, `buddy list` parsing and the commands
|
|
70
|
+
bun run typecheck
|
|
71
|
+
bun run package # build and create a .vsix
|
|
72
|
+
code --install-extension vscode-stacks-<version>.vsix
|
|
27
73
|
```
|
|
28
74
|
|
|
29
|
-
|
|
75
|
+
The extension has no runtime dependencies. It is published automatically by the Stacks release workflow, and `vscode:prepublish` builds `dist/` first.
|
|
30
76
|
|
|
31
|
-
## Relevant
|
|
77
|
+
## Relevant links
|
|
32
78
|
|
|
33
79
|
- [GitHub](https://github.com/stacksjs/stacks)
|
|
34
80
|
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=stacks.vscode-stacks)
|
|
35
|
-
|
|
36
|
-
**Enjoy!**
|
|
81
|
+
- [IDE setup guide](https://stacksjs.com/docs/bootcamp/how-to/ide-setup)
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
6
|
-
"description": "
|
|
5
|
+
"version": "0.75.41",
|
|
6
|
+
"description": "Develop Stacks apps without leaving VS Code: preview the dev server in the Simple Browser, start it, and run any buddy command.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
|
9
9
|
"sponsor": {
|
|
@@ -21,35 +21,129 @@
|
|
|
21
21
|
"keywords": [
|
|
22
22
|
"stacks",
|
|
23
23
|
"stacksjs",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"buddy",
|
|
25
|
+
"preview",
|
|
26
|
+
"simple browser",
|
|
27
|
+
"dev server",
|
|
28
|
+
"extension pack"
|
|
26
29
|
],
|
|
27
30
|
"categories": [
|
|
28
|
-
"Extension Packs"
|
|
31
|
+
"Extension Packs",
|
|
32
|
+
"Other"
|
|
29
33
|
],
|
|
30
34
|
"icon": "logo.png",
|
|
35
|
+
"main": "./dist/extension.js",
|
|
31
36
|
"files": [
|
|
32
37
|
".vscode/extensions.json",
|
|
33
38
|
".vscode/settings.json",
|
|
34
39
|
".vscode/stacks.code-snippets",
|
|
40
|
+
"dist/extension.js",
|
|
35
41
|
"LICENSE.md",
|
|
42
|
+
"README.md",
|
|
36
43
|
"logo.png"
|
|
37
44
|
],
|
|
38
45
|
"engines": {
|
|
39
46
|
"vscode": "^1.109.5"
|
|
40
47
|
},
|
|
48
|
+
"extensionKind": [
|
|
49
|
+
"workspace"
|
|
50
|
+
],
|
|
51
|
+
"activationEvents": [
|
|
52
|
+
"workspaceContains:buddy",
|
|
53
|
+
"workspaceContains:config/app.ts"
|
|
54
|
+
],
|
|
55
|
+
"capabilities": {
|
|
56
|
+
"untrustedWorkspaces": {
|
|
57
|
+
"supported": false,
|
|
58
|
+
"description": "The Stacks commands run the workspace's own ./buddy script, so they need a trusted workspace."
|
|
59
|
+
},
|
|
60
|
+
"virtualWorkspaces": false
|
|
61
|
+
},
|
|
41
62
|
"scripts": {
|
|
42
|
-
"
|
|
43
|
-
"
|
|
63
|
+
"build": "bun build ./src/extension.ts --outdir dist --target node --format cjs --external vscode",
|
|
64
|
+
"vscode:prepublish": "bun run build",
|
|
65
|
+
"test": "bun test ./tests",
|
|
66
|
+
"typecheck": "bun x --bun tsc --noEmit -p tsconfig.json",
|
|
67
|
+
"package": "bunx --bun vsce package --no-dependencies",
|
|
68
|
+
"release": "bunx --bun vsce publish --no-dependencies"
|
|
44
69
|
},
|
|
45
70
|
"galleryBanner": {
|
|
46
71
|
"color": "#d7e4f5",
|
|
47
72
|
"theme": "light"
|
|
48
73
|
},
|
|
49
74
|
"contributes": {
|
|
75
|
+
"commands": [
|
|
76
|
+
{
|
|
77
|
+
"command": "stacks.openPreview",
|
|
78
|
+
"title": "Open Preview",
|
|
79
|
+
"category": "Stacks",
|
|
80
|
+
"icon": "$(globe)"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"command": "stacks.startDevServer",
|
|
84
|
+
"title": "Start Dev Server",
|
|
85
|
+
"category": "Stacks",
|
|
86
|
+
"icon": "$(play)"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"command": "stacks.runBuddyCommand",
|
|
90
|
+
"title": "Run Buddy Command...",
|
|
91
|
+
"category": "Stacks",
|
|
92
|
+
"icon": "$(terminal)"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"menus": {
|
|
96
|
+
"commandPalette": [
|
|
97
|
+
{
|
|
98
|
+
"command": "stacks.openPreview",
|
|
99
|
+
"when": "stacks.isProject"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"command": "stacks.startDevServer",
|
|
103
|
+
"when": "stacks.isProject"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"command": "stacks.runBuddyCommand",
|
|
107
|
+
"when": "stacks.isProject"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"configuration": {
|
|
112
|
+
"title": "Stacks",
|
|
113
|
+
"properties": {
|
|
114
|
+
"stacks.preview.url": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"default": "",
|
|
117
|
+
"markdownDescription": "URL `Stacks: Open Preview` always opens, e.g. `http://localhost:3000/dashboard`. Leave empty to find the running dev server the way `./buddy dev` picks its URL (`APP_URL`, `PORT`, `APP_PATH`)."
|
|
118
|
+
},
|
|
119
|
+
"stacks.preview.preferLocalhost": {
|
|
120
|
+
"type": "boolean",
|
|
121
|
+
"default": false,
|
|
122
|
+
"markdownDescription": "Preview `http://localhost:<PORT>` even when `APP_URL` gives the app a pretty `https://` domain. Useful when the local certificate is not trusted, which leaves the Simple Browser blank."
|
|
123
|
+
},
|
|
124
|
+
"stacks.devServer.args": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"items": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
},
|
|
129
|
+
"default": [],
|
|
130
|
+
"markdownDescription": "Extra arguments for `./buddy dev`, e.g. `[\"frontend\"]` or `[\"--site\"]`. They are passed to the shell as written."
|
|
131
|
+
},
|
|
132
|
+
"stacks.devServer.openPreview": {
|
|
133
|
+
"type": "boolean",
|
|
134
|
+
"default": true,
|
|
135
|
+
"markdownDescription": "Open the preview in the Simple Browser once `Stacks: Start Dev Server` is up, instead of letting `./buddy dev` open the system browser."
|
|
136
|
+
},
|
|
137
|
+
"stacks.statusBar.enabled": {
|
|
138
|
+
"type": "boolean",
|
|
139
|
+
"default": true,
|
|
140
|
+
"description": "Show the Stacks item in the status bar, which opens the preview."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
50
144
|
"configurationDefaults": {
|
|
51
145
|
"files.associations": {
|
|
52
|
-
"buddy": "
|
|
146
|
+
"buddy": "shellscript",
|
|
53
147
|
"*.stx": "html"
|
|
54
148
|
},
|
|
55
149
|
"npm.packageManager": "bun",
|
|
@@ -60,15 +154,11 @@
|
|
|
60
154
|
"extensionPack": [
|
|
61
155
|
"dotenv.dotenvx-vscode",
|
|
62
156
|
"amazonwebservices.aws-toolkit-vscode",
|
|
63
|
-
"antfu.goto-alias",
|
|
64
157
|
"dbaeumer.vscode-eslint",
|
|
65
|
-
"christian-kohler.npm-intellisense",
|
|
66
158
|
"davidanson.vscode-markdownlint",
|
|
67
159
|
"foxundermoon.shell-format",
|
|
68
160
|
"mattpocock.ts-error-translator",
|
|
69
|
-
"mikestead.dotenv",
|
|
70
161
|
"mylesmurphy.prettify-ts",
|
|
71
|
-
"nhoizey.gremlins",
|
|
72
162
|
"Orta.vscode-twoslash-queries",
|
|
73
163
|
"oven.bun-vscode",
|
|
74
164
|
"streetsidesoftware.code-spell-checker"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extension entry VS Code loads (`main` in package.json, bundled to
|
|
3
|
+
* dist/extension.js). It only wires the real machine into the controller in
|
|
4
|
+
* ./stacks.ts; the behavior lives there and in ./project.ts.
|
|
5
|
+
*/
|
|
6
|
+
import type { Host } from './stacks'
|
|
7
|
+
import { execFile } from 'node:child_process'
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { connect } from 'node:net'
|
|
10
|
+
import * as vscode from 'vscode'
|
|
11
|
+
import { createStacksExtension } from './stacks'
|
|
12
|
+
|
|
13
|
+
const PROBE_TIMEOUT_MS = 500
|
|
14
|
+
|
|
15
|
+
const host: Host = {
|
|
16
|
+
exists: path => existsSync(path),
|
|
17
|
+
|
|
18
|
+
read(path) {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(path, 'utf8')
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
exec(file, args, cwd) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile(file, args, {
|
|
30
|
+
cwd,
|
|
31
|
+
timeout: 60_000,
|
|
32
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
33
|
+
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
34
|
+
}, (error, stdout) => error ? reject(error) : resolve(String(stdout)))
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
probe(hostname, port) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const socket = connect({ host: hostname, port })
|
|
41
|
+
const finish = (open: boolean) => {
|
|
42
|
+
socket.destroy()
|
|
43
|
+
resolve(open)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
socket.setTimeout(PROBE_TIMEOUT_MS)
|
|
47
|
+
socket.once('connect', () => finish(true))
|
|
48
|
+
socket.once('timeout', () => finish(false))
|
|
49
|
+
socket.once('error', () => finish(false))
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const extension = createStacksExtension(vscode, host)
|
|
57
|
+
|
|
58
|
+
export const activate = extension.activate
|
|
59
|
+
export const deactivate = extension.deactivate
|