@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 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);
@@ -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
- CONFIG_TYPE_NOT_FOUND = "PKU426",
28
- CONFIG_TYPE_UNDEFINED = "PKU427",
29
- SCHEMA_NO_ROOT = "PKU431",
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",
@@ -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
@@ -43,6 +43,7 @@ export function getInitialInspectorState(rootDir) {
43
43
  meta: {},
44
44
  files: new Map(),
45
45
  approvalDescriptions: {},
46
+ dynamicImportIds: new Set(),
46
47
  },
47
48
  http: {
48
49
  metaInputTypes: new Map(),
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.38",
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.54"
47
+ "@pikku/core": "^0.12.59"
48
48
  },
49
49
  "devDependencies": {
50
- "@pikku/core": "^0.12.54",
50
+ "@pikku/core": "^0.12.59",
51
51
  "@types/node": "^24.11.0"
52
52
  },
53
53
  "engines": {
54
- "node": ">=22"
54
+ "node": ">=24"
55
55
  }
56
56
  }
@@ -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
+ })
@@ -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
@@ -74,6 +74,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
74
74
  meta: {},
75
75
  files: new Map(),
76
76
  approvalDescriptions: {},
77
+ dynamicImportIds: new Set(),
77
78
  },
78
79
  http: {
79
80
  metaInputTypes: new Map(),
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
  })