@pikku/inspector 0.12.41 → 0.12.42

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,9 @@
1
+ ## 0.12.42
2
+
3
+ ### Patch Changes
4
+
5
+ - 854c342: Fix workspace addon integration: exclude nested pikku projects from inspection (prevents "More than one CoreUserSession/CoreConfig found" when a workspace addon is linked), widen the generated addon service `call()` data param to `unknown` so schema-less function inputs compile, and add `@pikku/inspector` + `@standard-schema/spec` to the generated addon devDependencies so its `.pikku` gen files typecheck.
6
+
1
7
  ## 0.12.41
2
8
 
3
9
  ### Patch Changes
package/dist/inspector.js CHANGED
@@ -5,6 +5,7 @@ import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
5
5
  import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js';
8
+ import { createNestedProjectFilter } from './utils/nested-project-filter.js';
8
9
  import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
9
10
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
10
11
  import { pikkuState } from '@pikku/core/internal';
@@ -225,17 +226,22 @@ export const inspect = async (logger, routeFiles, options = {}) => {
225
226
  const startSourceFiles = performance.now();
226
227
  // node_modules under rootDir (e.g. a locally-installed addon) is a
227
228
  // dependency, not project source — scanning it double-counts the addon's
228
- // own application types (CoreConfig/Services/SingletonServices).
229
+ // own application types (CoreConfig/Services/SingletonServices). Nested
230
+ // pikku projects (a workspace addon at packages/<addon>) are the same case:
231
+ // TS resolves the node_modules symlink to its realpath under rootDir, so
232
+ // the node_modules check alone misses them.
229
233
  // Sort by file name so the sweeps populate state in a stable order. The
230
234
  // program's own file order depends on glob + import-graph resolution, which
231
235
  // varies run to run — leaving generated meta keys (and anything serialized
232
236
  // in insertion order) non-reproducible across identical `pikku all` runs.
233
237
  // Safe because function registration is a dedicated pass (visitFunctions)
234
238
  // that completes before any order-sensitive wiring resolution in visitRoutes.
239
+ const isNestedProjectFile = createNestedProjectFilter(rootDir);
235
240
  const sourceFiles = program
236
241
  .getSourceFiles()
237
242
  .filter((sf) => sf.fileName.startsWith(rootDir) &&
238
- !sf.fileName.includes('/node_modules/'))
243
+ !sf.fileName.includes('/node_modules/') &&
244
+ !isNestedProjectFile(sf.fileName))
239
245
  .sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
240
246
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
241
247
  const state = getInitialInspectorState(rootDir);
