@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 +6 -0
- package/dist/inspector.js +8 -2
- package/dist/utils/nested-project-filter.d.ts +1 -0
- package/dist/utils/nested-project-filter.js +29 -0
- package/dist/utils/workflow/dsl/patterns.d.ts +6 -0
- package/dist/utils/workflow/dsl/patterns.js +13 -8
- package/dist/utils/workflow/dsl/validation.js +3 -4
- package/package.json +1 -1
- package/src/inspector.ts +8 -2
- package/src/utils/nested-project-filter.test.ts +53 -0
- package/src/utils/nested-project-filter.ts +28 -0
- package/src/utils/workflow/dsl/patterns.ts +16 -8
- package/src/utils/workflow/dsl/validation.ts +3 -4
- package/tsconfig.tsbuildinfo +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
propAccess.expression.text === 'workflow'
|
|
157
|
+
isWorkflowWireIdentifier(propAccess.expression)
|
|
159
158
|
) {
|
|
160
159
|
if (!parentIsAwait && !insidePromiseAll) {
|
|
161
160
|
errors.push({
|