@pikku/inspector 0.12.45 → 0.12.47

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,48 @@
1
+ ## 0.12.47
2
+
3
+ ### Patch Changes
4
+
5
+ - 91077ff: The auth factory's services are read from the auth definition, not from the handler generated out of it.
6
+
7
+ `pikkuBetterAuth((services) => ...)`'s destructured services are what `authorize` and the session callbacks actually use, and they reach the generated services map by being stamped onto the auth handler's function meta. On the first run in a clean checkout that handler's file has not been written yet — so there is nothing to stamp, nothing to aggregate, and the map comes out marking those services unused. Run it a second time, with the file now on disk, and it says the opposite.
8
+
9
+ The consequence is not cosmetic: `RequiredSingletonServices` is built from that map, so a clean build types the services as optional and tree-shakes them out of the deployed auth worker — the exact failure the stamping exists to prevent, reintroduced by the one case where it cannot run. A CI checkout is always the first run.
10
+
11
+ The definition is inspected from hand-written source on every pass, so its services are now folded into the required set directly. A clean build and an incremental one give the same answer.
12
+
13
+ ## 0.12.46
14
+
15
+ ### Patch Changes
16
+
17
+ - a8f9a7d: Distinguish an unresolvable schema type from an unsupported schema library.
18
+
19
+ Both failures shared one message, so a plain `z.object({...})` whose type TypeScript
20
+ could not resolve — a file outside tsconfig `include`, or a generated file such as
21
+ `.pikku/db/zod.gen.ts` that had not been written yet — was reported as
22
+ "Ensure your schema is imported from a supported validation library". The schema is
23
+ dropped either way (`inputSchemaName: null`, no generated schema file), so the message
24
+ is the only signal that a function has silently lost its input contract, and that
25
+ advice is unactionable when the schema already is zod.
26
+
27
+ - eaabcbf: fix(inspector): don't reject plain destructuring in DSL workflows
28
+
29
+ `extractDestructuredDeclaration` reported "Destructuring a step result is not
30
+ supported in DSL workflows" for EVERY destructuring statement whose initializer
31
+ wasn't `await Promise.all([...])` — including ordinary local bindings that
32
+ involve no step at all, such as `const { runId } = input`.
33
+
34
+ Destructuring the workflow's own input is the single most idiomatic line in a
35
+ DSL workflow, so this rejected working workflows wholesale under a message
36
+ about step results that named nothing the author had written.
37
+
38
+ The diagnostic now fires only when the destructured initializer really is a
39
+ step (`await workflow.do(...)`, a parallel group, or a parallel fanout).
40
+ Anything else passes through as a non-step, exactly as the identifier path
41
+ already does for `const x = someLocal`.
42
+
43
+ - Updated dependencies [f11675f]
44
+ - @pikku/core@0.12.68
45
+
1
46
  ## 0.12.45
2
47
 
3
48
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import type * as ts from 'typescript';
1
+ import * as ts from 'typescript';
2
2
  import type { SchemaVendor, InspectorLogger } from '../types.js';