@@ -0,0 +1 @@
1
+ export declare const createNestedProjectFilter: (rootDir: string) => (fileName: string) => boolean;
@@ -0,0 +1,29 @@
1
+ import { existsSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ export const createNestedProjectFilter = (rootDir) => {
4
+ const cache = new Map();
5
+ return (fileName) => {
6
+ const pending = [];
7
+ let dir = dirname(fileName);
8
+ let nested = false;
9
+ while (dir.startsWith(rootDir) && dir !== rootDir) {
10
+ const cached = cache.get(dir);
11
+ if (cached !== undefined) {
12
+ nested = cached;
13
+ break;
14
+ }
15
+ pending.push(dir);
16
+ if (existsSync(join(dir, 'pikku.config.json'))) {
17
+ nested = true;
18
+ break;
19
+ }
20
+ const parent = dirname(dir);
21
+ if (parent === dir)
22
+ break;
23
+ dir = parent;
24
+ }
25
+ for (const d of pending)
26
+ cache.set(d, nested);
27
+ return nested;
28
+ };
29
+ };
@@ -2,6 +2,12 @@ import * as ts from 'typescript';
2
2
  /**
3
3
  * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
+ /**
6
+ * The wire object handed to a DSL func is named `workflow` in a workflow and
7
+ * `scenario` in a scenario — both carry the same `do`/`sleep`/`suspend` DSL, so
8
+ * step extraction accepts either identifier.
9
+ */
10
+ export declare function isWorkflowWireIdentifier(expr: ts.Expression): boolean;
5
11
  /**
6
12
  * Check if a call expression is workflow.do() or workflow.expectEventually()
7
13
  * (both are RPC steps; expectEventually is the polling variant used by scenarios)
@@ -2,6 +2,15 @@ import * as ts from 'typescript';
2
2
  /**
3
3
  * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
+ /**
6
+ * The wire object handed to a DSL func is named `workflow` in a workflow and
7
+ * `scenario` in a scenario — both carry the same `do`/`sleep`/`suspend` DSL, so
8
+ * step extraction accepts either identifier.
9
+ */
10
+ export function isWorkflowWireIdentifier(expr) {
11
+ return (ts.isIdentifier(expr) &&
12
+ (expr.text === 'workflow' || expr.text === 'scenario'));
13
+ }
5
14
  /**
6
15
  * Check if a call expression is workflow.do() or workflow.expectEventually()
7
16
  * (both are RPC steps; expectEventually is the polling variant used by scenarios)
@@ -13,8 +22,7 @@ export function isWorkflowDoCall(node, checker) {
13
22
  const propAccess = node.expression;
14
23
  return ((propAccess.name.text === 'do' ||
15
24
  propAccess.name.text === 'expectEventually') &&
16
- ts.isIdentifier(propAccess.expression) &&
17
- propAccess.expression.text === 'workflow');
25
+ isWorkflowWireIdentifier(propAccess.expression));
18
26
  }
19
27
  /**
20
28
  * Check if a call expression is workflow.expectEventually()
@@ -22,8 +30,7 @@ export function isWorkflowDoCall(node, checker) {
22
30
  export function isWorkflowExpectEventuallyCall(node) {
23
31
  return (ts.isPropertyAccessExpression(node.expression) &&
24
32
  node.expression.name.text === 'expectEventually' &&
25
- ts.isIdentifier(node.expression.expression) &&
26
- node.expression.expression.text === 'workflow');
33
+ isWorkflowWireIdentifier(node.expression.expression));
27
34
  }
28
35
  /**
29
36
  * Extract the actor NAME from a step options object literal:
@@ -58,8 +65,7 @@ export function isWorkflowSleepCall(node, checker) {
58
65
  }
59
66
  const propAccess = node.expression;
60
67
  return (propAccess.name.text === 'sleep' &&
61
- ts.isIdentifier(propAccess.expression) &&
62
- propAccess.expression.text === 'workflow');
68
+ isWorkflowWireIdentifier(propAccess.expression));
63
69
  }
64
70
  /**
65
71
  * Check if a call expression is workflow.suspend()
@@ -70,8 +76,7 @@ export function isWorkflowSuspendCall(node, _checker) {
70
76
  }
71
77
  const propAccess = node.expression;
72
78
  return (propAccess.name.text === 'suspend' &&
73
- ts.isIdentifier(propAccess.expression) &&
74
- propAccess.expression.text === 'workflow');
79
+ isWorkflowWireIdentifier(propAccess.expression));
75
80
  }
76
81
  /**
77
82
  * Check if a throw statement throws WorkflowCancelledException
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript';
2
+ import { isWorkflowWireIdentifier } from './patterns.js';
2
3
  /**
3
4
  * Check if a node contains only allowed patterns
4
5
  *
@@ -58,8 +59,7 @@ export function validateNoDisallowedPatterns(node, options) {
58
59
  if (ts.isPropertyAccessExpression(node.expression)) {
59
60
  const propAccess = node.expression;
60
61
  if (propAccess.name.text === 'do' &&
61
- ts.isIdentifier(propAccess.expression) &&
62
- propAccess.expression.text === 'workflow') {
62
+ isWorkflowWireIdentifier(propAccess.expression)) {
63
63
  const secondArg = node.arguments[1];
64
64
  if (secondArg &&
65
65
  (ts.isArrowFunction(secondArg) ||
@@ -116,8 +116,7 @@ export function validateAwaitedCalls(node) {
116
116
  if (ts.isPropertyAccessExpression(node.expression)) {
117
117
  const propAccess = node.expression;
118
118
  if ((propAccess.name.text === 'do' || propAccess.name.text === 'sleep') &&
119
- ts.isIdentifier(propAccess.expression) &&
120
- propAccess.expression.text === 'workflow') {
119
+ isWorkflowWireIdentifier(propAccess.expression)) {
121
120
  if (!parentIsAwait && !insidePromiseAll) {
122
121
  errors.push({
123
122
  message: `workflow.${propAccess.name.text}() must be awaited`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.41",
3
+ "version": "0.12.42",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
package/src/inspector.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  } from './types.js'
11
11
  import { getFilesAndMethods } from './utils/get-files-and-methods.js'
12
12
  import { findCommonAncestor } from './utils/find-root-dir.js'
13
+ import { createNestedProjectFilter } from './utils/nested-project-filter.js'
13
14
  import {
14
15
  aggregateRequiredServices,
15
16
  stampAuthHandlerServices,
@@ -272,19 +273,24 @@ export const inspect = async (
272
273
  const startSourceFiles = performance.now()
273
274
  // node_modules under rootDir (e.g. a locally-installed addon) is a
274
275
  // dependency, not project source — scanning it double-counts the addon's
275
- // own application types (CoreConfig/Services/SingletonServices).
276
+ // own application types (CoreConfig/Services/SingletonServices). Nested
277
+ // pikku projects (a workspace addon at packages/<addon>) are the same case:
278
+ // TS resolves the node_modules symlink to its realpath under rootDir, so
279
+ // the node_modules check alone misses them.
276
280
  // Sort by file name so the sweeps populate state in a stable order. The
277
281
  // program's own file order depends on glob + import-graph resolution, which
278
282
  // varies run to run — leaving generated meta keys (and anything serialized
279
283
  // in insertion order) non-reproducible across identical `pikku all` runs.
280
284
  // Safe because function registration is a dedicated pass (visitFunctions)
281
285
  // that completes before any order-sensitive wiring resolution in visitRoutes.
286
+ const isNestedProjectFile = createNestedProjectFilter(rootDir)
282
287
  const sourceFiles = program
283
288
  .getSourceFiles()
284
289
  .filter(
285
290
  (sf) =>
286
291
  sf.fileName.startsWith(rootDir) &&
287
- !sf.fileName.includes('/node_modules/')
292
+ !sf.fileName.includes('/node_modules/') &&
293
+ !isNestedProjectFile(sf.fileName)
288
294
  )
289
295
  .sort((a, b) =>
290
296
  a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0
@@ -0,0 +1,53 @@
1
+ import { test } from 'node:test'
2
+ import * as assert from 'node:assert'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
4
+ import { tmpdir } from 'os'
5
+ import { join } from 'path'
6
+ import { createNestedProjectFilter } from './nested-project-filter.js'
7
+
8
+ test('createNestedProjectFilter', async (t) => {
9
+ const root = mkdtempSync(join(tmpdir(), 'pikku-nested-'))
10
+ t.after(() => rmSync(root, { recursive: true, force: true }))
11
+
12
+ writeFileSync(join(root, 'pikku.config.json'), '{}')
13
+ mkdirSync(join(root, 'packages/functions/src'), { recursive: true })
14
+ mkdirSync(join(root, 'packages/addon/src'), { recursive: true })
15
+ mkdirSync(join(root, 'packages/addon/types'), { recursive: true })
16
+ writeFileSync(join(root, 'packages/addon/pikku.config.json'), '{}')
17
+
18
+ const isNested = createNestedProjectFilter(root)
19
+
20
+ await t.test('keeps project source files', () => {
21
+ assert.strictEqual(
22
+ isNested(join(root, 'packages/functions/src/auth.ts')),
23
+ false
24
+ )
25
+ })
26
+
27
+ await t.test('keeps files directly under rootDir', () => {
28
+ assert.strictEqual(isNested(join(root, 'services.ts')), false)
29
+ })
30
+
31
+ await t.test('excludes files inside a nested pikku project', () => {
32
+ assert.strictEqual(
33
+ isNested(join(root, 'packages/addon/types/application-types.d.ts')),
34
+ true
35
+ )
36
+ assert.strictEqual(
37
+ isNested(join(root, 'packages/addon/src/index.ts')),
38
+ true
39
+ )
40
+ })
41
+
42
+ await t.test('keeps files outside rootDir untouched', () => {
43
+ assert.strictEqual(isNested('/somewhere/else/file.ts'), false)
44
+ })
45
+
46
+ await t.test('rootDir itself is not treated as nested', () => {
47
+ const addonScoped = createNestedProjectFilter(join(root, 'packages/addon'))
48
+ assert.strictEqual(
49
+ addonScoped(join(root, 'packages/addon/src/index.ts')),
50
+ false
51
+ )
52
+ })
53
+ })
@@ -0,0 +1,28 @@
1
+ import { existsSync } from 'fs'
2
+ import { dirname, join } from 'path'
3
+
4
+ export const createNestedProjectFilter = (rootDir: string) => {
5
+ const cache = new Map<string, boolean>()
6
+ return (fileName: string): boolean => {
7
+ const pending: string[] = []
8
+ let dir = dirname(fileName)
9
+ let nested = false
10
+ while (dir.startsWith(rootDir) && dir !== rootDir) {
11
+ const cached = cache.get(dir)
12
+ if (cached !== undefined) {
13
+ nested = cached
14
+ break
15
+ }
16
+ pending.push(dir)
17
+ if (existsSync(join(dir, 'pikku.config.json'))) {
18
+ nested = true
19
+ break
20
+ }
21
+ const parent = dirname(dir)
22
+ if (parent === dir) break
23
+ dir = parent
24
+ }
25
+ for (const d of pending) cache.set(d, nested)
26
+ return nested
27
+ }
28
+ }
@@ -4,6 +4,18 @@ import * as ts from 'typescript'
4
4
  * Pattern detection helpers for DSL workflow extraction
5
5
  */
6
6
 
7
+ /**
8
+ * The wire object handed to a DSL func is named `workflow` in a workflow and
9
+ * `scenario` in a scenario — both carry the same `do`/`sleep`/`suspend` DSL, so
10
+ * step extraction accepts either identifier.
11
+ */
12
+ export function isWorkflowWireIdentifier(expr: ts.Expression): boolean {
13
+ return (
14
+ ts.isIdentifier(expr) &&
15
+ (expr.text === 'workflow' || expr.text === 'scenario')
16
+ )
17
+ }
18
+
7
19
  /**
8
20
  * Check if a call expression is workflow.do() or workflow.expectEventually()
9
21
  * (both are RPC steps; expectEventually is the polling variant used by scenarios)
@@ -20,8 +32,7 @@ export function isWorkflowDoCall(
20
32
  return (
21
33
  (propAccess.name.text === 'do' ||
22
34
  propAccess.name.text === 'expectEventually') &&
23
- ts.isIdentifier(propAccess.expression) &&
24
- propAccess.expression.text === 'workflow'
35
+ isWorkflowWireIdentifier(propAccess.expression)
25
36
  )
26
37
  }
27
38
 
@@ -34,8 +45,7 @@ export function isWorkflowExpectEventuallyCall(
34
45
  return (
35
46
  ts.isPropertyAccessExpression(node.expression) &&
36
47
  node.expression.name.text === 'expectEventually' &&
37
- ts.isIdentifier(node.expression.expression) &&
38
- node.expression.expression.text === 'workflow'
48
+ isWorkflowWireIdentifier(node.expression.expression)
39
49
  )
40
50
  }
41
51
 
@@ -83,8 +93,7 @@ export function isWorkflowSleepCall(
83
93
  const propAccess = node.expression
84
94
  return (
85
95
  propAccess.name.text === 'sleep' &&
86
- ts.isIdentifier(propAccess.expression) &&
87
- propAccess.expression.text === 'workflow'
96
+ isWorkflowWireIdentifier(propAccess.expression)
88
97
  )
89
98
  }
90
99
 
@@ -102,8 +111,7 @@ export function isWorkflowSuspendCall(
102
111
  const propAccess = node.expression
103
112
  return (
104
113
  propAccess.name.text === 'suspend' &&
105
- ts.isIdentifier(propAccess.expression) &&
106
- propAccess.expression.text === 'workflow'
114
+ isWorkflowWireIdentifier(propAccess.expression)
107
115
  )
108
116
  }
109
117
 
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript'
2
+ import { isWorkflowWireIdentifier } from './patterns.js'
2
3
 
3
4
  /**
4
5
  * Validation rules for DSL workflows
@@ -78,8 +79,7 @@ export function validateNoDisallowedPatterns(
78
79
  const propAccess = node.expression
79
80
  if (
80
81
  propAccess.name.text === 'do' &&
81
- ts.isIdentifier(propAccess.expression) &&
82
- propAccess.expression.text === 'workflow'
82
+ isWorkflowWireIdentifier(propAccess.expression)
83
83
  ) {
84
84
  const secondArg = node.arguments[1]
85
85
  if (
@@ -154,8 +154,7 @@ export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
154
154
  const propAccess = node.expression
155
155
  if (
156
156
  (propAccess.name.text === 'do' || propAccess.name.text === 'sleep') &&
157
- ts.isIdentifier(propAccess.expression) &&
158
- propAccess.expression.text === 'workflow'
157
+ isWorkflowWireIdentifier(propAccess.expression)
159
158
  ) {
160
159
  if (!parentIsAwait && !insidePromiseAll) {
161
160
  errors.push({