@pikku/inspector 0.12.23 → 0.12.25

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/CHANGELOG.md CHANGED
@@ -1,3 +1,60 @@
1
+ ## 0.12.25
2
+
3
+ ### Patch Changes
4
+
5
+ - b6ba601: fix(lint): don't flag pikkuAuth's session param as a non-destructured wire
6
+
7
+ `pikkuAuth`'s handler is `(services, session)` — the second parameter is the
8
+ resolved user session, not a wires bag. The inspector was extracting "wires"
9
+ from that parameter (`extractUsedWires(handler, 1)`), so a permission like
10
+ `pikkuAuth(async ({ logger }, session) => !!session)` tripped
11
+ `wiresNotDestructured` even though `session` cannot be destructured. pikkuAuth
12
+ exposes no user-facing wires parameter, so no wires meta is recorded for it.
13
+
14
+ - ae7fc5d: Include gateway platform and auth fields in inspected gateway metadata.
15
+ - decdad5: fix(lint): don't fail the build on framework-synthesized functions
16
+
17
+ The `servicesNotDestructured`/`wiresNotDestructured` defaults (`error`) were
18
+ tripping on functions the user can't edit: generated `.gen.ts` wrappers (the
19
+ opaque `authHandler`, the cli channel raw dispatcher) and synthetic route→addon
20
+ bridges (`http:<method>:<route>`, no source file). `computeDiagnostics` now skips
21
+ any function without a real, non-generated source file, so the lint only nudges
22
+ hand-written user code. Also destructures the CLI's own `all` command.
23
+
24
+ - Updated dependencies [ae7fc5d]
25
+ - Updated dependencies [fa7a09c]
26
+ - @pikku/core@0.12.37
27
+
28
+ ## 0.12.24
29
+
30
+ ### Patch Changes
31
+
32
+ - 5fe3f47: fix(better-auth): skip the auto-generated stateless session middleware when the
33
+ project registers its own. Closes #754.
34
+
35
+ With `session.cookieCache` enabled the CLI generates a global
36
+ `betterAuthStatelessSession()` using the default `{ userId }` map. Because session
37
+ middleware short-circuits once a session is set (`if (session) next()`) and the
38
+ generated file is imported before user wirings, that default-map middleware ran
39
+ first and **pre-empted** a project's own `betterAuthStatelessSession({ mapSession })`
40
+ — silently dropping custom session fields (`role`, `locale`, …).
41
+
42
+ The inspector now detects a user-owned global registration (a
43
+ `betterAuthStatelessSession(...)` call inside `addGlobalMiddleware` or the global
44
+ form of `addHTTPMiddleware` — the array form or the `'*'` pattern, not a
45
+ route-scoped `addHTTPMiddleware('/path', …)`; ignoring `.gen.ts` files and bare
46
+ standalone calls) and
47
+ sets `state.auth.userStatelessSession`. When set, the CLI skips writing
48
+ `auth-middleware.gen.ts` (and removes a stale one) so the project's own middleware
49
+ — with its custom `mapSession` — is the only one registered. Projects without a
50
+ custom map are unaffected: the default middleware is still generated.
51
+
52
+ - 3ba12ca: Stop consumed-addon parent services from polluting every per-unit deploy bundle, and stub the AI SDKs out of non-agent units.
53
+
54
+ `aggregateRequiredServices` added `addonRequiredParentServices` (the services a consumed addon needs from its parent — e.g. `aiAgentRunner`, `deploymentService`, `metaService`) to **every** unit's `requiredServices` unconditionally. For any project that consumes an addon, this marked those services required on all units, so the per-unit service tree-shaking (and the gen-file/module stubs that key off the `false` flags) never fired — every unit shipped the full set. These parent services are now added only to units that actually deploy an addon function (its `pikkuFuncId` appears in `usedFunctions`); a unit that only calls the addon over RPC, or never touches it, no longer carries them.
55
+
56
+ On the back of the now-honest flags, the bundler stubs the AI SDK packages (`@pikku/ai-vercel`, `@ai-sdk/*`, `ai`) out of any unit where `aiAgentRunner` is not required, via a new service→module stub map alongside the existing gen-file stub map. The shared services factory must guard runner construction behind a defined-check on the dynamic import so a stubbed unit simply skips building the runner.
57
+
1
58
  ## 0.12.23