3
3
  /**
4
4
  * Detect the schema vendor by tracing the type back to its library origin.
@@ -1,4 +1,16 @@
1
+ import * as ts from 'typescript';
1
2
  import { ErrorCode } from '../error-codes.js';
3
+ /**
4
+ * Whether the checker could type the identifier at all.
5
+ *
6
+ * `any` here is not a schema written in an exotic library — it is TypeScript telling us
7
+ * it could not resolve the declaration, which happens when the schema comes from a file
8
+ * outside the program (not matched by tsconfig `include`) or from a generated file that
9
+ * has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
10
+ * The distinction matters because the two have opposite fixes, and conflating them told
11
+ * users to "import from a supported validation library" about a plain `z.object`.
12
+ */
13
+ const isUnresolved = (type) => (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
2
14
  /**
3
15
  * Detect the schema vendor by tracing the type back to its library origin.
4
16
  * This handles locally-defined schemas like `export const MySchema = z.object({...})`
@@ -67,6 +79,20 @@ export const detectSchemaVendor = (identifier, checker) => {
67
79
  export const detectSchemaVendorOrError = (identifier, checker, logger, context, sourceFile) => {
68
80
  const vendor = detectSchemaVendor(identifier, checker);
69
81
  if (vendor === 'unknown') {
82
+ // Two different failures used to share one message. Reporting the wrong one is not a
83
+ // cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
84
+ // generated schema file), so the message is the only thing standing between the user
85
+ // and a function that silently validates nothing — and "use a supported validation
86
+ // library" is unactionable when the schema already IS `z.object({...})`.
87
+ if (isUnresolved(checker.getTypeAtLocation(identifier))) {
88
+ logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
89
+ `TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
90
+ `resolution problem, not a schema problem: the file it is imported from is either ` +
91
+ `outside your tsconfig 'include', or has not been generated yet (a schema built ` +
92
+ `from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
93
+ `Fix the import so the type resolves, then re-run.`);
94
+ return undefined;
95
+ }
70
96
  logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema vendor could not be determined from '${sourceFile}'. ` +
71
97
  `Supported vendors: zod, valibot, arktype, @effect/schema. ` +
72
98
  `Ensure your schema is imported from a supported validation library.`);
@@ -120,6 +120,19 @@ export function aggregateRequiredServices(state) {
120
120
  addServices(funcMeta.services);
121
121
  }
122
122
  });
123
+ // 1b. Services the auth factory touches, read from the definition rather than
124
+ // from the generated handler.
125
+ //
126
+ // `stampAuthHandlerServices` copies them onto that handler's meta, but on the
127
+ // first run in a clean checkout the handler's file has not been written yet,
128
+ // so there is nothing to stamp and nothing to aggregate — and the services map
129
+ // comes out claiming `kysely` (or whatever else `authorize` reads) is unused.
130
+ // The second run, with the file on disk, says the opposite. The definition is
131
+ // inspected from hand-written source either way, so taking the answer from
132
+ // there makes a clean build agree with an incremental one.
133
+ if (state.auth?.definition) {
134
+ addServices(state.auth.definition.services);
135
+ }
123
136
  // 2. Services from used middleware (individual + groups)
124
137
  usedMiddleware.forEach((middlewareName) => {
125
138
  const middlewareMeta = state.middleware.definitions[middlewareName];
@@ -304,8 +304,15 @@ function extractVariableDeclaration(statement, context) {
304
304
  *
305
305
  * `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
306
306
  * in parallel and keep both results, so each element of the pattern is bound to
307
- * the matching child step's output. Every other destructuring shape reports a
307
+ * the matching child step's output. Every other destructuring OF A STEP reports a
308
308
  * diagnostic rather than silently dropping the step.
309
+ *
310
+ * A destructure whose initializer is NOT a step is just an ordinary local binding
311
+ * — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
312
+ * with step results. Those pass through as non-steps, exactly as the identifier
313
+ * path already does for `const x = someLocal`. Reporting them was rejecting the
314
+ * single most idiomatic line in a DSL workflow (destructuring the workflow's own
315
+ * input), under a message about step results that named nothing the author wrote.
309
316
  */
310
317
  function extractDestructuredDeclaration(statement, decl, context) {
311
318
  const init = decl.initializer;
@@ -342,6 +349,17 @@ function extractDestructuredDeclaration(statement, decl, context) {
342
349
  }
343
350
  }
344
351
  }
