@pikku/inspector 0.12.38 → 0.12.40
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 +36 -0
- package/dist/add/add-functions.js +25 -0
- package/dist/error-codes.d.ts +4 -16
- package/dist/error-codes.js +5 -18
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/post-process.js +8 -0
- package/dist/utils/serialize-inspector-state.js +1 -0
- package/package.json +4 -4
- package/src/add/add-functions.ts +31 -0
- package/src/add/dynamic-import-check.test.ts +119 -0
- package/src/error-codes-emitted.test.ts +76 -0
- package/src/error-codes.ts +6 -20
- package/src/inspector.ts +1 -0
- package/src/types.ts +6 -0
- package/src/utils/compute-diagnostics.test.ts +43 -6
- package/src/utils/post-process.ts +8 -0
- package/src/utils/serialize-inspector-state.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
## 0.12.40
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 1f3f510: Warn when a Pikku function body performs a runtime dynamic `import(...)`.
|
|
6
|
+
|
|
7
|
+
The inspector now flags any `pikkuFunc`/`pikkuSessionlessFunc` (and friends) whose handler body contains a dynamic `import(...)` call — including nested callbacks — with the new `PKU498` diagnostic. Function bodies run on every invocation, so a dynamic import there adds per-call latency and defeats bundling/tree-shaking; the import belongs at the top of the module or in your services/`wireServices` setup instead.
|
|
8
|
+
|
|
9
|
+
Type-only positions like `import('x').Foo` are not flagged. The rule defaults to `warn` — a printed yellow warning that does not fail the build — and is configurable via `lint.functionDynamicImport` in `pikku.config.json` (`'off'` to silence, `'error'` to make it a hard build failure), matching the existing `servicesNotDestructured`/`wiresNotDestructured` lints.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [1f3f510]
|
|
12
|
+
- @pikku/core@0.12.59
|
|
13
|
+
|
|
14
|
+
## 0.12.39
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 4f92e6f: `pikku db` schema-codegen warnings are now coded diagnostics routed through the CLI logger instead of raw `console.warn`, so they participate in the existing `--fail-on-warn` gate.
|
|
19
|
+
|
|
20
|
+
Each warning now carries a PKU code and `warn` severity: `PKU481` (JSON/JSONB column with no concrete `tsType`, degrading to `unknown`), `PKU480` (column named like a date/bool but whose DB type contradicts it), and `PKU482` (a `format` annotation ignored on a non-string column). Running `pikku db migrate --fail-on-warn` (e.g. in CI) now turns these into a hard failure, forcing the `db/annotations.ts` entry — closing the loophole where an untyped jsonb column silently degrades type-safety. Default behaviour is unchanged: the warnings still print, and only fail the build when `--fail-on-warn` is set.
|
|
21
|
+
|
|
22
|
+
- daec082: Drop Node 22 support — the minimum supported runtime is now Node 24 (LTS).
|
|
23
|
+
|
|
24
|
+
Node 22 deadlocks `pikku dev` at `loadUserBootstrap` (tsx `register()` + `require(esm)` cycle handling on node 22.12+), and Node 20 is already below our floor. The `engines.node` requirement is raised to `>=24` across all packages, matching `.nvmrc` and the CI test matrix. Closes #751.
|
|
25
|
+
|
|
26
|
+
- ad26273: Remove 16 dormant `ErrorCode` enum entries that were defined but never emitted anywhere in the framework. These were placeholder registrations that were never wired to a diagnostic, or codes whose emission sites were removed in later refactors (e.g. `PKU901`, `PKU431`). A whole-repo audit found zero emission sites — no user could ever see them — so they only cluttered the registry and demanded docs pages for errors that cannot occur.
|
|
27
|
+
|
|
28
|
+
Removed: `PKU300`, `PKU426`, `PKU427`, `PKU431`, `PKU488`, `PKU529`, `PKU568`, `PKU685`, `PKU715`, `PKU736`, `PKU787`, `PKU835`, `PKU836`, `PKU901`, `PKU937`, `PKU975`.
|
|
29
|
+
|
|
30
|
+
A new guard test (`error-codes-emitted.test.ts`) fails if any `ErrorCode` value has no `ErrorCode.<NAME>` or raw `PKU###` reference in the source, so dead entries can't silently accumulate again.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [7b17b14]
|
|
33
|
+
- Updated dependencies [daec082]
|
|
34
|
+
- Updated dependencies [e0fd352]
|
|
35
|
+
- @pikku/core@0.12.58
|
|
36
|
+
|
|
1
37
|
## 0.12.38
|
|
2
38
|
|
|
3
39
|
### Patch Changes
|
|
@@ -15,6 +15,28 @@ const isValidVariableName = (name) => {
|
|
|
15
15
|
const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
16
16
|
return regex.test(name);
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* True when the handler body contains a runtime dynamic `import(...)` call
|
|
20
|
+
* anywhere (including nested callbacks). Type-only positions like
|
|
21
|
+
* `import('x').Foo` are `ImportTypeNode`s, not `CallExpression`s with an
|
|
22
|
+
* `ImportKeyword` callee, so they are intentionally not matched.
|
|
23
|
+
*/
|
|
24
|
+
const handlerHasDynamicImport = (handler) => {
|
|
25
|
+
let found = false;
|
|
26
|
+
const visit = (node) => {
|
|
27
|
+
if (found) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (ts.isCallExpression(node) &&
|
|
31
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
32
|
+
found = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
ts.forEachChild(node, visit);
|
|
36
|
+
};
|
|
37
|
+
visit(handler.body);
|
|
38
|
+
return found;
|
|
39
|
+
};
|
|
18
40
|
const nullifyTypes = (type) => {
|
|
19
41
|
if (type === 'void' ||
|
|
20
42
|
type === 'undefined' ||
|
|
@@ -779,6 +801,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
779
801
|
exportedName: exportedName || undefined,
|
|
780
802
|
...bodySpan,
|
|
781
803
|
};
|
|
804
|
+
if (handlerHasDynamicImport(handler)) {
|
|
805
|
+
state.functions.dynamicImportIds.add(pikkuFuncId);
|
|
806
|
+
}
|
|
782
807
|
// Populate node metadata if node config is present
|
|
783
808
|
if (nodeDisplayName && nodeCategory && nodeType) {
|
|
784
809
|
state.nodes.files.add(node.getSourceFile().fileName);
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -14,37 +14,25 @@ export declare enum ErrorCode {
|
|
|
14
14
|
MISSING_URI = "PKU220",
|
|
15
15
|
MISSING_FUNC = "PKU236",
|
|
16
16
|
INVALID_TAGS_TYPE = "PKU247",
|
|
17
|
-
INVALID_HANDLER = "PKU300",
|
|
18
17
|
MISSING_TITLE = "PKU370",
|
|
19
18
|
MISSING_QUEUE_NAME = "PKU384",
|
|
20
19
|
MISSING_CHANNEL_NAME = "PKU400",
|
|
21
20
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
|
|
22
21
|
SCENARIO_HAS_SERVICES = "PKU673",
|
|
23
22
|
EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
|
|
24
|
-
DYNAMIC_STEP_NAME = "PKU529",
|
|
25
23
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
26
24
|
INVALID_DSL_WORKFLOW = "PKU641",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
|
|
26
|
+
DB_JSON_COLUMN_UNTYPED = "PKU481",
|
|
27
|
+
DB_FORMAT_ON_NON_STRING = "PKU482",
|
|
30
28
|
SCHEMA_GENERATION_ERROR = "PKU456",
|
|
31
|
-
SCHEMA_LOAD_ERROR = "PKU488",
|
|
32
29
|
INLINE_SCHEMA = "PKU489",
|
|
33
30
|
FUNCTION_METADATA_NOT_FOUND = "PKU559",
|
|
34
|
-
HANDLER_NOT_RESOLVED = "PKU568",
|
|
35
31
|
DUPLICATE_AUTH_DEFINITION = "PKU581",
|
|
36
32
|
AUTH_NOT_EXPORTED = "PKU582",
|
|
37
33
|
ROUTE_PARAM_MISMATCH = "PKU571",
|
|
38
34
|
ROUTE_QUERY_MISMATCH = "PKU572",
|
|
39
35
|
AUTH_DISABLED_REQUIRES_SESSIONLESS = "PKU573",
|
|
40
|
-
MIDDLEWARE_HANDLER_INVALID = "PKU685",
|
|
41
|
-
MIDDLEWARE_TAG_INVALID = "PKU715",
|
|
42
|
-
MIDDLEWARE_EMPTY_ARRAY = "PKU736",
|
|
43
|
-
MIDDLEWARE_PATTERN_INVALID = "PKU787",
|
|
44
|
-
PERMISSION_HANDLER_INVALID = "PKU835",
|
|
45
|
-
PERMISSION_TAG_INVALID = "PKU836",
|
|
46
|
-
PERMISSION_EMPTY_ARRAY = "PKU937",
|
|
47
|
-
PERMISSION_PATTERN_INVALID = "PKU975",
|
|
48
36
|
DUPLICATE_FUNCTION_VERSION = "PKU850",
|
|
49
37
|
DUPLICATE_FUNCTION_NAME = "PKU851",
|
|
50
38
|
MANIFEST_MISSING = "PKU860",
|
|
@@ -58,8 +46,8 @@ export declare enum ErrorCode {
|
|
|
58
46
|
SCHEMA_AND_WIRING_COLOCATED = "PKU490",
|
|
59
47
|
SERVICES_NOT_DESTRUCTURED = "PKU410",
|
|
60
48
|
WIRES_NOT_DESTRUCTURED = "PKU411",
|
|
49
|
+
FUNCTION_DYNAMIC_IMPORT = "PKU498",
|
|
61
50
|
DUPLICATE_CORE_VERSION = "PKU717",
|
|
62
|
-
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
|
|
63
51
|
PII_IN_OUTPUT = "PKU910",
|
|
64
52
|
ADDON_WIRING_NOT_ALLOWED = "PKU920",
|
|
65
53
|
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
|
package/dist/error-codes.js
CHANGED
|
@@ -16,26 +16,23 @@ export var ErrorCode;
|
|
|
16
16
|
ErrorCode["MISSING_URI"] = "PKU220";
|
|
17
17
|
ErrorCode["MISSING_FUNC"] = "PKU236";
|
|
18
18
|
ErrorCode["INVALID_TAGS_TYPE"] = "PKU247";
|
|
19
|
-
ErrorCode["INVALID_HANDLER"] = "PKU300";
|
|
20
19
|
ErrorCode["MISSING_TITLE"] = "PKU370";
|
|
21
20
|
ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
|
|
22
21
|
ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
|
|
23
22
|
ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
|
|
24
23
|
ErrorCode["SCENARIO_HAS_SERVICES"] = "PKU673";
|
|
25
24
|
ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
|
|
26
|
-
ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
|
|
27
25
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
28
26
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
27
|
+
// Database schema codegen warnings
|
|
28
|
+
ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
|
|
29
|
+
ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
|
|
30
|
+
ErrorCode["DB_FORMAT_ON_NON_STRING"] = "PKU482";
|
|
29
31
|
// Configuration errors
|
|
30
|
-
ErrorCode["CONFIG_TYPE_NOT_FOUND"] = "PKU426";
|
|
31
|
-
ErrorCode["CONFIG_TYPE_UNDEFINED"] = "PKU427";
|
|
32
|
-
ErrorCode["SCHEMA_NO_ROOT"] = "PKU431";
|
|
33
32
|
ErrorCode["SCHEMA_GENERATION_ERROR"] = "PKU456";
|
|
34
|
-
ErrorCode["SCHEMA_LOAD_ERROR"] = "PKU488";
|
|
35
33
|
ErrorCode["INLINE_SCHEMA"] = "PKU489";
|
|
36
34
|
// Function errors
|
|
37
35
|
ErrorCode["FUNCTION_METADATA_NOT_FOUND"] = "PKU559";
|
|
38
|
-
ErrorCode["HANDLER_NOT_RESOLVED"] = "PKU568";
|
|
39
36
|
// Auth errors
|
|
40
37
|
ErrorCode["DUPLICATE_AUTH_DEFINITION"] = "PKU581";
|
|
41
38
|
ErrorCode["AUTH_NOT_EXPORTED"] = "PKU582";
|
|
@@ -43,15 +40,6 @@ export var ErrorCode;
|
|
|
43
40
|
ErrorCode["ROUTE_PARAM_MISMATCH"] = "PKU571";
|
|
44
41
|
ErrorCode["ROUTE_QUERY_MISMATCH"] = "PKU572";
|
|
45
42
|
ErrorCode["AUTH_DISABLED_REQUIRES_SESSIONLESS"] = "PKU573";
|
|
46
|
-
// Middleware/Permission errors
|
|
47
|
-
ErrorCode["MIDDLEWARE_HANDLER_INVALID"] = "PKU685";
|
|
48
|
-
ErrorCode["MIDDLEWARE_TAG_INVALID"] = "PKU715";
|
|
49
|
-
ErrorCode["MIDDLEWARE_EMPTY_ARRAY"] = "PKU736";
|
|
50
|
-
ErrorCode["MIDDLEWARE_PATTERN_INVALID"] = "PKU787";
|
|
51
|
-
ErrorCode["PERMISSION_HANDLER_INVALID"] = "PKU835";
|
|
52
|
-
ErrorCode["PERMISSION_TAG_INVALID"] = "PKU836";
|
|
53
|
-
ErrorCode["PERMISSION_EMPTY_ARRAY"] = "PKU937";
|
|
54
|
-
ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
|
|
55
43
|
// Versioning errors
|
|
56
44
|
ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
|
|
57
45
|
ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
|
|
@@ -70,10 +58,9 @@ export var ErrorCode;
|
|
|
70
58
|
// Optimization diagnostics
|
|
71
59
|
ErrorCode["SERVICES_NOT_DESTRUCTURED"] = "PKU410";
|
|
72
60
|
ErrorCode["WIRES_NOT_DESTRUCTURED"] = "PKU411";
|
|
61
|
+
ErrorCode["FUNCTION_DYNAMIC_IMPORT"] = "PKU498";
|
|
73
62
|
// Dependency integrity errors
|
|
74
63
|
ErrorCode["DUPLICATE_CORE_VERSION"] = "PKU717";
|
|
75
|
-
// Feature Flag
|
|
76
|
-
ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
|
|
77
64
|
// Data classification errors
|
|
78
65
|
ErrorCode["PII_IN_OUTPUT"] = "PKU910";
|
|
79
66
|
// Addon authoring errors
|
package/dist/inspector.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export interface InspectorFunctionState {
|
|
|
76
76
|
exportedName: string;
|
|
77
77
|
}>;
|
|
78
78
|
approvalDescriptions: Record<string, InspectorApprovalDescriptionDefinition>;
|
|
79
|
+
/**
|
|
80
|
+
* pikkuFuncIds whose handler body performs a runtime dynamic `import(...)`.
|
|
81
|
+
* Transient lint signal consumed by computeDiagnostics (PKU498) in the same
|
|
82
|
+
* inspection pass — deliberately not part of the serialized function meta.
|
|
83
|
+
*/
|
|
84
|
+
dynamicImportIds: Set<string>;
|
|
79
85
|
}
|
|
80
86
|
export interface InspectorChannelState {
|
|
81
87
|
meta: ChannelsMeta;
|
|
@@ -581,6 +581,14 @@ export function computeDiagnostics(state) {
|
|
|
581
581
|
position: 0,
|
|
582
582
|
});
|
|
583
583
|
}
|
|
584
|
+
if (state.functions.dynamicImportIds.has(id)) {
|
|
585
|
+
diagnostics.push({
|
|
586
|
+
code: ErrorCode.FUNCTION_DYNAMIC_IMPORT,
|
|
587
|
+
message: `Function '${id}' performs a runtime dynamic 'import(...)' in its body. Move the import to the top of the module (static import) or into your services/wireServices setup — function bodies run on every invocation, so a dynamic import there adds latency and defeats bundling/tree-shaking.`,
|
|
588
|
+
sourceFile: meta.pikkuFuncId,
|
|
589
|
+
position: 0,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
584
592
|
}
|
|
585
593
|
for (const [id, def] of Object.entries(state.middleware.definitions)) {
|
|
586
594
|
if (def.services && !def.services.optimized) {
|
|
@@ -194,6 +194,7 @@ export function deserializeInspectorState(data) {
|
|
|
194
194
|
meta: data.functions.meta,
|
|
195
195
|
files: new Map(data.functions.files),
|
|
196
196
|
approvalDescriptions: data.functions.approvalDescriptions || {},
|
|
197
|
+
dynamicImportIds: new Set(),
|
|
197
198
|
},
|
|
198
199
|
http: {
|
|
199
200
|
metaInputTypes: new Map(data.http.metaInputTypes),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.40",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"zod-to-ts": "^2.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@pikku/core": "^0.12.
|
|
47
|
+
"@pikku/core": "^0.12.59"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@pikku/core": "^0.12.
|
|
50
|
+
"@pikku/core": "^0.12.59",
|
|
51
51
|
"@types/node": "^24.11.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
|
-
"node": ">=
|
|
54
|
+
"node": ">=24"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/add/add-functions.ts
CHANGED
|
@@ -27,6 +27,33 @@ const isValidVariableName = (name: string) => {
|
|
|
27
27
|
return regex.test(name)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* True when the handler body contains a runtime dynamic `import(...)` call
|
|
32
|
+
* anywhere (including nested callbacks). Type-only positions like
|
|
33
|
+
* `import('x').Foo` are `ImportTypeNode`s, not `CallExpression`s with an
|
|
34
|
+
* `ImportKeyword` callee, so they are intentionally not matched.
|
|
35
|
+
*/
|
|
36
|
+
const handlerHasDynamicImport = (
|
|
37
|
+
handler: ts.ArrowFunction | ts.FunctionExpression
|
|
38
|
+
): boolean => {
|
|
39
|
+
let found = false
|
|
40
|
+
const visit = (node: ts.Node) => {
|
|
41
|
+
if (found) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
ts.isCallExpression(node) &&
|
|
46
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
47
|
+
) {
|
|
48
|
+
found = true
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
ts.forEachChild(node, visit)
|
|
52
|
+
}
|
|
53
|
+
visit(handler.body)
|
|
54
|
+
return found
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
const nullifyTypes = (type: string | null) => {
|
|
31
58
|
if (
|
|
32
59
|
type === 'void' ||
|
|
@@ -1083,6 +1110,10 @@ export const addFunctions: AddWiring = (
|
|
|
1083
1110
|
...bodySpan,
|
|
1084
1111
|
}
|
|
1085
1112
|
|
|
1113
|
+
if (handlerHasDynamicImport(handler)) {
|
|
1114
|
+
state.functions.dynamicImportIds.add(pikkuFuncId)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1086
1117
|
// Populate node metadata if node config is present
|
|
1087
1118
|
if (nodeDisplayName && nodeCategory && nodeType) {
|
|
1088
1119
|
state.nodes.files.add(node.getSourceFile().fileName)
|
|
@@ -0,0 +1,119 @@
|
|
|
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 logger: InspectorLogger = {
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {},
|
|
15
|
+
diagnostic: () => {},
|
|
16
|
+
critical: () => {},
|
|
17
|
+
hasCriticalErrors: () => false,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function inspectSource(source: string) {
|
|
21
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-dynimport-'))
|
|
22
|
+
const file = join(rootDir, 'my.function.ts')
|
|
23
|
+
await writeFile(file, source)
|
|
24
|
+
try {
|
|
25
|
+
return await inspect(logger, [file], { rootDir })
|
|
26
|
+
} finally {
|
|
27
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('dynamic import in function bodies', () => {
|
|
32
|
+
test('flags a function whose body does `await import(...)`', async () => {
|
|
33
|
+
const state = await inspectSource(
|
|
34
|
+
[
|
|
35
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
36
|
+
'',
|
|
37
|
+
'export const greedy = pikkuSessionlessFunc({',
|
|
38
|
+
' func: async ({ logger }) => {',
|
|
39
|
+
" const { readFile } = await import('node:fs/promises')",
|
|
40
|
+
' return { readFile: typeof readFile }',
|
|
41
|
+
' },',
|
|
42
|
+
'})',
|
|
43
|
+
].join('\n')
|
|
44
|
+
)
|
|
45
|
+
const meta = (state.functions.meta as any).greedy
|
|
46
|
+
assert.ok(meta, 'greedy should be inspected')
|
|
47
|
+
assert.ok(state.functions.dynamicImportIds.has(meta.pikkuFuncId))
|
|
48
|
+
const diagnostic = state.diagnostics.find(
|
|
49
|
+
(d) => d.code === ErrorCode.FUNCTION_DYNAMIC_IMPORT
|
|
50
|
+
)
|
|
51
|
+
assert.ok(
|
|
52
|
+
diagnostic,
|
|
53
|
+
'expected a FUNCTION_DYNAMIC_IMPORT diagnostic for the function'
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('flags a nested `import(...)` call inside a callback', async () => {
|
|
58
|
+
const state = await inspectSource(
|
|
59
|
+
[
|
|
60
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
61
|
+
'',
|
|
62
|
+
'export const nested = pikkuSessionlessFunc({',
|
|
63
|
+
' func: async ({ logger }) => {',
|
|
64
|
+
' await Promise.all([',
|
|
65
|
+
" import('node:os').then((m) => m.hostname()),",
|
|
66
|
+
' ])',
|
|
67
|
+
' return { ok: true }',
|
|
68
|
+
' },',
|
|
69
|
+
'})',
|
|
70
|
+
].join('\n')
|
|
71
|
+
)
|
|
72
|
+
const meta = (state.functions.meta as any).nested
|
|
73
|
+
assert.ok(state.functions.dynamicImportIds.has(meta.pikkuFuncId))
|
|
74
|
+
assert.ok(
|
|
75
|
+
state.diagnostics.some(
|
|
76
|
+
(d) => d.code === ErrorCode.FUNCTION_DYNAMIC_IMPORT
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('does NOT flag a function with only static imports', async () => {
|
|
82
|
+
const state = await inspectSource(
|
|
83
|
+
[
|
|
84
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
85
|
+
'',
|
|
86
|
+
'export const clean = pikkuSessionlessFunc({',
|
|
87
|
+
' func: async ({ logger }, data: { n: number }) => {',
|
|
88
|
+
' return { doubled: data.n * 2 }',
|
|
89
|
+
' },',
|
|
90
|
+
'})',
|
|
91
|
+
].join('\n')
|
|
92
|
+
)
|
|
93
|
+
const meta = (state.functions.meta as any).clean
|
|
94
|
+
assert.ok(meta, 'clean should be inspected')
|
|
95
|
+
assert.ok(!state.functions.dynamicImportIds.has(meta.pikkuFuncId))
|
|
96
|
+
assert.ok(
|
|
97
|
+
!state.diagnostics.some(
|
|
98
|
+
(d) => d.code === ErrorCode.FUNCTION_DYNAMIC_IMPORT
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('does NOT flag a type-only import position `import("x").T`', async () => {
|
|
104
|
+
const state = await inspectSource(
|
|
105
|
+
[
|
|
106
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
107
|
+
'',
|
|
108
|
+
'export const typeOnly = pikkuSessionlessFunc({',
|
|
109
|
+
" func: async ({ logger }, data: import('node:buffer').Blob) => {",
|
|
110
|
+
' return { size: data.size }',
|
|
111
|
+
' },',
|
|
112
|
+
'})',
|
|
113
|
+
].join('\n')
|
|
114
|
+
)
|
|
115
|
+
const meta = (state.functions.meta as any).typeOnly
|
|
116
|
+
assert.ok(meta, 'typeOnly should be inspected')
|
|
117
|
+
assert.ok(!state.functions.dynamicImportIds.has(meta.pikkuFuncId))
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Guards against dead `ErrorCode` registry entries.
|
|
9
|
+
*
|
|
10
|
+
* Every value in the `ErrorCode` enum must have at least one emission site in
|
|
11
|
+
* the framework source — a `ErrorCode.<NAME>` reference or the raw `PKU###`
|
|
12
|
+
* string in a non-test `.ts` file under `packages/*/src`. A code that is
|
|
13
|
+
* defined but never thrown is a dead entry: it can never appear in a user's
|
|
14
|
+
* terminal, yet it still demands a docs page and clutters the registry. This
|
|
15
|
+
* test fails and names any such entries so they get wired up or removed rather
|
|
16
|
+
* than silently accumulating.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const packagesDir = join(here, '..', '..')
|
|
21
|
+
const errorCodesFile = join(here, 'error-codes.ts')
|
|
22
|
+
|
|
23
|
+
const enumMembers = [
|
|
24
|
+
...readFileSync(errorCodesFile, 'utf8').matchAll(
|
|
25
|
+
/^\s*([A-Z0-9_]+)\s*=\s*'(PKU\d+)'/gm
|
|
26
|
+
),
|
|
27
|
+
].map((m) => ({ name: m[1], code: m[2] }))
|
|
28
|
+
|
|
29
|
+
const collectSource = () => {
|
|
30
|
+
const contents: string[] = []
|
|
31
|
+
for (const pkg of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
32
|
+
if (!pkg.isDirectory()) continue
|
|
33
|
+
const srcDir = join(packagesDir, pkg.name, 'src')
|
|
34
|
+
let entries
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(srcDir, { recursive: true, withFileTypes: true })
|
|
37
|
+
} catch {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isFile()) continue
|
|
42
|
+
const name = entry.name
|
|
43
|
+
if (
|
|
44
|
+
!name.endsWith('.ts') ||
|
|
45
|
+
name.endsWith('.d.ts') ||
|
|
46
|
+
name.endsWith('.test.ts') ||
|
|
47
|
+
name === 'error-codes.ts'
|
|
48
|
+
) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
contents.push(readFileSync(join(entry.parentPath, name), 'utf8'))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return contents.join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('every ErrorCode value has an emission site in the source', () => {
|
|
58
|
+
assert.ok(
|
|
59
|
+
enumMembers.length > 0,
|
|
60
|
+
'Failed to parse any ErrorCode members from error-codes.ts'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const source = collectSource()
|
|
64
|
+
|
|
65
|
+
const dead = enumMembers.filter(
|
|
66
|
+
({ name, code }) =>
|
|
67
|
+
!source.includes(`ErrorCode.${name}`) && !source.includes(code)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
dead.map(({ code, name }) => `${code} ${name}`),
|
|
72
|
+
[],
|
|
73
|
+
`Dead ErrorCode entries (defined but never emitted). Wire them to a diagnostic or delete them:\n` +
|
|
74
|
+
dead.map(({ code, name }) => ` - ${code} ${name}`).join('\n')
|
|
75
|
+
)
|
|
76
|
+
})
|
package/src/error-codes.ts
CHANGED
|
@@ -16,28 +16,26 @@ export enum ErrorCode {
|
|
|
16
16
|
MISSING_URI = 'PKU220',
|
|
17
17
|
MISSING_FUNC = 'PKU236',
|
|
18
18
|
INVALID_TAGS_TYPE = 'PKU247',
|
|
19
|
-
INVALID_HANDLER = 'PKU300',
|
|
20
19
|
MISSING_TITLE = 'PKU370',
|
|
21
20
|
MISSING_QUEUE_NAME = 'PKU384',
|
|
22
21
|
MISSING_CHANNEL_NAME = 'PKU400',
|
|
23
22
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
|
|
24
23
|
SCENARIO_HAS_SERVICES = 'PKU673',
|
|
25
24
|
EXPECT_EVENTUALLY_SCENARIO_ONLY = 'PKU675',
|
|
26
|
-
DYNAMIC_STEP_NAME = 'PKU529',
|
|
27
25
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
|
|
28
26
|
INVALID_DSL_WORKFLOW = 'PKU641',
|
|
29
27
|
|
|
28
|
+
// Database schema codegen warnings
|
|
29
|
+
DB_COLUMN_NAME_TYPE_CONTRADICTION = 'PKU480',
|
|
30
|
+
DB_JSON_COLUMN_UNTYPED = 'PKU481',
|
|
31
|
+
DB_FORMAT_ON_NON_STRING = 'PKU482',
|
|
32
|
+
|
|
30
33
|
// Configuration errors
|
|
31
|
-
CONFIG_TYPE_NOT_FOUND = 'PKU426',
|
|
32
|
-
CONFIG_TYPE_UNDEFINED = 'PKU427',
|
|
33
|
-
SCHEMA_NO_ROOT = 'PKU431',
|
|
34
34
|
SCHEMA_GENERATION_ERROR = 'PKU456',
|
|
35
|
-
SCHEMA_LOAD_ERROR = 'PKU488',
|
|
36
35
|
INLINE_SCHEMA = 'PKU489',
|
|
37
36
|
|
|
38
37
|
// Function errors
|
|
39
38
|
FUNCTION_METADATA_NOT_FOUND = 'PKU559',
|
|
40
|
-
HANDLER_NOT_RESOLVED = 'PKU568',
|
|
41
39
|
|
|
42
40
|
// Auth errors
|
|
43
41
|
DUPLICATE_AUTH_DEFINITION = 'PKU581',
|
|
@@ -48,16 +46,6 @@ export enum ErrorCode {
|
|
|
48
46
|
ROUTE_QUERY_MISMATCH = 'PKU572',
|
|
49
47
|
AUTH_DISABLED_REQUIRES_SESSIONLESS = 'PKU573',
|
|
50
48
|
|
|
51
|
-
// Middleware/Permission errors
|
|
52
|
-
MIDDLEWARE_HANDLER_INVALID = 'PKU685',
|
|
53
|
-
MIDDLEWARE_TAG_INVALID = 'PKU715',
|
|
54
|
-
MIDDLEWARE_EMPTY_ARRAY = 'PKU736',
|
|
55
|
-
MIDDLEWARE_PATTERN_INVALID = 'PKU787',
|
|
56
|
-
PERMISSION_HANDLER_INVALID = 'PKU835',
|
|
57
|
-
PERMISSION_TAG_INVALID = 'PKU836',
|
|
58
|
-
PERMISSION_EMPTY_ARRAY = 'PKU937',
|
|
59
|
-
PERMISSION_PATTERN_INVALID = 'PKU975',
|
|
60
|
-
|
|
61
49
|
// Versioning errors
|
|
62
50
|
DUPLICATE_FUNCTION_VERSION = 'PKU850',
|
|
63
51
|
DUPLICATE_FUNCTION_NAME = 'PKU851',
|
|
@@ -80,13 +68,11 @@ export enum ErrorCode {
|
|
|
80
68
|
// Optimization diagnostics
|
|
81
69
|
SERVICES_NOT_DESTRUCTURED = 'PKU410',
|
|
82
70
|
WIRES_NOT_DESTRUCTURED = 'PKU411',
|
|
71
|
+
FUNCTION_DYNAMIC_IMPORT = 'PKU498',
|
|
83
72
|
|
|
84
73
|
// Dependency integrity errors
|
|
85
74
|
DUPLICATE_CORE_VERSION = 'PKU717',
|
|
86
75
|
|
|
87
|
-
// Feature Flag
|
|
88
|
-
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = 'PKU901',
|
|
89
|
-
|
|
90
76
|
// Data classification errors
|
|
91
77
|
PII_IN_OUTPUT = 'PKU910',
|
|
92
78
|
|
package/src/inspector.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -101,6 +101,12 @@ export interface InspectorFunctionState {
|
|
|
101
101
|
meta: FunctionsMeta
|
|
102
102
|
files: Map<string, { path: string; exportedName: string }>
|
|
103
103
|
approvalDescriptions: Record<string, InspectorApprovalDescriptionDefinition>
|
|
104
|
+
/**
|
|
105
|
+
* pikkuFuncIds whose handler body performs a runtime dynamic `import(...)`.
|
|
106
|
+
* Transient lint signal consumed by computeDiagnostics (PKU498) in the same
|
|
107
|
+
* inspection pass — deliberately not part of the serialized function meta.
|
|
108
|
+
*/
|
|
109
|
+
dynamicImportIds: Set<string>
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export interface InspectorChannelState {
|
|
@@ -5,10 +5,11 @@ import type { InspectorState } from '../types.js'
|
|
|
5
5
|
import { ErrorCode } from '../error-codes.js'
|
|
6
6
|
|
|
7
7
|
function stateWithFunctions(
|
|
8
|
-
meta: InspectorState['functions']['meta']
|
|
8
|
+
meta: InspectorState['functions']['meta'],
|
|
9
|
+
dynamicImportIds: string[] = []
|
|
9
10
|
): InspectorState {
|
|
10
11
|
return {
|
|
11
|
-
functions: { meta },
|
|
12
|
+
functions: { meta, dynamicImportIds: new Set(dynamicImportIds) },
|
|
12
13
|
middleware: { definitions: {} },
|
|
13
14
|
permissions: { definitions: {} },
|
|
14
15
|
} as unknown as InspectorState
|
|
@@ -27,10 +28,7 @@ describe('computeDiagnostics', () => {
|
|
|
27
28
|
})
|
|
28
29
|
computeDiagnostics(state)
|
|
29
30
|
assert.equal(state.diagnostics.length, 1)
|
|
30
|
-
assert.equal(
|
|
31
|
-
state.diagnostics[0].code,
|
|
32
|
-
ErrorCode.SERVICES_NOT_DESTRUCTURED
|
|
33
|
-
)
|
|
31
|
+
assert.equal(state.diagnostics[0].code, ErrorCode.SERVICES_NOT_DESTRUCTURED)
|
|
34
32
|
})
|
|
35
33
|
|
|
36
34
|
test('does NOT flag a generated .gen.ts function (user cannot edit it)', () => {
|
|
@@ -66,4 +64,43 @@ describe('computeDiagnostics', () => {
|
|
|
66
64
|
computeDiagnostics(state)
|
|
67
65
|
assert.equal(state.diagnostics.length, 0)
|
|
68
66
|
})
|
|
67
|
+
|
|
68
|
+
test('flags a user-authored function that does a dynamic import in its body', () => {
|
|
69
|
+
const state = stateWithFunctions(
|
|
70
|
+
{
|
|
71
|
+
greedy: {
|
|
72
|
+
pikkuFuncId: 'greedy',
|
|
73
|
+
inputSchemaName: null,
|
|
74
|
+
outputSchemaName: null,
|
|
75
|
+
sourceFile: '/project/src/greedy.ts',
|
|
76
|
+
services: { optimized: true, services: ['logger'] },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
['greedy']
|
|
80
|
+
)
|
|
81
|
+
computeDiagnostics(state)
|
|
82
|
+
assert.equal(
|
|
83
|
+
state.diagnostics.filter(
|
|
84
|
+
(d) => d.code === ErrorCode.FUNCTION_DYNAMIC_IMPORT
|
|
85
|
+
).length,
|
|
86
|
+
1
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('does NOT flag a dynamic import in a generated .gen.ts function', () => {
|
|
91
|
+
const state = stateWithFunctions(
|
|
92
|
+
{
|
|
93
|
+
authHandler: {
|
|
94
|
+
pikkuFuncId: 'authHandler',
|
|
95
|
+
inputSchemaName: null,
|
|
96
|
+
outputSchemaName: null,
|
|
97
|
+
sourceFile: '/project/.pikku/auth.gen.ts',
|
|
98
|
+
services: { optimized: true, services: [] },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
['authHandler']
|
|
102
|
+
)
|
|
103
|
+
computeDiagnostics(state)
|
|
104
|
+
assert.equal(state.diagnostics.length, 0)
|
|
105
|
+
})
|
|
69
106
|
})
|