@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.
Files changed (110) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +24 -1
  2. package/ai/skills/stacks-commerce/SKILL.md +1 -1
  3. package/ai/skills/stacks-config/SKILL.md +1 -1
  4. package/ai/skills/stacks-database/SKILL.md +1 -1
  5. package/ai/skills/stacks-dependencies/SKILL.md +35 -3
  6. package/ai/skills/stacks-desktop/SKILL.md +12 -0
  7. package/ai/skills/stacks-env/SKILL.md +4 -1
  8. package/ai/skills/stacks-migrations/SKILL.md +2 -2
  9. package/ai/skills/stacks-models/SKILL.md +2 -2
  10. package/app/Actions/Commerce/Product/ProductStoreAction.ts +9 -1
  11. package/app/Actions/Commerce/Product/ProductUpdateAction.ts +4 -1
  12. package/app/Actions/Commerce/commerce-action.test.ts +30 -1
  13. package/app/Actions/Commerce/commerce-action.ts +44 -0
  14. package/app/Actions/Dashboard/Analytics/sales-analytics.test.ts +19 -0
  15. package/app/Actions/Dashboard/Analytics/sales-analytics.ts +2 -1
  16. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +3 -2
  17. package/app/Actions/Dashboard/Commerce/commerce-dashboard.test.ts +29 -9
  18. package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +5 -12
  19. package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +15 -0
  20. package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +8 -10
  21. package/app/Actions/Dashboard/Commerce/commerce-pos.test.ts +26 -6
  22. package/app/Actions/Dashboard/Commerce/commerce-pos.ts +8 -3
  23. package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +69 -0
  24. package/app/Actions/Dashboard/Marketing/abandoned-cart-records.test.ts +39 -0
  25. package/app/Actions/Dashboard/Marketing/abandoned-cart-records.ts +9 -3
  26. package/app/Actions/Dashboard/Marketing/campaign-records.ts +17 -3
  27. package/app/Actions/Payment/CreatePaymentIntentAction.ts +1 -1
  28. package/app/Actions/Payment/CreateSetupIntentAction.ts +1 -1
  29. package/app/Actions/Storefront/AddToCartAction.ts +4 -1
  30. package/app/Actions/Storefront/GetCartAction.ts +25 -10
  31. package/app/Actions/Storefront/PlaceOrderAction.ts +6 -1
  32. package/app/Actions/Storefront/_shipping.ts +63 -37
  33. package/app/Broadcasts/OrderShipped.ts +1 -1
  34. package/app/Mail/OrderConfirmation.ts +14 -7
  35. package/app/Models/Campaign.ts +9 -4
  36. package/app/Models/commerce/Cart.ts +5 -4
  37. package/app/Models/commerce/CartItem.ts +6 -4
  38. package/app/Models/commerce/Coupon.ts +9 -3
  39. package/app/Models/commerce/Customer.ts +2 -1
  40. package/app/Models/commerce/GiftCard.ts +5 -2
  41. package/app/Models/commerce/Order.ts +6 -5
  42. package/app/Models/commerce/OrderItem.ts +2 -1
  43. package/app/Models/commerce/ProductVariant.ts +71 -4
  44. package/app/Models/commerce/Transaction.ts +3 -2
  45. package/app/Storefront/StorefrontMoney.ts +54 -0
  46. package/ide/vscode/.vscode/settings.json +1 -1
  47. package/ide/vscode/README.md +65 -20
  48. package/ide/vscode/package.json +102 -12
  49. package/ide/vscode/src/extension.ts +59 -0
  50. package/ide/vscode/src/project.ts +440 -0
  51. package/ide/vscode/src/stacks.ts +475 -0
  52. package/ide/vscode/src/vscode.d.ts +181 -0
  53. package/ide/vscode/tests/extension.test.ts +420 -0
  54. package/ide/vscode/tests/project.test.ts +268 -0
  55. package/ide/vscode/tsconfig.json +19 -0
  56. package/package.json +4 -4
  57. package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +8 -8
  58. package/resources/components/Dashboard/Analytics/SalesAnalytics.stx +4 -3
  59. package/resources/components/Dashboard/Billing/BillingPlanSummary.stx +4 -1
  60. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +4 -1
  61. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +34 -10
  62. package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +3 -1
  63. package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +4 -1
  64. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +2 -6
  65. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +6 -6
  66. package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +3 -11
  67. package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +2 -6
  68. package/resources/components/Dashboard/Commerce/CommerceGiftCardBreakdown.stx +2 -6
  69. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +2 -6
  70. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +20 -17
  71. package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +3 -2
  72. package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +2 -6
  73. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +2 -6
  74. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +28 -20
  75. package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +7 -15
  76. package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +2 -6
  77. package/resources/components/Dashboard/Commerce/CommercePaymentsBreakdown.stx +2 -6
  78. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +2 -6
  79. package/resources/components/Dashboard/Commerce/CommercePosCart.stx +2 -6
  80. package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +2 -6
  81. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +2 -6
  82. package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +5 -7
  83. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +2 -8
  84. package/resources/components/Dashboard/Commerce/CommerceProductDetailDashboard.stx +3 -20
  85. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +27 -19
  86. package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +4 -15
  87. package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +2 -6
  88. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +12 -10
  89. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +3 -4
  90. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +7 -5
  91. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +3 -2
  92. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +3 -4
  93. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +2 -6
  94. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +12 -10
  95. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +3 -10
  96. package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +3 -8
  97. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +26 -8
  98. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +4 -2
  99. package/resources/components/Dashboard/Marketing/RecoveryCampaignDialog.stx +25 -13
  100. package/resources/components/Dashboard/Transaction/index.stx +6 -3
  101. package/resources/components/Storefront/CartDrawer.stx +11 -7
  102. package/resources/views/cart.stx +19 -23
  103. package/resources/views/checkout/contact.stx +12 -14
  104. package/resources/views/checkout/payment.stx +13 -13
  105. package/resources/views/checkout/shipping.stx +13 -13
  106. package/resources/views/cms/blocks/form.stx +50 -1
  107. package/resources/views/orders/[id].stx +13 -4
  108. package/vcs/github/workflows/buddy-bot.yml +5 -5
  109. package/vcs/github/workflows/ci.yml +3 -3
  110. 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
