@stacksjs/defaults 0.74.63 → 0.74.65

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.
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.63",
5
+ "version": "0.74.65",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.63",
5
+ "version": "0.74.65",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.74.63",
58
+ "@stacksjs/mobile": "^0.74.65",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -1,9 +1,23 @@
1
- <script>
1
+ <script server>
2
+ import { defineProps, withDefaults } from 'stx'
3
+
4
+ /**
5
+ * The default holding page body, mounted by `views/coming-soon.stx`.
6
+ *
7
+ * Every app that turns on coming-soon mode shows this, so it names the app
8
+ * it is given and nothing else. It used to carry the Stacks logo, a pitch
9
+ * for the framework, a link to the framework's docs and a screenshot of the
10
+ * framework dashboard.
11
+ */
2
12
  interface ComingSoonProps {
3
- // No server-rendered props — the form is fully client-driven.
13
+ appName?: string
14
+ description?: string
4
15
  }
5
16
 
6
- defineProps<ComingSoonProps>()
17
+ const { appName, description } = withDefaults(
18
+ defineProps<ComingSoonProps>(),
19
+ { appName: '', description: '' },
20
+ )
7
21
  </script>
8
22
 
9
23
  <div class="overflow-hidden relative bg-white isolate">
@@ -16,22 +30,23 @@ defineProps<ComingSoonProps>()
16
30
  <rect width="100%" height="100%" stroke-width="0" fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)" />
17
31
  </svg>
18
32
 
19
- <div class="lg:flex mx-auto pb-24 pt-10 px-6 sm:pb-32 lg:px-8 lg:py-20 max-w-7xl">
20
- <div class="lg:flex-shrink-0 mx-auto lg:mx-0 lg:pt-40 max-w-2xl lg:max-w-xl">
21
- <Logo class="h-11 w-auto" />
33
+ <div class="mx-auto pb-24 pt-10 px-6 sm:pb-32 lg:px-8 lg:py-20 max-w-7xl">
34
+ <div class="mx-auto lg:pt-24 max-w-2xl">
35
+ @if(appName)
36
+ <p class="font-semibold text-gray-900 text-lg">{{ appName }}</p>
37
+ @endif
22
38
 
23
39
  <div class="mt-24 sm:mt-32 lg:mt-16">
24
40
  <span class="inline-block px-3 py-1 font-semibold leading-6 text-blue-600 text-sm bg-blue-600/10 ring-1 ring-blue-600/10 ring-inset rounded-full">Coming soon</span>
25
41
  </div>
26
42
 
27
43
  <h1 class="mt-10 font-bold text-4xl text-gray-900 tracking-tight sm:text-6xl">
28
- Develop modern apps, clouds & libraries.
29
- Faster.
44
+ Something new is on the way.
30
45
  </h1>
31
46
 
32
- <p class="mt-6 leading-8 text-gray-600 text-lg">
33
- Stacks is a type-safe full-stack framework for Web Artisans. Rapid development, for the best full-stack teams, and individuals.
34
- </p>
47
+ @if(description)
48
+ <p class="mt-6 leading-8 text-gray-600 text-lg">{{ description }}</p>
49
+ @endif
35
50
 
36
51
  <p class="mt-3 italic leading-8 text-gray-600">
37
52
  Enter your email below to stay in the loop.
@@ -61,22 +76,13 @@ defineProps<ComingSoonProps>()
61
76
  <div id="coming-soon-spinner" class="hidden h-4 w-4 animate-spin i-hugeicons-loading-01"></div>
62
77
  </button>
63
78
  </form>
64
-
65
- <a href="https://stacksjs-docs.netlify.app" class="font-semibold leading-6 text-gray-900 text-sm">Learn more <span aria-hidden="true">→</span></a>
66
79
  </div>
67
80
 
68
81
  <p id="coming-soon-error" class="hidden pt-2 text-red-500 text-xs"></p>
69
82
  <p id="coming-soon-success" class="hidden pt-2 text-green-700 text-xs">
70
- Thanks — we'll be in touch.
83
+ Thanks, we'll be in touch.
71
84
  </p>
72
85
  </div>
73
- <div class="flex lg:flex-none mt-16 mx-auto sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 xl:ml-32 max-w-2xl lg:max-w-none">
74
- <div class="flex-none max-w-3xl sm:max-w-5xl lg:max-w-none">
75
- <div class="-m-2 p-2 lg:-m-4 lg:p-4 bg-gray-900/5 ring-1 ring-gray-900/10 ring-inset rounded-xl lg:rounded-2xl">
76
- <img src="/images/screenshots/dashboard.png" alt="App screenshot" width="2432" height="1442" class="w-[76rem] ring-1 ring-gray-900/10 rounded-md shadow-2xl">
77
- </div>
78
- </div>
79
- </div>
80
86
  </div>
81
87
  </div>
82
88
 
@@ -145,7 +151,7 @@ if (form) {
145
151
  }