352
+ // Only a destructure OF A STEP is an error. Anything else is a plain local
353
+ // binding the DSL simply doesn't model as a step.
354
+ const stepCall = init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
355
+ ? init.expression
356
+ : null;
357
+ const destructuresAStep = stepCall !== null &&
358
+ (isWorkflowDoCall(stepCall, context.checker) ||
359
+ isParallelGroup(stepCall) ||
360
+ isParallelFanout(stepCall));
361
+ if (!destructuresAStep)
362
+ return null;
345
363
  context.errors.push({
346
364
  message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
347
365
  node: statement,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.45",
3
+ "version": "0.12.47",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -44,10 +44,10 @@
44
44
  "zod-to-ts": "^2.1.0"
45
45
  },
46
46
  "peerDependencies": {
47
- "@pikku/core": "^0.12.67"
47
+ "@pikku/core": "^0.12.68"
48
48
  },
49
49
  "devDependencies": {
50
- "@pikku/core": "^0.12.67",
50
+ "@pikku/core": "^0.12.68",
51
51
  "@types/node": "^24.11.0"
52
52
  },
53
53
  "engines": {
@@ -0,0 +1,123 @@
1
+ import { test, describe, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
4
+ import { tmpdir } from 'os'
5
+ import { join } from 'path'
6
+ import * as ts from 'typescript'
7
+
8
+ import { detectSchemaVendorOrError } from './detect-schema-vendor.js'
9
+
10
+ let projectDir: string
11
+ let program: ts.Program
12
+ let checker: ts.TypeChecker
13
+ let sourceFile: ts.SourceFile
14
+
15
+ const criticals: string[] = []
16
+ const logger = {
17
+ debug() {},
18
+ info() {},
19
+ warn() {},
20
+ error() {},
21
+ critical(_code: unknown, message: string) {
22
+ criticals.push(message)
23
+ },
24
+ } as any
25
+
26
+ const initializerIdentifier = (name: string): ts.Identifier => {
27
+ let found: ts.Identifier | undefined
28
+ const visit = (node: ts.Node) => {
29
+ if (
30
+ ts.isVariableDeclaration(node) &&
31
+ ts.isIdentifier(node.name) &&
32
+ node.name.text === name &&
33
+ node.initializer &&
34
+ ts.isIdentifier(node.initializer)
35
+ ) {
36
+ found = node.initializer
37
+ }
38
+ ts.forEachChild(node, visit)
39
+ }
40
+ visit(sourceFile)
41
+ assert.ok(found, `no identifier initializer found for '${name}'`)
42
+ return found
43
+ }
44
+
45
+ before(() => {
46
+ projectDir = mkdtempSync(join(tmpdir(), 'pikku-schema-vendor-'))
47
+ mkdirSync(join(projectDir, 'src'))
48
+ writeFileSync(
49
+ join(projectDir, 'src', 'schemas.ts'),
50
+ [
51
+ // Imported from a file that does not exist — exactly what a schema built
52
+ // from an ungenerated '.pikku/db/zod.gen.ts' looks like to the checker.
53
+ `import { UngeneratedSchema } from './not-written-yet.js'`,
54
+ `export const unresolved = UngeneratedSchema`,
55
+ `const plainObject = { a: 1 }`,
56
+ `export const resolvedButNotASchema = plainObject`,
57
+ '',
58
+ ].join('\n')
59
+ )
60
+
61
+ program = ts.createProgram([join(projectDir, 'src', 'schemas.ts')], {
62
+ target: ts.ScriptTarget.ESNext,
63
+ module: ts.ModuleKind.NodeNext,
64
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
65
+ strict: true,
66
+ skipLibCheck: true,
67
+ noEmit: true,
68
+ })
69
+ checker = program.getTypeChecker()
70
+ sourceFile = program.getSourceFile(join(projectDir, 'src', 'schemas.ts'))!
71
+ })
72
+
73
+ after(() => {
74
+ rmSync(projectDir, { recursive: true, force: true })
75
+ })
76
+
77
+ describe('detectSchemaVendorOrError', () => {
78
+ test('reports a type the checker could not resolve as a resolution failure', () => {
79
+ criticals.length = 0
80
+
81
+ const vendor = detectSchemaVendorOrError(
82
+ initializerIdentifier('unresolved'),
83
+ checker,
84
+ logger,
85
+ `Function 'createTodo' input`,
86
+ 'src/schemas.ts'
87
+ )
88
+
89
+ assert.equal(vendor, undefined)
90
+ assert.equal(criticals.length, 1)
91
+ const [message] = criticals
92
+ assert.match(message, /could not be TYPED/)
93
+ assert.match(message, /UngeneratedSchema/)
94
+ assert.match(message, /pikku db generate/)
95
+ // The unactionable advice must NOT be given for a type that simply failed
96
+ // to resolve — the schema may well already be zod.
97
+ assert.doesNotMatch(
98
+ message,
99
+ /Ensure your schema is imported from a supported validation library/
100
+ )
101
+ })
102
+
103
+ test('still reports a resolvable non-vendor type as an unsupported library', () => {
104
+ criticals.length = 0
105
+
106
+ const vendor = detectSchemaVendorOrError(
107
+ initializerIdentifier('resolvedButNotASchema'),
108
+ checker,
109
+ logger,
110
+ `Function 'createTodo' input`,
111
+ 'src/schemas.ts'
112
+ )
113
+
114
+ assert.equal(vendor, undefined)
115
+ assert.equal(criticals.length, 1)
116
+ const [message] = criticals
117
+ assert.match(
118
+ message,
119
+ /Ensure your schema is imported from a supported validation library/
120
+ )
121
+ assert.doesNotMatch(message, /could not be TYPED/)
122
+ })
123
+ })
@@ -1,7 +1,20 @@
1
- import type * as ts from 'typescript'
1
+ import * as ts from 'typescript'
2
2
  import type { SchemaVendor, InspectorLogger } from '../types.js'
3
3
  import { ErrorCode } from '../error-codes.js'
4
4
 
5
+ /**
6
+ * Whether the checker could type the identifier at all.
7
+ *
8
+ * `any` here is not a schema written in an exotic library — it is TypeScript telling us
9
+ * it could not resolve the declaration, which happens when the schema comes from a file
10
+ * outside the program (not matched by tsconfig `include`) or from a generated file that
11
+ * has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
12
+ * The distinction matters because the two have opposite fixes, and conflating them told
13
+ * users to "import from a supported validation library" about a plain `z.object`.
14
+ */
15
+ const isUnresolved = (type: ts.Type): boolean =>
16
+ (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
17
+
5
18
  /**
6
19
  * Detect the schema vendor by tracing the type back to its library origin.
7
20
  * This handles locally-defined schemas like `export const MySchema = z.object({...})`
@@ -78,6 +91,23 @@ export const detectSchemaVendorOrError = (
78
91
  ): Exclude<SchemaVendor, 'unknown'> | undefined => {
79
92
  const vendor = detectSchemaVendor(identifier, checker)
80
93
  if (vendor === 'unknown') {
94
+ // Two different failures used to share one message. Reporting the wrong one is not a
95
+ // cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
96
+ // generated schema file), so the message is the only thing standing between the user
97
+ // and a function that silently validates nothing — and "use a supported validation
98
+ // library" is unactionable when the schema already IS `z.object({...})`.
99
+ if (isUnresolved(checker.getTypeAtLocation(identifier))) {
100
+ logger.critical(
101
+ ErrorCode.INLINE_SCHEMA,
102
+ `${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
103
+ `TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
104
+ `resolution problem, not a schema problem: the file it is imported from is either ` +
105
+ `outside your tsconfig 'include', or has not been generated yet (a schema built ` +
106
+ `from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
107
+ `Fix the import so the type resolves, then re-run.`
108
+ )
109
+ return undefined
110
+ }
81
111
  logger.critical(
82
112
  ErrorCode.INLINE_SCHEMA,
83
113
  `${context} schema vendor could not be determined from '${sourceFile}'. ` +
@@ -63,6 +63,7 @@ function makeState(
63
63
  functionsMeta?: Record<string, any>
64
64
  addonFunctions?: Record<string, Record<string, any>>
65
65
  addonRequiredParentServices?: string[]
66
+ authServices?: string[]
66
67
  } = {}
67
68
  ): Omit<InspectorState, 'typesLookup'> {
68
69
  return {
@@ -99,6 +100,13 @@ function makeState(
99
100
  rpc: { internalMeta: {}, exposedMeta: {} },
100
101
  addonFunctions: overrides.addonFunctions ?? {},
101
102
  addonRequiredParentServices: overrides.addonRequiredParentServices ?? [],
103
+ auth: overrides.authServices
104
+ ? {
105
+ definition: {
106
+ services: { optimized: true, services: overrides.authServices },
107
+ },
108
+ }
109
+ : {},
102
110
  } as unknown as Omit<InspectorState, 'typesLookup'>
103
111
  }
104
112
 
@@ -306,6 +314,26 @@ describe('aggregateRequiredServices — per-function addon services', () => {
306
314
  })
307
315
  })
308
316
 
317
+ describe('aggregateRequiredServices — the auth factory’s own services', () => {
318
+ test('an auth definition contributes its services before its handler exists', () => {
319
+ // The state a clean checkout produces: the project declares
320
+ // `pikkuBetterAuth`, so the definition is inspected, but the handler the CLI
321
+ // generates from it has not been written yet — so nothing in functions.meta
322
+ // mentions the services `authorize` reads.
323
+ const state = makeState({ authServices: ['kysely', 'jwt'] })
324
+ aggregateRequiredServices(state)
325
+ const required = state.serviceAggregation.requiredServices
326
+ assert.ok(required.has('kysely'))
327
+ assert.ok(required.has('jwt'))
328
+ })
329
+
330
+ test('a project without auth is unaffected', () => {
331
+ const state = makeState()
332
+ aggregateRequiredServices(state)
333
+ assert.equal(state.serviceAggregation.requiredServices.size, 0)
334
+ })
335
+ })
336
+
309
337
  describe('override validation resolves the override target (value), not the logical key', () => {
310
338
  test('validateSecretOverrides accepts an override whose value is a declared secret', () => {
311
339
  const { logger, criticals } = makeCriticalLogger()
@@ -422,7 +450,10 @@ describe('validateRemoteAddonDependencies (wireRemoteAddon must be a devDependen
422
450
  })
423
451
  )
424
452
  assert.equal(criticals.length, 1)
425
- assert.equal(criticals[0]!.code, ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY)
453
+ assert.equal(
454
+ criticals[0]!.code,
455
+ ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY
456
+ )
426
457
  assert.match(criticals[0]!.message, /devDependencies/)
427
458
  } finally {
428
459
  rmSync(dir, { recursive: true, force: true })
@@ -441,7 +472,10 @@ describe('validateRemoteAddonDependencies (wireRemoteAddon must be a devDependen
441
472
  })
442
473
  )
443
474
  assert.equal(criticals.length, 1)
444
- assert.equal(criticals[0]!.code, ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY)
475
+ assert.equal(
476
+ criticals[0]!.code,
477
+ ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY
478
+ )
445
479
  } finally {
446
480
  rmSync(dir, { recursive: true, force: true })
447
481
  }
@@ -154,6 +154,20 @@ export function aggregateRequiredServices(
154
154
  }
155
155
  })
156
156
 
157
+ // 1b. Services the auth factory touches, read from the definition rather than
158
+ // from the generated handler.
159
+ //
160
+ // `stampAuthHandlerServices` copies them onto that handler's meta, but on the
161
+ // first run in a clean checkout the handler's file has not been written yet,
162
+ // so there is nothing to stamp and nothing to aggregate — and the services map
163
+ // comes out claiming `kysely` (or whatever else `authorize` reads) is unused.
164
+ // The second run, with the file on disk, says the opposite. The definition is
165
+ // inspected from hand-written source either way, so taking the answer from
166
+ // there makes a clean build agree with an incremental one.
167
+ if (state.auth?.definition) {
168
+ addServices(state.auth.definition.services)
169
+ }
170
+
157
171
  // 2. Services from used middleware (individual + groups)
158
172
  usedMiddleware.forEach((middlewareName) => {
159
173
  const middlewareMeta = state.middleware.definitions[middlewareName]
@@ -447,8 +447,15 @@ function extractVariableDeclaration(
447
447
  *
448
448
  * `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
449
449
  * in parallel and keep both results, so each element of the pattern is bound to
450
- * the matching child step's output. Every other destructuring shape reports a
450
+ * the matching child step's output. Every other destructuring OF A STEP reports a
451
451
  * diagnostic rather than silently dropping the step.
452
+ *
453
+ * A destructure whose initializer is NOT a step is just an ordinary local binding
454
+ * — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
455
+ * with step results. Those pass through as non-steps, exactly as the identifier
456
+ * path already does for `const x = someLocal`. Reporting them was rejecting the
457
+ * single most idiomatic line in a DSL workflow (destructuring the workflow's own
458
+ * input), under a message about step results that named nothing the author wrote.
452
459
  */
453
460
  function extractDestructuredDeclaration(
454
461
  statement: ts.VariableStatement,
@@ -499,6 +506,19 @@ function extractDestructuredDeclaration(
499
506
  }
500
507
  }
501
508
 
509
+ // Only a destructure OF A STEP is an error. Anything else is a plain local
510
+ // binding the DSL simply doesn't model as a step.
511
+ const stepCall =
512
+ init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
513
+ ? init.expression
514
+ : null
515
+ const destructuresAStep =
516
+ stepCall !== null &&
517
+ (isWorkflowDoCall(stepCall, context.checker) ||
518
+ isParallelGroup(stepCall) ||
519
+ isParallelFanout(stepCall))
520
+ if (!destructuresAStep) return null
521
+
502
522
  context.errors.push({
503
523
  message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
504
524
  node: statement,