- factory: faker => faker.number.int({ min: 1, max: 1000 }),
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: 1, max: 1000 }),
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: 50 }),
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
- factory: faker => faker.number.int({ min: 0, max: 25 }),
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
- factory: faker => faker.number.int({ min: 5, max: 50 }),
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
- factory: faker => faker.number.int({ min: 0, max: 50 }),
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
- factory: faker => faker.number.int({ min: 5, max: 100 }),
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
- factory: faker => faker.number.int({ min: 0, max: 2000 }),
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
- factory: faker => faker.number.int({ min: 100, max: 2000 }),
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
- factory: () => 1,
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
- factory: faker => faker.number.int({ min: 100, max: 2000 }),
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: 10, max: 200 }),
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: 150 }),
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.number.int({ min: 0, max: 100 }),
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: 200 }),
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
- factory: faker => faker.number.int({ min: 5, max: 50 }),
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
- rule: schema.number().required().min(0.01),
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: 5, max: 500 }),
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
+ }
@@ -117,7 +117,7 @@
117
117
  // Stacks
118
118
  "typescript.preferGoToSourceDefinition": true,
119
119
  "files.associations": {
120
- "buddy": "typescript",
120
+ "buddy": "shellscript",
121
121
  "*.stx": "html"
122
122
  },
123
123
  "editor.quickSuggestions": {
@@ -1,36 +1,81 @@
1
- # Stacks VS Code Extension Pack
1
+ # Stacks for VS Code
2
2
 
3
- A collection of extensions for an enhanced developer experience while working on Stacks projects.
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
- _Please note, the Stacks framework auto-configures any extension that needs to be configured._
5
+ The extension activates in workspaces that contain a `buddy` file or a `config/app.ts`.
6
6
 
7
- ## Extensions Included
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) - Encrypted environment file support
16
- - [Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Simple source code spell checker for your code
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
- ## Contributing
58
+ Some extensions were dropped from the pack because VS Code now does the same thing itself:
19
59
 
20
- We welcome contributions to this extension pack.
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
- ## Local Usage
65
+ ## Development
23
66
 
24
67
  ```bash
25
- bun run package
26
- code --install-extension vscode-stacks-0.70.160.vsix
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
- _If you would like to add an extension, please open an issue or start a Discussion first 🙏🏼._
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 Links
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)
@@ -2,8 +2,8 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.38",
6
- "description": "A modern Stacks development environment.",
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
- "extension pack",
25
- "stacks extension collection"
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
- "package": "bunx --bun vsce package",
43
- "release": "bunx --bun vsce publish"
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": "typescript",
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