146
152
  }
147
153
  catch {
148
- showError('Network error — please try again.')
154
+ showError('Network error, please try again.')
149
155
  }
150
156
  finally {
151
157
  setLoading(false)
@@ -209,6 +209,7 @@ async function belongsToThisProject(specifier: string): Promise<boolean> {
209
209
  // explicitly when needed — see #1835 root cause 3.
210
210
  export async function loadAutoImports() {
211
211
  const { Glob } = await import('bun')
212
+ const { userFunctionFiles } = await import('./user-functions')
212
213
  const pathPackage = '@stacksjs/' + 'path'
213
214
  const path = await import('../../../core/path/src/index.ts')
214
215
  .catch(() => import(pathPackage))
@@ -289,12 +290,7 @@ export async function loadAutoImports() {
289
290
  const functionsPath = path.resourcesPath('functions')
290
291
  const glob = new Glob('**/*.ts')
291
292
 
292
- for await (const file of glob.scan({
293
- cwd: functionsPath,
294
- absolute: true,
295
- onlyFiles: true,
296
- })) {
297
- if (file.endsWith('.d.ts')) continue
293
+ for await (const file of userFunctionFiles(functionsPath)) {
298
294
 
299
295
  try {
300
296
  const module = await import(file)
@@ -0,0 +1,23 @@
1
+ import { Glob } from 'bun'
2
+ import { existsSync } from 'node:fs'
3
+
4
+ /**
5
+ * The `.ts` files an app keeps in `resources/functions`, or none.
6
+ *
7
+ * An app with no functions of its own is normal - `buddy new --minimal`
8
+ * leaves the folder empty, and git does not keep an empty folder - but
9
+ * `Glob.scan` throws ENOENT on a missing `cwd`. Unguarded, that aborted the
10
+ * preload for every command in a fresh checkout: CI's `buddy key:generate`
11
+ * died with a bare "no such file or directory, open .../resources/functions"
12
+ * and no stack, while the same app ran fine on the laptop that still had the
13
+ * empty folder. The model, job and controller scans below were already
14
+ * wrapped; this one was not.
15
+ */
16
+ export async function* userFunctionFiles(dir: string): AsyncGenerator<string> {
17
+ if (!existsSync(dir))
18
+ return
19
+ for await (const file of new Glob('**/*.ts').scan({ cwd: dir, absolute: true, onlyFiles: true })) {
20
+ if (!file.endsWith('.d.ts'))
21
+ yield file
22
+ }
23
+ }
@@ -2,7 +2,8 @@
2
2
  Form block: renders a @stacksjs/forms form inline on a CMS page.
3
3
  `props.formUuid` names the form; the client script fetches the public
4
4
  definition (same-origin), renders the fields, evaluates conditions live,
5
- and submits with the CSRF header the page response seeded.
5
+ and submits through useMutation, which repeats the page's CSRF cookie in
6
+ the header the submit route checks.
6
7
  --}}
7
8
  <div class="cms-form mx-auto max-w-xl px-6 py-8" data-form-uuid="{{ props.formUuid }}">
8
9
  <script client>
@@ -84,24 +85,24 @@
84
85
  payload.append('field', field.name)
85
86
  payload.append('file', file)
86
87
 
87
- const reply = await fetch(`/api/forms/${uuid}/uploads`, {
88
- method: 'POST',
89
- credentials: 'same-origin',
90
- body: payload,
91
- })
92
- const body = await reply.json().catch(() => ({}))
93
-
94
- if (!reply.ok) {
88
+ // Through the data layer rather than a bare fetch(): it is what echoes
89
+ // the CSRF cookie the submit routes require, and it passes FormData
90
+ // through untouched.
91
+ let body: { path?: string }
92
+ try {
93
+ body = await useMutation(`/api/forms/${uuid}/uploads`).mutate(payload)
94
+ }
95
+ catch (failure) {
95
96
  // Clear the input as well as the value: leaving a rejected file
96
97
  // showing next to its own error message reads as though it was taken.
97
98
  input.value = ''
98
99
  delete values[field.name]
99
100
  delete attached[field.name]
100
- errors.set({ ...errors(), [field.name]: body.message ?? 'That file could not be uploaded.' })
101
+ errors.set({ ...errors(), [field.name]: (failure as { data?: { message?: string } }).data?.message ?? 'That file could not be uploaded.' })
101
102
  return
102
103
  }
103
104
 
104
- values[field.name] = body.path
105
+ values[field.name] = body.path ?? ''
105
106
  attached[field.name] = file.name
106
107
  }
107
108
  catch {
@@ -144,20 +145,19 @@
144
145
 
145
146
  try {
146
147
  const uuid = host()?.dataset.formUuid
147
- const reply = await fetch(`/api/forms/${uuid}/submissions`, {
148
- method: 'POST',
149
- headers: { 'Content-Type': 'application/json' },
150
- credentials: 'same-origin',
151
- body: JSON.stringify({ ...values, _renderedMs: Date.now() - openedAt }),
152
- })
153
- const body = await reply.json().catch(() => ({}))
154
-
155
- if (reply.status === 422) {
156
- errors.set(body.errors ?? {})
157
- return
148
+ // useMutation, not fetch(): the submit route is CSRF-protected and the
149
+ // data layer is what repeats the cookie in the header. A bare fetch got
150
+ // 403 "CSRF token mismatch" on every submission.
151
+ let body: { message?: string, redirect?: string }
152
+ try {
153
+ body = await useMutation(`/api/forms/${uuid}/submissions`).mutate({ ...values, _renderedMs: Date.now() - openedAt })
158
154
  }
159
- if (!reply.ok) {
160
- errors.set({ _form: body.message ?? 'Something went wrong. Please try again.' })
155
+ catch (failure) {
156
+ const { status, data } = failure as { status?: number, data?: { errors?: FormErrors, message?: string } }
157
+ if (status === 422)
158
+ errors.set(data?.errors ?? {})
159
+ else
160
+ errors.set({ _form: data?.message ?? 'Something went wrong. Please try again.' })
161
161
  return
162
162
  }
163
163
  if (body.redirect) {
@@ -1,3 +1,12 @@
1
+ <script server>
2
+ import { config } from '@stacksjs/config'
3
+
4
+ // The app's own name and description, so the holding page describes the app
5
+ // that turned coming-soon mode on rather than the framework.
6
+ const appName = config.app.name || ''
7
+ const description = config.app.description || ''
8
+ </script>
9
+
1
10
  <main>
2
- <ComingSoon />
11
+ <ComingSoon appName="{{ appName }}" description="{{ description }}" />
3
12
  </main>
package/routes/forms.ts CHANGED
@@ -82,7 +82,7 @@ route.post('/api/forms/{uuid}/uploads', async (request: any) => {
82
82
  }).rateLimit(20, 'minute')
83
83
 
84
84
  route.post('/api/forms/{uuid}/submissions', async (request: any) => {
85
- const { dispatchSubmissionNotifications, loadFormByUuid, submitForm } = await import('@stacksjs/forms')
85
+ const { dispatchSubmissionNotifications, loadFormByUuid, submissionIdentity, submitForm } = await import('@stacksjs/forms')
86
86
 
87
87
  const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
88
88
  if (!form)
@@ -105,10 +105,14 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
105
105
 
106
106
  // Fire-and-forget AFTER the write: a slow mail transport must not hold a
107
107
  // parent's phone on a spinner, and a failed one must not undo the answers.
108
+ // The identity lookup belongs inside that chain too. It used to be awaited
109
+ // in the handler, so when it threw - and it always did, selecting a
110
+ // `values` column that has never existed - the visitor got a 500 for an
111
+ // answer that was already saved, and every retry saved another copy.
108
112
  if (result.submissionId > 0) {
109
- void dispatchSubmissionNotifications(form, result, {
110
- ...await submissionIdentity(result.submissionId),
111
- }).catch(() => {})
113
+ void submissionIdentity(result.submissionId)
114
+ .then(identity => dispatchSubmissionNotifications(form, result, identity))
115
+ .catch(() => {})
112
116
  }
113
117
 
114
118
  return response.json({
@@ -119,38 +123,3 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
119
123
  redirect: result.redirect,
120
124
  }, { status: 201 })
121
125
  }).rateLimit(10, 'minute')
122
-
123
- async function submissionIdentity(submissionId: number): Promise<{ email: string | null, name: string | null, values: Record<string, unknown> }> {
124
- const { db } = await import('@stacksjs/database')
125
- const row = await db
126
- .selectFrom('form_submissions')
127
- .where('id', '=', submissionId)
128
- .select(['email', 'name', 'values'])
129
- .executeTakeFirst() as { email: string | null, name: string | null, values: string | null } | undefined
130
-
131
- let values: Record<string, unknown> = {}
132
- try {
133
- values = row?.values ? JSON.parse(row.values) as Record<string, unknown> : {}
134
- }
135
- catch {
136
- // unreadable values only degrade the notification summary
137
- }
138
- return { email: row?.email ?? null, name: row?.name ?? null, values }
139
- }
140
-
141
- /** Admin CSV export. Auth'd; site scoping rides the form lookup. */
142
- route.get('/api/admin/forms/{uuid}/submissions.csv', async (request: any) => {
143
- const { exportSubmissionsCsv, loadFormByUuid } = await import('@stacksjs/forms')
144
-
145
- const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
146
- if (!form)
147
- return response.notFound('Form not found')
148
-
149
- const csv = await exportSubmissionsCsv(form)
150
- return new Response(csv, {
151
- headers: {
152
- 'Content-Type': 'text/csv; charset=utf-8',
153
- 'Content-Disposition': `attachment; filename="${form.handle}-submissions.csv"`,
154
- },
155
- })
156
- }).middleware('auth')