@stacksjs/defaults 0.70.351 → 0.70.353

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.
@@ -227,6 +227,7 @@ function concreteApiPath(
227
227
  dynamicPages: string[],
228
228
  modelRoutes: string[],
229
229
  ): string {
230
+ // eslint-disable-next-line pickier/no-unused-vars
230
231
  const match = (prefix: string) => dynamicPages
231
232
  .find(route => route.startsWith(`${prefix}/`))
232
233
  ?.slice(prefix.length + 1)
@@ -298,11 +298,34 @@ const wrappedHandler = withEvents('emails', originalHandler)
298
298
  ```
299
299
 
300
300
  ### OnQueueEvent Decorator
301
+ Subscribes an **instance method** on `new`, with `this` bound to that instance.
302
+ Declaring the class subscribes nothing, so the class has to be constructed — and
303
+ the instance has to stay referenced, because the global emitter holds listeners
304
+ weakly (a collected listener stops receiving events).
305
+
301
306
  ```typescript
302
307
  class MyHandler {
303
308
  @OnQueueEvent('job:failed')
304
309
  handleFailed(payload: QueueEventPayload) { ... }
305
310
  }
311
+
312
+ // Required: subscription happens here, and this binding keeps it alive.
313
+ export const myHandler = new MyHandler()
314
+
315
+ // Deterministic teardown (otherwise it lasts as long as the instance does)
316
+ getQueueEvents().unsubscribeListener(myHandler)
317
+ ```
318
+
319
+ Rejected with a `TypeError` at class-definition time: static methods, fields,
320
+ accessors and whole classes (no instance to bind to — use `onQueueEvent(...)`),
321
+ and legacy `experimentalDecorators` decoration (no construction-time hook).
322
+
323
+ ### Listener Introspection
324
+ ```typescript
325
+ const events = getQueueEvents()
326
+ events.subscribeListener(obj, 'job:failed', fn) // weak, `this` === obj
327
+ events.unsubscribeListener(obj) // → count removed
328
+ events.listenerCount('job:failed') // live handlers ('*' for wildcards)
306
329
  ```
307
330
 
308
331
  ### QueueMetrics
@@ -9,7 +9,7 @@ export interface AuthorInput {
9
9
  avatar: string
10
10
  }
11
11
 
12
- // eslint-disable-next-line
12
+ // eslint-disable-next-line pickier/no-unused-vars
13
13
  export function parseAuthorInput(inputRequest: RequestInstance): { data: AuthorInput } | { message: string } {
14
14
  const data = {
15
15
  name: str(inputRequest.get('name')).trim(),
@@ -10,7 +10,7 @@ export function parsePublished(value: unknown): boolean {
10
10
  return value === true || value === 1 || value === '1' || value === 'true'
11
11
  }
12
12
 
13
- // eslint-disable-next-line
13
+ // eslint-disable-next-line pickier/no-unused-vars
14
14
  export function parsePageInput(inputRequest: RequestInstance): { data: PageInput } | { message: string } {
15
15
  const data = {
16
16
  title: str(inputRequest.get('title')).trim(),
@@ -58,6 +58,7 @@ export default new Action({
58
58
  const { count: _count, ...metadata } = definition
59
59
  return formatDashboardStat(metadata, results[index])
60
60
  })
61
+ // eslint-disable-next-line pickier/no-unused-vars
61
62
  const issues = results.flatMap((result, index) => result.status === 'rejected'
62
63
  ? [{
63
64
  source: definitions[index].title,
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.351",
5
+ "version": "0.70.353",
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.70.351",
5
+ "version": "0.70.353",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -128,6 +128,81 @@ if (!isRepl && !isPostinstall) {
128
128
  // eslint-disable-next-line antfu/no-top-level-await
129
129
  // await import('bun-plugin-stx')
130
130
 
131
+ /**
132
+ * Whether a bare `@stacksjs/*` specifier resolves to something belonging to
133
+ * THIS project, and is therefore safe to import.
134
+ *
135
+ * A bare specifier resolves through node_modules, and when that is missing or
136
+ * half-installed bun falls back to its GLOBAL install cache. So a project with
137
+ * a broken install did not fail. It silently booted against whatever published
138
+ * version happened to be sitting in ~/.bun/install/cache, which is worse than
139
+ * loading nothing and completely invisible.
140
+ *
141
+ * It also hung, and that is how it was found. On Linux the first such
142
+ * cache-resolved import never settles: no rejection, no active handles, the
143
+ * process simply stops, so every stage of the preloader after it is silently
144
+ * unreachable. Both callers wrap their import in a `catch` that assumes a bad
145
+ * specifier fails FAST. That holds for one that cannot be resolved at all. It
146
+ * does not hold for one that resolves to a stale copy.
147
+ *
148
+ * ## Why this is a directory probe and not `Bun.resolveSync`
149
+ *
150
+ * The first version of this guard asked `Bun.resolveSync`, which answers the
151
+ * question exactly but pays full module resolution to do it. Measured on a
152
+ * Linux CI runner, a specifier that is NOT in `node_modules` cost **0.9 to 2.0
153
+ * seconds per call**, because bun walks the entire tree and then scans a global
154
+ * cache the install had just filled with 600+ packages. Twenty of those is 20
155
+ * to 40 seconds, so the guard turned a hang into a crawl and the preloader test
156
+ * kept timing out, intermittently, depending on how loaded the runner was.
157
+ *
158
+ * Locating the `node_modules/@stacksjs` directory once and then asking
159
+ * `existsSync` per package is the same question answered with stat calls:
160
+ * microseconds, and it never touches the global cache. The walk is memoised
161
+ * because the answer cannot change within a process.
162
+ *
163
+ * Accepted: a package present in this project's `@stacksjs` scope directory,
164
+ * which covers a real install and a vendored checkout alike (the framework's
165
+ * own core packages are symlinked into it). Anchored on `import.meta.dir`
166
+ * rather than the cwd, so running a command from a subdirectory does not change
167
+ * what loads.
168
+ */
169
+ let stacksScopeDir: string | null | undefined
170
+
171
+ async function findStacksScopeDir(): Promise<string | null> {
172
+ if (stacksScopeDir !== undefined)
173
+ return stacksScopeDir
174
+
175
+ const { existsSync } = await import('node:fs')
176
+ const { dirname, join } = await import('node:path')
177
+
178
+ let dir = import.meta.dir
179
+ for (;;) {
180
+ const candidate = join(dir, 'node_modules', '@stacksjs')
181
+ if (existsSync(candidate)) {
182
+ stacksScopeDir = candidate
183
+ return candidate
184
+ }
185
+ const parent = dirname(dir)
186
+ if (parent === dir)
187
+ break
188
+ dir = parent
189
+ }
190
+
191
+ stacksScopeDir = null
192
+ return null
193
+ }
194
+
195
+ async function belongsToThisProject(specifier: string): Promise<boolean> {
196
+ const scopeDir = await findStacksScopeDir()
197
+ if (!scopeDir)
198
+ return false
199
+
200
+ const { existsSync } = await import('node:fs')
201
+ const { join } = await import('node:path')
202
+
203
+ return existsSync(join(scopeDir, specifier.slice('@stacksjs/'.length)))
204
+ }
205
+
131
206
  // Auto-import ALL Stacks framework modules into globalThis
132
207
  // This allows using Action, response, Activity, etc. without ANY imports.
133
208
  // Exported so server entrypoints (e.g. `dev/api.ts`) can opt back in
@@ -190,6 +265,12 @@ export async function loadAutoImports() {
190
265
  ]
191
266
 
192
267
  for (const pkg of stacksPackages) {
268
+ // See `belongsToThisProject`. Skipping is what the `catch` below always
269
+ // meant to do; it just never got the chance for a specifier that resolves
270
+ // to a stale copy instead of failing.
271
+ if (!(await belongsToThisProject(pkg)))
272
+ continue
273
+
193
274
  try {
194
275
  const module = await import(pkg)
195
276
  for (const [name, value] of Object.entries(module)) {
@@ -347,12 +428,14 @@ if (!skipAutoImports) {
347
428
  await loadAutoImports()
348
429
 
349
430
  // Run package auto-discovery after all imports are loaded
350
- try {
351
- const actionsPackage = '@stacksjs/' + 'actions'
352
- const { discoverPackages } = await import(actionsPackage)
353
- await discoverPackages()
354
- }
355
- catch {
356
- // Discovery may fail during early bootstrap — not critical
431
+ const actionsPackage = '@stacksjs/' + 'actions'
432
+ if (await belongsToThisProject(actionsPackage)) {
433
+ try {
434
+ const { discoverPackages } = await import(actionsPackage)
435
+ await discoverPackages()
436
+ }
437
+ catch {
438
+ // Discovery may fail during early bootstrap — not critical
439
+ }
357
440
  }
358
441
  }
@@ -620,9 +620,9 @@ onDestroy(() => {
620
620
  theme="macos"
621
621
  width="250"
622
622
  placement="fixed"
623
- persist-key="stacks-dashboard-sidebar"
624
- shell-selector="[data-stx-content]"
625
- :follow-system-appearance="false"
623
+ persistKey="stacks-dashboard-sidebar"
624
+ shellSelector="[data-stx-content]"
625
+ :followSystemAppearance="false"
626
626
  @itemClick="handleSidebarItemClick($event)"
627
627
  >
628
628
  <template #header>
@@ -630,7 +630,7 @@ onDestroy(() => {
630
630
  titlebarHidden), so this reserves their space and acts as the window
631
631
  drag region; in a plain browser it is the same empty strip macOS
632
632
  sidebars carry above their first row. -->
633
- <SidebarHeader :show-window-controls="false" />
633
+ <SidebarHeader :showWindowControls="false" />
634
634
  </template>
635
635
  </Sidebar>
636
636
  </div>