2
59
 
3
60
  ### Patch Changes
@@ -71,6 +71,36 @@ const readPluginId = (el) => {
71
71
  return el.expression.text;
72
72
  return undefined;
73
73
  };
74
+ /**
75
+ * True when `node` sits inside a GLOBAL middleware registration — i.e. an actual
76
+ * global registration, not a bare standalone call or a route-scoped one.
77
+ *
78
+ * `addGlobalMiddleware(...)` is always global. `addHTTPMiddleware` is global only
79
+ * in its array form (`addHTTPMiddleware([...])`) or with the `'*'` wildcard
80
+ * pattern; a specific route pattern (`addHTTPMiddleware('/api/admin/*', [...])`)
81
+ * scopes the middleware to that route and must NOT count as a global stateless
82
+ * registration (#754).
83
+ */
84
+ const isInsideGlobalMiddlewareRegistration = (node) => {
85
+ let parent = node.parent;
86
+ while (parent) {
87
+ if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
88
+ const fn = parent.expression.text;
89
+ if (fn === 'addGlobalMiddleware')
90
+ return true;
91
+ if (fn === 'addHTTPMiddleware') {
92
+ const first = parent.arguments[0];
93
+ if (!first)
94
+ return false;
95
+ // String first arg → route pattern (global only when '*'); otherwise
96
+ // the array form, which is global.
97
+ return ts.isStringLiteral(first) ? first.text === '*' : true;
98
+ }
99
+ }
100
+ parent = parent.parent;
101
+ }
102
+ return false;
103
+ };
74
104
  /**
75
105
  * Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
76
106
  *
@@ -94,6 +124,19 @@ export const addAuth = (logger, node, _checker, state) => {
94
124
  if (!ts.isCallExpression(node))
95
125
  return;
96
126
  const expression = node.expression;
127
+ // A user-registered stateless session middleware (custom mapSession) means the
128
+ // CLI must NOT auto-generate its own default-map one — the generated one runs
129
+ // first and pre-empts the user's via the `if (session) next()` short-circuit
130
+ // (pikkujs/pikku#754). Only a GLOBAL registration counts (inside
131
+ // addHTTPMiddleware/addGlobalMiddleware) — a bare betterAuthStatelessSession()
132
+ // call (e.g. a test harness) is not a registration. Ignore generated files so
133
+ // the emitted middleware can't self-trigger the skip.
134
+ if (ts.isIdentifier(expression) &&
135
+ expression.text === 'betterAuthStatelessSession' &&
136
+ !node.getSourceFile().fileName.endsWith('.gen.ts') &&
137
+ isInsideGlobalMiddlewareRegistration(node)) {
138
+ state.auth.userStatelessSession = true;
139
+ }
97
140
  if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
98
141
  return;
99
142
  const sourceFile = node.getSourceFile().fileName;
@@ -23,6 +23,8 @@ export const addGateway = (logger, node, checker, state, _options) => {
23
23
  const nameValue = getPropertyValue(obj, 'name');
24
24
  const typeValue = getPropertyValue(obj, 'type');
25
25
  const routeValue = getPropertyValue(obj, 'route');
26
+ const platformValue = getPropertyValue(obj, 'platform');
27
+ const authValue = getPropertyValue(obj, 'auth');
26
28
  const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker);
27
29
  if (disabled)
28
30
  return;
@@ -51,7 +53,9 @@ export const addGateway = (logger, node, checker, state, _options) => {
51
53
  ...(packageName && { packageName }),
52
54
  name: nameValue,
53
55
  type: typeValue,
54
- route: routeValue,
56
+ ...(routeValue && { route: routeValue }),
57
+ ...(platformValue && { platform: platformValue }),
58
+ ...(typeof authValue === 'boolean' && { auth: authValue }),
55
59
  gateway: true,
56
60
  summary,
57
61
  description,
@@ -141,7 +141,11 @@ export const addPermission = (logger, node, checker, state) => {
141
141
  return;
142
142
  }
143
143
  const services = extractServicesFromFunction(actualHandler);
144
- const wires = extractUsedWires(actualHandler, 1);
144
+ // pikkuAuth's handler is (services, session) — its second parameter is the
145
+ // resolved user session, NOT a wires bag, so it must not be analyzed (or
146
+ // flagged) as a non-destructured wires parameter. pikkuAuth exposes no
147
+ // user-facing wires parameter.
148
+ const wires = { optimized: true, wires: [] };
145
149
  let { pikkuFuncId, exportedName } = extractFunctionName(node, checker, state.rootDir);
146
150
  if (pikkuFuncId.startsWith('__temp_')) {
147
151
  if (ts.isVariableDeclaration(node.parent) &&
package/dist/types.d.ts CHANGED
@@ -431,6 +431,11 @@ export interface InspectorState {
431
431
  * codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
432
432
  * More than one `pikkuBetterAuth` is a critical error. */
433
433
  definition: AuthDefinition | null;
434
+ /** True when a user (non-generated) file already registers
435
+ * `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
436
+ * own default-map stateless middleware, which would otherwise pre-empt the
437
+ * user's custom mapSession (pikkujs/pikku#754). */
438
+ userStatelessSession?: boolean;
434
439
  };
435
440
  secrets: {
436
441
  definitions: SecretDefinitions;
@@ -245,9 +245,22 @@ export function aggregateRequiredServices(state) {
245
245
  if (Object.keys(state.channels.meta).length > 0) {
246
246
  requiredServices.add('eventHub');
247
247
  }
248
- // 7. Services that addons need from the parent project
249
- for (const service of state.addonRequiredParentServices ?? []) {
250
- requiredServices.add(service);
248
+ // 7. Services that consumed addons need from the parent project.
249
+ // These are required ONLY by units that actually deploy an addon function;
250
+ // a unit that merely calls the addon over RPC (or never touches it) must not
251
+ // carry them, or every per-unit bundle would over-include the addon's
252
+ // parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
253
+ // defeat per-unit tree-shaking.
254
+ const addonFuncIds = new Set();
255
+ for (const fns of Object.values(state.addonFunctions ?? {})) {
256
+ for (const id of Object.keys(fns))
257
+ addonFuncIds.add(id);
258
+ }
259
+ const unitDeploysAddonFn = [...usedFunctions].some((fn) => addonFuncIds.has(fn));
260
+ if (unitDeploysAddonFn) {
261
+ for (const service of state.addonRequiredParentServices ?? []) {
262
+ requiredServices.add(service);
263
+ }
251
264
  }
252
265
  }
253
266
  export function validateSecretOverrides(logger, state) {
@@ -512,6 +525,14 @@ export function validateSchemaWiringSeparation(logger, state) {
512
525
  export function computeDiagnostics(state) {
513
526
  const diagnostics = [];
514
527
  for (const [id, meta] of Object.entries(state.functions.meta)) {
528
+ // Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
529
+ // opaque authHandler, the cli channel's raw dispatcher) and synthetic route
530
+ // bridges that reference addon functions (id `http:<method>:<route>`, no
531
+ // source file). The user can't edit any of these, so a destructure lint
532
+ // meant to nudge them about their own code must not fail the build over them.
533
+ if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
534
+ continue;
535
+ }
515
536
  if (meta.services && !meta.services.optimized) {
516
537
  diagnostics.push({
517
538
  code: ErrorCode.SERVICES_NOT_DESTRUCTURED,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.23",
3
+ "version": "0.12.25",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.35",
38
+ "@pikku/core": "^0.12.37",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "^2.5.0",
@@ -522,4 +522,98 @@ describe('addAuth inspector', () => {
522
522
  await rm(rootDir, { recursive: true, force: true })
523
523
  }
524
524
  })
525
+
526
+ test('user-registered betterAuthStatelessSession sets userStatelessSession', async () => {
527
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-stateless-'))
528
+ const file = join(rootDir, 'middleware.ts')
529
+ await writeFile(
530
+ file,
531
+ [
532
+ "import { addHTTPMiddleware } from '#pikku'",
533
+ "import { betterAuthStatelessSession } from '@pikku/better-auth'",
534
+ "addHTTPMiddleware('*', [",
535
+ ' betterAuthStatelessSession({',
536
+ ' mapSession: (r: any) => ({ userId: r.user.id, role: r.user.role }),',
537
+ ' }),',
538
+ '])',
539
+ ].join('\n')
540
+ )
541
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
542
+ try {
543
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
544
+ assert.equal(state.auth.userStatelessSession, true)
545
+ } finally {
546
+ await rm(rootDir, { recursive: true, force: true })
547
+ }
548
+ })
549
+
550
+ test('a standalone betterAuthStatelessSession() call (not a registration) does NOT set the flag', async () => {
551
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-standalone-'))
552
+ const file = join(rootDir, 'start.ts')
553
+ await writeFile(
554
+ file,
555
+ [
556
+ "import { betterAuthStatelessSession } from '@pikku/better-auth'",
557
+ '// harness use, not a global registration',
558
+ 'const mw = betterAuthStatelessSession()',
559
+ 'void mw',
560
+ ].join('\n')
561
+ )
562
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
563
+ try {
564
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
565
+ assert.ok(
566
+ !state.auth.userStatelessSession,
567
+ 'a bare call must not count as a registration'
568
+ )
569
+ } finally {
570
+ await rm(rootDir, { recursive: true, force: true })
571
+ }
572
+ })
573
+
574
+ test('a route-scoped addHTTPMiddleware registration does NOT set the flag', async () => {
575
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-scoped-'))
576
+ const file = join(rootDir, 'middleware.ts')
577
+ await writeFile(
578
+ file,
579
+ [
580
+ "import { addHTTPMiddleware } from '#pikku'",
581
+ "import { betterAuthStatelessSession } from '@pikku/better-auth'",
582
+ "addHTTPMiddleware('/api/admin/*', [betterAuthStatelessSession()])",
583
+ ].join('\n')
584
+ )
585
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
586
+ try {
587
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
588
+ assert.ok(
589
+ !state.auth.userStatelessSession,
590
+ 'a route-scoped registration must not suppress the global generated middleware'
591
+ )
592
+ } finally {
593
+ await rm(rootDir, { recursive: true, force: true })
594
+ }
595
+ })
596
+
597
+ test('betterAuthStatelessSession in a .gen.ts file does NOT set the flag', async () => {
598
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-genonly-'))
599
+ const file = join(rootDir, 'auth-middleware.gen.ts')
600
+ await writeFile(
601
+ file,
602
+ [
603
+ "import { addHTTPMiddleware } from '#pikku'",
604
+ "import { betterAuthStatelessSession } from '@pikku/better-auth'",
605
+ "addHTTPMiddleware('*', [betterAuthStatelessSession()])",
606
+ ].join('\n')
607
+ )
608
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
609
+ try {
610
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
611
+ assert.ok(
612
+ !state.auth.userStatelessSession,
613
+ 'generated file must not self-trigger the skip'
614
+ )
615
+ } finally {
616
+ await rm(rootDir, { recursive: true, force: true })
617
+ }
618
+ })
525
619
  })
@@ -99,6 +99,35 @@ const readPluginId = (el: ts.Expression): string | undefined => {
99
99
  return undefined
100
100
  }
101
101
 
102
+ /**
103
+ * True when `node` sits inside a GLOBAL middleware registration — i.e. an actual
104
+ * global registration, not a bare standalone call or a route-scoped one.
105
+ *
106
+ * `addGlobalMiddleware(...)` is always global. `addHTTPMiddleware` is global only
107
+ * in its array form (`addHTTPMiddleware([...])`) or with the `'*'` wildcard
108
+ * pattern; a specific route pattern (`addHTTPMiddleware('/api/admin/*', [...])`)
109
+ * scopes the middleware to that route and must NOT count as a global stateless
110
+ * registration (#754).
111
+ */
112
+ const isInsideGlobalMiddlewareRegistration = (node: ts.Node): boolean => {
113
+ let parent: ts.Node | undefined = node.parent
114
+ while (parent) {
115
+ if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
116
+ const fn = parent.expression.text
117
+ if (fn === 'addGlobalMiddleware') return true
118
+ if (fn === 'addHTTPMiddleware') {
119
+ const first = parent.arguments[0]
120
+ if (!first) return false
121
+ // String first arg → route pattern (global only when '*'); otherwise
122
+ // the array form, which is global.
123
+ return ts.isStringLiteral(first) ? first.text === '*' : true
124
+ }
125
+ }
126
+ parent = parent.parent
127
+ }
128
+ return false
129
+ }
130
+
102
131
  /**
103
132
  * Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
104
133
  *
@@ -122,6 +151,23 @@ export const addAuth: AddWiring = (logger, node, _checker, state) => {
122
151
  if (!ts.isCallExpression(node)) return
123
152
 
124
153
  const expression = node.expression
154
+
155
+ // A user-registered stateless session middleware (custom mapSession) means the
156
+ // CLI must NOT auto-generate its own default-map one — the generated one runs
157
+ // first and pre-empts the user's via the `if (session) next()` short-circuit
158
+ // (pikkujs/pikku#754). Only a GLOBAL registration counts (inside
159
+ // addHTTPMiddleware/addGlobalMiddleware) — a bare betterAuthStatelessSession()
160
+ // call (e.g. a test harness) is not a registration. Ignore generated files so
161
+ // the emitted middleware can't self-trigger the skip.
162
+ if (
163
+ ts.isIdentifier(expression) &&
164
+ expression.text === 'betterAuthStatelessSession' &&
165
+ !node.getSourceFile().fileName.endsWith('.gen.ts') &&
166
+ isInsideGlobalMiddlewareRegistration(node)
167
+ ) {
168
+ state.auth.userStatelessSession = true
169
+ }
170
+
125
171
  if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
126
172
  return
127
173
 
@@ -43,7 +43,9 @@ export const addGateway: AddWiring = (
43
43
 
44
44
  const nameValue = getPropertyValue(obj, 'name') as string | null
45
45
  const typeValue = getPropertyValue(obj, 'type') as GatewayTransportType | null
46
- const routeValue = getPropertyValue(obj, 'route') as string | undefined
46
+ const routeValue = getPropertyValue(obj, 'route') as string | null
47
+ const platformValue = getPropertyValue(obj, 'platform') as string | null
48
+ const authValue = getPropertyValue(obj, 'auth')
47
49
  const { disabled, tags, summary, description, errors } =
48
50
  getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker)
49
51
 
@@ -94,7 +96,9 @@ export const addGateway: AddWiring = (
94
96
  ...(packageName && { packageName }),
95
97
  name: nameValue,
96
98
  type: typeValue,
97
- route: routeValue,
99
+ ...(routeValue && { route: routeValue }),
100
+ ...(platformValue && { platform: platformValue }),
101
+ ...(typeof authValue === 'boolean' && { auth: authValue }),
98
102
  gateway: true,
99
103
  summary,
100
104
  description,
@@ -0,0 +1,59 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
+ ({
12
+ debug: () => {},
13
+ info: () => {},
14
+ warn: () => {},
15
+ error: () => {},
16
+ diagnostic: ({ code, message }) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ critical: (code: ErrorCode, message: string) => {
20
+ criticals.push({ code, message })
21
+ },
22
+ hasCriticalErrors: () => criticals.length > 0,
23
+ }) satisfies InspectorLogger
24
+
25
+ describe('addPermission — pikkuAuth', () => {
26
+ test('does not record a wires meta for the session parameter', async () => {
27
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-auth-wires-'))
28
+ const file = join(rootDir, 'auth.ts')
29
+
30
+ await writeFile(
31
+ file,
32
+ [
33
+ 'const pikkuAuth = (x: any) => x',
34
+ 'export const isAuthenticated = pikkuAuth(async ({ logger }, session) => {',
35
+ ' logger.info({ type: "auth-check" })',
36
+ ' return !!session',
37
+ '})',
38
+ ].join('\n')
39
+ )
40
+
41
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
42
+ try {
43
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
44
+ const def = state.permissions.definitions['isAuthenticated']
45
+ assert.ok(def, 'isAuthenticated permission should be recorded')
46
+ // The pikkuAuth handler is (services, session) — session is NOT a wires
47
+ // bag and must not be flagged as a non-destructured wires parameter.
48
+ assert.equal(def.wires, undefined)
49
+ const wireDiag = (state.diagnostics ?? []).find(
50
+ (d) =>
51
+ d.code === ErrorCode.WIRES_NOT_DESTRUCTURED &&
52
+ d.message.includes('isAuthenticated')
53
+ )
54
+ assert.equal(wireDiag, undefined)
55
+ } finally {
56
+ await rm(rootDir, { recursive: true, force: true })
57
+ }
58
+ })
59
+ })
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript'
2
+ import type { FunctionWiresMeta } from '@pikku/core'
2
3
  import type { AddWiring, InspectorState } from '../types.js'
3
4
  import {
4
5
  extractFunctionName,
@@ -195,7 +196,11 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
195
196
  }
196
197
 
197
198
  const services = extractServicesFromFunction(actualHandler)
198
- const wires = extractUsedWires(actualHandler, 1)
199
+ // pikkuAuth's handler is (services, session) — its second parameter is the
200
+ // resolved user session, NOT a wires bag, so it must not be analyzed (or
201
+ // flagged) as a non-destructured wires parameter. pikkuAuth exposes no
202
+ // user-facing wires parameter.
203
+ const wires: FunctionWiresMeta = { optimized: true, wires: [] }
199
204
  let { pikkuFuncId, exportedName } = extractFunctionName(
200
205
  node,
201
206
  checker,
package/src/types.ts CHANGED
@@ -497,6 +497,11 @@ export interface InspectorState {
497
497
  * codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
498
498
  * More than one `pikkuBetterAuth` is a critical error. */
499
499
  definition: AuthDefinition | null
500
+ /** True when a user (non-generated) file already registers
501
+ * `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
502
+ * own default-map stateless middleware, which would otherwise pre-empt the
503
+ * user's custom mapSession (pikkujs/pikku#754). */
504
+ userStatelessSession?: boolean
500
505
  }
501
506
  secrets: {
502
507
  definitions: SecretDefinitions
@@ -0,0 +1,69 @@
1
+ import { test, describe } from 'node:test'
2
+ import { strict as assert } from 'node:assert'
3
+ import { computeDiagnostics } from './post-process.js'
4
+ import type { InspectorState } from '../types.js'
5
+ import { ErrorCode } from '../error-codes.js'
6
+
7
+ function stateWithFunctions(
8
+ meta: InspectorState['functions']['meta']
9
+ ): InspectorState {
10
+ return {
11
+ functions: { meta },
12
+ middleware: { definitions: {} },
13
+ permissions: { definitions: {} },
14
+ } as unknown as InspectorState
15
+ }
16
+
17
+ describe('computeDiagnostics', () => {
18
+ test('flags a user-authored function that does not destructure services', () => {
19
+ const state = stateWithFunctions({
20
+ myFunc: {
21
+ pikkuFuncId: 'myFunc',
22
+ inputSchemaName: null,
23
+ outputSchemaName: null,
24
+ sourceFile: '/project/src/my-func.ts',
25
+ services: { optimized: false, services: ['kysely'] },
26
+ },
27
+ })
28
+ computeDiagnostics(state)
29
+ assert.equal(state.diagnostics.length, 1)
30
+ assert.equal(
31
+ state.diagnostics[0].code,
32
+ ErrorCode.SERVICES_NOT_DESTRUCTURED
33
+ )
34
+ })
35
+
36
+ test('does NOT flag a generated .gen.ts function (user cannot edit it)', () => {
37
+ const state = stateWithFunctions({
38
+ cliRaw: {
39
+ pikkuFuncId: 'cliRaw',
40
+ inputSchemaName: null,
41
+ outputSchemaName: null,
42
+ sourceFile: '/project/src/wirings/cli-channel.gen.ts',
43
+ services: { optimized: false, services: ['kysely'] },
44
+ },
45
+ authHandler: {
46
+ pikkuFuncId: 'authHandler',
47
+ inputSchemaName: null,
48
+ outputSchemaName: null,
49
+ sourceFile: '/project/.pikku/auth.gen.ts',
50
+ wires: { optimized: false, wires: ['http'] },
51
+ },
52
+ })
53
+ computeDiagnostics(state)
54
+ assert.equal(state.diagnostics.length, 0)
55
+ })
56
+
57
+ test('does NOT flag a synthetic route bridge with no source file', () => {
58
+ const state = stateWithFunctions({
59
+ 'http:get:/workflow-run/:runId/stream': {
60
+ pikkuFuncId: 'http:get:/workflow-run/:runId/stream',
61
+ inputSchemaName: null,
62
+ outputSchemaName: null,
63
+ services: { optimized: false, services: [] },
64
+ },
65
+ })
66
+ computeDiagnostics(state)
67
+ assert.equal(state.diagnostics.length, 0)
68
+ })
69
+ })
@@ -317,9 +317,23 @@ export function aggregateRequiredServices(
317
317
  requiredServices.add('eventHub')
318
318
  }
319
319
 
320
- // 7. Services that addons need from the parent project
321
- for (const service of state.addonRequiredParentServices ?? []) {
322
- requiredServices.add(service)
320
+ // 7. Services that consumed addons need from the parent project.
321
+ // These are required ONLY by units that actually deploy an addon function;
322
+ // a unit that merely calls the addon over RPC (or never touches it) must not
323
+ // carry them, or every per-unit bundle would over-include the addon's
324
+ // parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
325
+ // defeat per-unit tree-shaking.
326
+ const addonFuncIds = new Set<string>()
327
+ for (const fns of Object.values(state.addonFunctions ?? {})) {
328
+ for (const id of Object.keys(fns)) addonFuncIds.add(id)
329
+ }
330
+ const unitDeploysAddonFn = [...usedFunctions].some((fn) =>
331
+ addonFuncIds.has(fn)
332
+ )
333
+ if (unitDeploysAddonFn) {
334
+ for (const service of state.addonRequiredParentServices ?? []) {
335
+ requiredServices.add(service)
336
+ }
323
337
  }
324
338
  }
325
339
 
@@ -647,6 +661,14 @@ export function computeDiagnostics(state: InspectorState): void {
647
661
  const diagnostics: InspectorDiagnostic[] = []
648
662
 
649
663
  for (const [id, meta] of Object.entries(state.functions.meta)) {
664
+ // Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
665
+ // opaque authHandler, the cli channel's raw dispatcher) and synthetic route
666
+ // bridges that reference addon functions (id `http:<method>:<route>`, no
667
+ // source file). The user can't edit any of these, so a destructure lint
668
+ // meant to nudge them about their own code must not fail the build over them.
669
+ if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
670
+ continue
671
+ }
650
672
  if (meta.services && !meta.services.optimized) {
651
673
  diagnostics.push({
652
674
  code: ErrorCode.SERVICES_NOT_DESTRUCTURED,