@pikku/inspector 0.12.109 → 0.12.111

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.
Files changed (46) hide show
  1. package/dist/add/add-channel.js +8 -0
  2. package/dist/add/add-file-with-factory.d.ts +7 -1
  3. package/dist/add/add-file-with-factory.js +63 -2
  4. package/dist/add/add-functions.js +32 -1
  5. package/dist/add/add-pikku-studio.d.ts +15 -0
  6. package/dist/add/add-pikku-studio.js +307 -0
  7. package/dist/add/add-rpc-invocations.d.ts +1 -1
  8. package/dist/add/add-rpc-invocations.js +14 -2
  9. package/dist/add/add-wire-remote-addon.js +25 -4
  10. package/dist/add/check-disabled-references.d.ts +27 -0
  11. package/dist/add/check-disabled-references.js +226 -0
  12. package/dist/error-codes.d.ts +14 -0
  13. package/dist/error-codes.js +14 -0
  14. package/dist/finalize.js +3 -1
  15. package/dist/index.d.ts +6 -1
  16. package/dist/index.js +3 -0
  17. package/dist/initial-state.js +1 -0
  18. package/dist/inspector.js +4 -0
  19. package/dist/lite.d.ts +7 -1
  20. package/dist/lite.js +5 -1
  21. package/dist/types.d.ts +65 -13
  22. package/dist/utils/addon-dependencies.d.ts +43 -0
  23. package/dist/utils/addon-dependencies.js +91 -0
  24. package/dist/utils/contract-hashes.js +2 -2
  25. package/dist/utils/find-unused-functions.d.ts +20 -0
  26. package/dist/utils/find-unused-functions.js +132 -0
  27. package/dist/utils/find-wire-remote-addon-specs.d.ts +17 -0
  28. package/dist/utils/find-wire-remote-addon-specs.js +53 -0
  29. package/dist/utils/get-property-value.js +4 -1
  30. package/dist/utils/load-addon-functions-meta.js +69 -46
  31. package/dist/utils/post-process.js +58 -8
  32. package/dist/utils/remote-addon-sdk.d.ts +57 -0
  33. package/dist/utils/remote-addon-sdk.js +111 -0
  34. package/dist/utils/resolve-addon-package.d.ts +1 -0
  35. package/dist/utils/resolve-addon-package.js +1 -1
  36. package/dist/utils/resolve-versions.js +18 -0
  37. package/dist/utils/serialize-inspector-state.d.ts +5 -2
  38. package/dist/utils/serialize-inspector-state.js +12 -4
  39. package/dist/utils/validate-addon-zod-contract.d.ts +23 -0
  40. package/dist/utils/validate-addon-zod-contract.js +70 -0
  41. package/dist/utils/validate-exposed-functions-gated.d.ts +3 -1
  42. package/dist/utils/validate-exposed-functions-gated.js +15 -8
  43. package/dist/visit.js +6 -3
  44. package/package.json +3 -3
  45. package/dist/add/add-define-screens.d.ts +0 -8
  46. package/dist/add/add-define-screens.js +0 -140
@@ -0,0 +1,57 @@
1
+ /** Committed beside pikku.config.json: the SDK version and integrity each versioned `wireRemoteAddon` resolved to. */
2
+ export declare const ADDONS_LOCK_FILE = "pikku-addons.lock";
3
+ /** Where `pikku all` extracts SDKs, relative to the project root. Gitignored with the rest of `.pikku`. */
4
+ export declare const REMOTE_SDK_DIR: string;
5
+ /** Written into an extracted SDK: the integrity of the tarball it came from. */
6
+ export declare const SDK_INTEGRITY_MARKER = ".integrity";
7
+ export type AddonsLockEntry = {
8
+ /** The range the spec asked for when this entry was written. */
9
+ range: string;
10
+ version: string;
11
+ /** `sha512-<base64>` of the SDK tarball. */
12
+ integrity: string;
13
+ };
14
+ export type AddonsLock = {
15
+ lockfileVersion: 1;
16
+ sdks: Record<string, AddonsLockEntry>;
17
+ };
18
+ /** `manifest.json` at the root of an SDK. */
19
+ export type RemoteAddonSdkManifest = {
20
+ name: string;
21
+ version: string;
22
+ /** The @pikku/core range the SDK's types were generated against. */
23
+ pikkuCore?: string;
24
+ contractHash?: string;
25
+ };
26
+ export type RemoteAddonPackageSpec = {
27
+ name: string;
28
+ range?: string;
29
+ };
30
+ /**
31
+ * `@scope/name@^1.4.0` → name and range, split at the last `@` after the first
32
+ * character. Mirrors `parseRemoteAddonPackage` in @pikku/core, which the
33
+ * runtime uses on the same literal.
34
+ */
35
+ export declare const parseRemoteAddonPackage: (spec: string) => RemoteAddonPackageSpec;
36
+ export declare const emptyAddonsLock: () => AddonsLock;
37
+ /** The project's lock, an empty one when there is none. Throws when the file is not a lock. */
38
+ export declare const readAddonsLock: (rootDir: string) => AddonsLock;
39
+ /** Sorted by package name, each entry's keys in a fixed order, so the same lock always writes the same bytes. */
40
+ export declare const serializeAddonsLock: (lock: AddonsLock) => string;
41
+ export declare const remoteSdkDir: (rootDir: string, name: string, version: string) => string;
42
+ export declare const readSdkManifest: (dir: string) => RemoteAddonSdkManifest | null;
43
+ export type LockedSdk = {
44
+ ok: true;
45
+ entry: AddonsLockEntry;
46
+ dir: string;
47
+ manifest: RemoteAddonSdkManifest;
48
+ } | {
49
+ ok: false;
50
+ reason: string;
51
+ };
52
+ /**
53
+ * Whether the SDK for `name@range` is on disk exactly as the lock pins it: the
54
+ * lock was written for this range, the extracted directory carries the lock's
55
+ * integrity, and its manifest names this package and version.
56
+ */
57
+ export declare const checkLockedSdk: (rootDir: string, name: string, range: string, lock?: AddonsLock) => LockedSdk;
@@ -0,0 +1,111 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ /** Committed beside pikku.config.json: the SDK version and integrity each versioned `wireRemoteAddon` resolved to. */
4
+ export const ADDONS_LOCK_FILE = 'pikku-addons.lock';
5
+ /** Where `pikku all` extracts SDKs, relative to the project root. Gitignored with the rest of `.pikku`. */
6
+ export const REMOTE_SDK_DIR = join('.pikku', 'remote');
7
+ /** Written into an extracted SDK: the integrity of the tarball it came from. */
8
+ export const SDK_INTEGRITY_MARKER = '.integrity';
9
+ /**
10
+ * `@scope/name@^1.4.0` → name and range, split at the last `@` after the first
11
+ * character. Mirrors `parseRemoteAddonPackage` in @pikku/core, which the
12
+ * runtime uses on the same literal.
13
+ */
14
+ export const parseRemoteAddonPackage = (spec) => {
15
+ const trimmed = spec.trim();
16
+ const at = trimmed.lastIndexOf('@');
17
+ if (at <= 0) {
18
+ if (!trimmed || trimmed === '@') {
19
+ throw new Error(`'${spec}' is not a package name`);
20
+ }
21
+ return { name: trimmed };
22
+ }
23
+ const name = trimmed.slice(0, at);
24
+ const range = trimmed.slice(at + 1).trim();
25
+ if (!range) {
26
+ throw new Error(`'${spec}' ends in '@' with no version range after it. Write '${name}@^1.0.0', or drop the '@'.`);
27
+ }
28
+ return { name, range };
29
+ };
30
+ export const emptyAddonsLock = () => ({
31
+ lockfileVersion: 1,
32
+ sdks: {},
33
+ });
34
+ /** The project's lock, an empty one when there is none. Throws when the file is not a lock. */
35
+ export const readAddonsLock = (rootDir) => {
36
+ const path = join(rootDir, ADDONS_LOCK_FILE);
37
+ if (!existsSync(path))
38
+ return emptyAddonsLock();
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
42
+ }
43
+ catch (e) {
44
+ throw new Error(`${path} is not valid JSON: ${e?.message ?? e}`);
45
+ }
46
+ if (!parsed ||
47
+ typeof parsed !== 'object' ||
48
+ typeof parsed.sdks !== 'object') {
49
+ throw new Error(`${path} has no "sdks" object`);
50
+ }
51
+ return { lockfileVersion: 1, sdks: parsed.sdks ?? {} };
52
+ };
53
+ /** Sorted by package name, each entry's keys in a fixed order, so the same lock always writes the same bytes. */
54
+ export const serializeAddonsLock = (lock) => {
55
+ const sdks = {};
56
+ for (const name of Object.keys(lock.sdks).sort()) {
57
+ const { range, version, integrity } = lock.sdks[name];
58
+ sdks[name] = { range, version, integrity };
59
+ }
60
+ return `${JSON.stringify({ lockfileVersion: 1, sdks }, null, 2)}\n`;
61
+ };
62
+ export const remoteSdkDir = (rootDir, name, version) => join(rootDir, REMOTE_SDK_DIR, `${name}@${version}`);
63
+ export const readSdkManifest = (dir) => {
64
+ try {
65
+ return JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf-8'));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ /**
72
+ * Whether the SDK for `name@range` is on disk exactly as the lock pins it: the
73
+ * lock was written for this range, the extracted directory carries the lock's
74
+ * integrity, and its manifest names this package and version.
75
+ */
76
+ export const checkLockedSdk = (rootDir, name, range, lock = readAddonsLock(rootDir)) => {
77
+ const entry = lock.sdks[name];
78
+ if (!entry) {
79
+ return { ok: false, reason: `${ADDONS_LOCK_FILE} has no entry for it` };
80
+ }
81
+ if (entry.range !== range) {
82
+ return {
83
+ ok: false,
84
+ reason: `${ADDONS_LOCK_FILE} was resolved for '${entry.range}', not '${range}'`,
85
+ };
86
+ }
87
+ const dir = remoteSdkDir(rootDir, name, entry.version);
88
+ let integrity = null;
89
+ try {
90
+ integrity = readFileSync(join(dir, SDK_INTEGRITY_MARKER), 'utf-8').trim();
91
+ }
92
+ catch {
93
+ return { ok: false, reason: `${dir} is missing` };
94
+ }
95
+ if (integrity !== entry.integrity) {
96
+ return {
97
+ ok: false,
98
+ reason: `${dir} was extracted from a different tarball than ${ADDONS_LOCK_FILE} pins`,
99
+ };
100
+ }
101
+ const manifest = readSdkManifest(dir);
102
+ if (!manifest ||
103
+ manifest.name !== name ||
104
+ manifest.version !== entry.version) {
105
+ return {
106
+ ok: false,
107
+ reason: `${dir}/manifest.json does not name ${name}@${entry.version}`,
108
+ };
109
+ }
110
+ return { ok: true, entry, dir, manifest };
111
+ };
@@ -1,5 +1,6 @@
1
1
  import type { CredentialOverrideMeta } from '../types.js';
2
2
  import * as ts from 'typescript';
3
+ export declare const findPackageNameForFile: (filePath: string) => string | null;
3
4
  /**
4
5
  * Resolve the addon package name from an imported identifier.
5
6
  * Checks if the identifier's import module specifier matches any
@@ -2,7 +2,7 @@ import * as ts from 'typescript';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { dirname, join, parse } from 'path';
4
4
  const packageNameCache = new Map();
5
- const findPackageNameForFile = (filePath) => {
5
+ export const findPackageNameForFile = (filePath) => {
6
6
  if (packageNameCache.has(filePath)) {
7
7
  return packageNameCache.get(filePath);
8
8
  }
@@ -90,6 +90,24 @@ export function resolveLatestVersions(state, logger) {
90
90
  }
91
91
  }
92
92
  }
93
+ // A remote caller names a version the same way: `fn` is the latest remote
94
+ // version, and every remote `fn@vN` stays callable by its own id.
95
+ const remote = [
96
+ ...group.explicit,
97
+ ...(group.unversioned
98
+ ? [{ id: state.rpc.internalMeta[baseName], version: maxVersion + 1 }]
99
+ : []),
100
+ ].filter(({ id }) => functionsMeta[id]?.remote === true);
101
+ if (remote.length > 0) {
102
+ state.rpc.remoteMeta[baseName] = remote.reduce((a, b) => a.version > b.version ? a : b).id;
103
+ for (const { id } of remote) {
104
+ state.rpc.remoteMeta[id] = id;
105
+ const fileEntry = state.rpc.internalFiles.get(id);
106
+ if (fileEntry) {
107
+ state.rpc.remoteFiles.set(id, fileEntry);
108
+ }
109
+ }
110
+ }
93
111
  for (const entry of group.explicit) {
94
112
  state.rpc.invokedFunctions.add(entry.id);
95
113
  }
@@ -118,6 +118,7 @@ export interface SerializableInspectorState {
118
118
  }]>;
119
119
  approvalDescriptions: InspectorState['functions']['approvalDescriptions'];
120
120
  dynamicImportIds?: string[];
121
+ disabled?: InspectorState['functions']['disabled'];
121
122
  };
122
123
  http: {
123
124
  metaInputTypes: Array<[
@@ -194,6 +195,7 @@ export interface SerializableInspectorState {
194
195
  string,
195
196
  {
196
197
  package: string;
198
+ range?: string;
197
199
  file?: string;
198
200
  rpcEndpoint?: string;
199
201
  remote?: boolean;
@@ -326,8 +328,9 @@ export interface SerializableInspectorState {
326
328
  openAPISpec: Record<string, any> | null;
327
329
  diagnostics: InspectorDiagnostic[];
328
330
  addonFunctions: InspectorState['addonFunctions'];
329
- screensManifest?: InspectorState['screensManifest'];
330
- addonScreens?: InspectorState['addonScreens'];
331
+ remoteAddonFunctions?: InspectorState['remoteAddonFunctions'];
332
+ studio?: InspectorState['studio'];
333
+ addonStudio?: InspectorState['addonStudio'];
331
334
  addonContributors?: InspectorState['addonContributors'];
332
335
  exportedContracts: InspectorState['exportedContracts'];
333
336
  }
@@ -41,6 +41,9 @@ export function serializeInspectorState(state) {
41
41
  files: Array.from(state.functions.files.entries()),
42
42
  approvalDescriptions: state.functions.approvalDescriptions,
43
43
  dynamicImportIds: Array.from(state.functions.dynamicImportIds ?? []),
44
+ ...(Object.keys(state.functions.disabled ?? {}).length > 0
45
+ ? { disabled: state.functions.disabled }
46
+ : {}),
44
47
  },
45
48
  http: {
46
49
  metaInputTypes: Array.from(state.http.metaInputTypes.entries()),
@@ -190,8 +193,9 @@ export function serializeInspectorState(state) {
190
193
  openAPISpec: state.openAPISpec,
191
194
  diagnostics: state.diagnostics,
192
195
  addonFunctions: state.addonFunctions,
193
- screensManifest: state.screensManifest,
194
- addonScreens: state.addonScreens,
196
+ remoteAddonFunctions: state.remoteAddonFunctions,
197
+ studio: state.studio,
198
+ addonStudio: state.addonStudio,
195
199
  addonContributors: state.addonContributors,
196
200
  exportedContracts: state.exportedContracts,
197
201
  };
@@ -239,6 +243,7 @@ export function deserializeInspectorState(data) {
239
243
  files: new Map(data.functions.files),
240
244
  approvalDescriptions: data.functions.approvalDescriptions || {},
241
245
  dynamicImportIds: new Set(data.functions.dynamicImportIds ?? []),
246
+ disabled: data.functions.disabled ?? {},
242
247
  },
243
248
  http: {
244
249
  metaInputTypes: new Map(data.http.metaInputTypes),
@@ -396,8 +401,11 @@ export function deserializeInspectorState(data) {
396
401
  openAPISpec: data.openAPISpec || null,
397
402
  diagnostics: data.diagnostics || [],
398
403
  addonFunctions: data.addonFunctions || {},
399
- screensManifest: data.screensManifest,
400
- addonScreens: data.addonScreens,
404
+ ...(data.remoteAddonFunctions
405
+ ? { remoteAddonFunctions: data.remoteAddonFunctions }
406
+ : {}),
407
+ studio: data.studio,
408
+ addonStudio: data.addonStudio,
401
409
  addonContributors: data.addonContributors,
402
410
  exportedContracts: data.exportedContracts || {
403
411
  http: {},
@@ -0,0 +1,23 @@
1
+ import type { TypesMap } from '../types-map.js';
2
+ /**
3
+ * The named, source-declared types a function's input or output resolves to:
4
+ * every name the typed maps would have to import rather than declare inline.
5
+ * A custom alias (a union, an array of rows) is followed through to the names
6
+ * it references.
7
+ */
8
+ export declare const namedSourceTypes: (typesMap: TypesMap, names: string[]) => string[];
9
+ export interface AddonContractTypeSource {
10
+ name: string;
11
+ usesGenerics: boolean;
12
+ hasInputSchema: boolean;
13
+ hasOutputSchema: boolean;
14
+ inputNames: string[];
15
+ outputNames: string[];
16
+ }
17
+ /**
18
+ * Why a `remote: true` addon function cannot ship in an SDK, or null when it
19
+ * can. Its input and output have to be zod schemas: a TS generic or a named
20
+ * source type makes codegen emit `import type { X } from '../../../src/...'`
21
+ * into the remote map, a path that does not exist on the consumer's machine.
22
+ */
23
+ export declare const addonZodContractViolation: (typesMap: TypesMap, fn: AddonContractTypeSource) => string | null;
@@ -0,0 +1,70 @@
1
+ import { findPackageNameForFile } from './resolve-addon-package.js';
2
+ /**
3
+ * A type declared in `@pikku/core` is part of the SDK's allowed import set, so
4
+ * naming one (an MCP response, say) does not drag the addon's source along.
5
+ * Core is recognised by its install path or by the name in the nearest
6
+ * package.json, never by a directory name an addon could also use.
7
+ */
8
+ const isPikkuCorePath = (path) => /[\\/]node_modules[\\/]@pikku[\\/]core[\\/]/.test(path) ||
9
+ findPackageNameForFile(path) === '@pikku/core';
10
+ /**
11
+ * The named, source-declared types a function's input or output resolves to:
12
+ * every name the typed maps would have to import rather than declare inline.
13
+ * A custom alias (a union, an array of rows) is followed through to the names
14
+ * it references.
15
+ */
16
+ export const namedSourceTypes = (typesMap, names) => {
17
+ const found = new Set();
18
+ const visit = (name, seen) => {
19
+ if (seen.has(name))
20
+ return;
21
+ seen.add(name);
22
+ const custom = typesMap.customTypes.get(name);
23
+ if (custom) {
24
+ for (const ref of custom.references)
25
+ visit(ref, seen);
26
+ return;
27
+ }
28
+ let path = null;
29
+ let originalName = name;
30
+ try {
31
+ const meta = typesMap.getTypeMeta(name);
32
+ path = meta.path;
33
+ originalName = meta.originalName;
34
+ }
35
+ catch {
36
+ return;
37
+ }
38
+ if (path && !isPikkuCorePath(path))
39
+ found.add(originalName);
40
+ };
41
+ for (const name of names)
42
+ visit(name, new Set());
43
+ return [...found].sort();
44
+ };
45
+ /**
46
+ * Why a `remote: true` addon function cannot ship in an SDK, or null when it
47
+ * can. Its input and output have to be zod schemas: a TS generic or a named
48
+ * source type makes codegen emit `import type { X } from '../../../src/...'`
49
+ * into the remote map, a path that does not exist on the consumer's machine.
50
+ */
51
+ export const addonZodContractViolation = (typesMap, fn) => {
52
+ const problems = [];
53
+ if (fn.usesGenerics) {
54
+ problems.push('declares its input/output with TypeScript generics');
55
+ }
56
+ const named = [
57
+ ...(fn.hasInputSchema ? [] : namedSourceTypes(typesMap, fn.inputNames)),
58
+ ...(fn.hasOutputSchema ? [] : namedSourceTypes(typesMap, fn.outputNames)),
59
+ ];
60
+ if (named.length > 0) {
61
+ problems.push(`types its input/output with named source types (${[...new Set(named)].join(', ')})`);
62
+ }
63
+ if (problems.length === 0)
64
+ return null;
65
+ return (`Addon function '${fn.name}' is remote: true but ${problems.join(' and ')}. ` +
66
+ `A remote addon's contract ships as a types-only SDK, so its input and output must be zod schemas: ` +
67
+ `export each schema as a zod variable from a file that does not import #pikku (a sibling <fn>.schemas.ts), ` +
68
+ `reference it as \`input:\` / \`output:\` on the function, and drop the generics. ` +
69
+ `TypeScript types would make the SDK import from the addon's src/, which the consumer does not have.`);
70
+ };
@@ -11,4 +11,6 @@ import type { InspectorLogger, InspectorState } from '../types.js';
11
11
  *
12
12
  * knowledge: decisions/security/console-addon-privileged-functions-gate-themselves.md
13
13
  */
14
- export declare function validateExposedFunctionsGated(logger: InspectorLogger, state: InspectorState): void;
14
+ export declare function validateExposedFunctionsGated(logger: InspectorLogger, state: InspectorState, { isAddon }?: {
15
+ isAddon?: boolean;
16
+ }): void;
@@ -11,9 +11,13 @@ import { ErrorCode } from '../error-codes.js';
11
11
  *
12
12
  * knowledge: decisions/security/console-addon-privileged-functions-gate-themselves.md
13
13
  */
14
- export function validateExposedFunctionsGated(logger, state) {
14
+ export function validateExposedFunctionsGated(logger, state, { isAddon = false } = {}) {
15
15
  const ungated = [];
16
- for (const [funcName, meta] of Object.entries(state.functions.meta)) {
16
+ // An addon's own `expose: true` only makes a function eligible: nothing is
17
+ // reachable until a host's `wireAddon` opts in, and that host is warned below.
18
+ for (const [funcName, meta] of isAddon
19
+ ? []
20
+ : Object.entries(state.functions.meta)) {
17
21
  if (!meta.expose)
18
22
  continue;
19
23
  if (gatesItself(meta))
@@ -22,23 +26,26 @@ export function validateExposedFunctionsGated(logger, state) {
22
26
  continue;
23
27
  ungated.push(funcName);
24
28
  }
25
- // A `wireAddon` expose list can open an addon function its author never
26
- // exposed, so the app that listed it is the one to tell. The namespaced call
27
- // is governed by that instance's config alone.
29
+ // An addon function is exposed only when the app's `wireAddon` opts in, so
30
+ // the app that opted in is the one to tell. The namespaced call is governed
31
+ // by that instance's config alone.
28
32
  //
29
33
  // knowledge: decisions/security/wire-addon-expose-selects-the-rpc-surface.md
30
34
  for (const [namespace, declaration] of state.rpc?.wireAddonDeclarations ??
31
35
  []) {
32
- if (!Array.isArray(declaration.expose))
36
+ if (!declaration.expose)
33
37
  continue;
34
38
  if (declaration.auth === true)
35
39
  continue;
36
40
  if (declaration.scopes && declaration.scopes.length > 0)
37
41
  continue;
38
42
  const addonMeta = state.addonFunctions?.[namespace] ?? {};
39
- for (const funcName of declaration.expose) {
43
+ const selected = Array.isArray(declaration.expose)
44
+ ? declaration.expose
45
+ : Object.keys(addonMeta);
46
+ for (const funcName of selected) {
40
47
  const meta = addonMeta[funcName];
41
- if (!meta || gatesItself(meta))
48
+ if (!meta?.expose || gatesItself(meta))
42
49
  continue;
43
50
  ungated.push(`${namespace}:${funcName}`);
44
51
  }
package/dist/visit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { addFileWithFactory } from './add/add-file-with-factory.js';
2
+ import { addFileWithFactory, checkHiddenFactory, } from './add/add-file-with-factory.js';
3
3
  import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
4
4
  import { addHTTPRoute } from './add/add-http-route.js';
5
5
  import { addHTTPRoutes } from './add/add-http-routes.js';
@@ -15,7 +15,7 @@ import { addChannel } from './add/add-channel.js';
15
15
  import { addGateway } from './add/add-gateway.js';
16
16
  import { addRPCInvocations } from './add/add-rpc-invocations.js';
17
17
  import { addWireAddon } from './add/add-wire-addon.js';
18
- import { addDefineScreens } from './add/add-define-screens.js';
18
+ import { addPikkuStudio } from './add/add-pikku-studio.js';
19
19
  import { addWireRemoteAddon } from './add/add-wire-remote-addon.js';
20
20
  import { addMiddleware } from './add/add-middleware.js';
21
21
  import { addPermission } from './add/add-permission.js';
@@ -36,6 +36,7 @@ import { addOutgoingWebhook } from './add/add-outgoing-webhook.js';
36
36
  import { addAgentScorer } from './add/add-agent-scorer.js';
37
37
  import { addApprovalDescription } from './add/add-approval-description.js';
38
38
  import { checkIndirectRegistrations } from './add/check-indirect-registrations.js';
39
+ import { checkDisabledReferences } from './add/check-disabled-references.js';
39
40
  export const visitSetup = (logger, checker, node, state, options) => {
40
41
  addFileExtendsCoreType(node, checker, state.singletonServicesTypeImportMap, 'CoreSingletonServices', state);
41
42
  addFileExtendsCoreType(node, checker, state.wireServicesTypeImportMap, 'CoreServices', state);
@@ -45,11 +46,12 @@ export const visitSetup = (logger, checker, node, state, options) => {
45
46
  addFileWithFactory(node, checker, state.wireServicesFactories, 'CreateWireServices', state);
46
47
  addFileWithFactory(node, checker, state.configFactories, 'CreateConfig');
47
48
  addFileWithFactory(node, checker, state.serverLifecycleFactories, 'ServerLifecycle');
49
+ checkHiddenFactory(logger, node, state);
48
50
  addAnalytics(logger, node, checker, state);
49
51
  addOutgoingWebhook(logger, node, checker, state);
50
52
  addRPCInvocations(node, state, logger);
51
53
  addWireAddon(node, state, logger);
52
- addDefineScreens(node, state, logger);
54
+ addPikkuStudio(node, state, logger);
53
55
  addWireRemoteAddon(node, state, logger);
54
56
  addMiddleware(logger, node, checker, state, options);
55
57
  addPermission(logger, node, checker, state, options);
@@ -100,5 +102,6 @@ export const visitRoutes = (logger, checker, node, state, options) => {
100
102
  addFeature(logger, node, checker, state, nextOptions);
101
103
  addAgent(logger, node, checker, state, nextOptions);
102
104
  addAgentScorer(logger, node, checker, state, nextOptions);
105
+ checkDisabledReferences(logger, node, checker, state, nextOptions);
103
106
  ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
104
107
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.109",
3
+ "version": "0.12.111",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -70,10 +70,10 @@
70
70
  "zod": "^4.6.7"
71
71
  },
72
72
  "peerDependencies": {
73
- "@pikku/core": "^0.12.145"
73
+ "@pikku/core": "^0.12.147"
74
74
  },
75
75
  "devDependencies": {
76
- "@pikku/core": "^0.12.145",
76
+ "@pikku/core": "^0.12.147",
77
77
  "@types/node": "^26.6.5"
78
78
  },
79
79
  "engines": {
@@ -1,8 +0,0 @@
1
- import * as ts from 'typescript';
2
- import type { InspectorState, InspectorLogger } from '../types.js';
3
- /**
4
- * Detect `defineScreens({ title, icon, screens })` and record the manifest.
5
- * A screen with a field that is not a literal is reported and left out: a
6
- * navigation entry the host cannot read would otherwise vanish with no sign.
7
- */
8
- export declare function addDefineScreens(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
@@ -1,140 +0,0 @@
1
- import * as ts from 'typescript';
2
- import { ErrorCode } from '../error-codes.js';
3
- const propName = (prop) => {
4
- if (!ts.isPropertyAssignment(prop))
5
- return undefined;
6
- return ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
7
- ? prop.name.text
8
- : undefined;
9
- };
10
- const stringArray = (node) => {
11
- if (!ts.isArrayLiteralExpression(node))
12
- return undefined;
13
- const values = [];
14
- for (const element of node.elements) {
15
- if (!ts.isStringLiteral(element))
16
- return undefined;
17
- values.push(element.text);
18
- }
19
- return values;
20
- };
21
- /**
22
- * The module specifier of `() => import('./screens/x')`. Anything else cannot
23
- * be handed to the host's bundler as a lazy import, so it is not read.
24
- */
25
- const lazyImportSpecifier = (node) => {
26
- if (!ts.isArrowFunction(node))
27
- return undefined;
28
- const body = node.body;
29
- if (!ts.isCallExpression(body) ||
30
- body.expression.kind !== ts.SyntaxKind.ImportKeyword)
31
- return undefined;
32
- const [specifier] = body.arguments;
33
- return specifier && ts.isStringLiteral(specifier) ? specifier.text : undefined;
34
- };
35
- /**
36
- * Detect `defineScreens({ title, icon, screens })` and record the manifest.
37
- * A screen with a field that is not a literal is reported and left out: a
38
- * navigation entry the host cannot read would otherwise vanish with no sign.
39
- */
40
- export function addDefineScreens(node, state, logger) {
41
- if (!ts.isCallExpression(node))
42
- return;
43
- const { expression, arguments: args } = node;
44
- if (!ts.isIdentifier(expression) || expression.text !== 'defineScreens')
45
- return;
46
- const [firstArg] = args;
47
- if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
48
- return;
49
- const fail = (message) => logger.critical(ErrorCode.ADDON_SCREENS_NOT_STATIC, message);
50
- let title;
51
- let icon;
52
- const screens = [];
53
- for (const prop of firstArg.properties) {
54
- const key = propName(prop);
55
- if (!key || !ts.isPropertyAssignment(prop))
56
- continue;
57
- if (key === 'title' && ts.isStringLiteral(prop.initializer)) {
58
- title = prop.initializer.text;
59
- }
60
- else if (key === 'icon' && ts.isStringLiteral(prop.initializer)) {
61
- icon = prop.initializer.text;
62
- }
63
- else if (key === 'screens') {
64
- if (!ts.isArrayLiteralExpression(prop.initializer)) {
65
- fail(`defineScreens's screens must be an array literal, got: ${prop.initializer.getText()}`);
66
- continue;
67
- }
68
- for (const element of prop.initializer.elements) {
69
- if (!ts.isObjectLiteralExpression(element)) {
70
- fail(`A defineScreens screen must be an object literal, got: ${element.getText()}`);
71
- continue;
72
- }
73
- let path;
74
- let screenTitle;
75
- let nav;
76
- let scopes;
77
- let component;
78
- let app;
79
- for (const field of element.properties) {
80
- const fieldKey = propName(field);
81
- if (!fieldKey || !ts.isPropertyAssignment(field))
82
- continue;
83
- const value = field.initializer;
84
- if (fieldKey === 'path' && ts.isStringLiteral(value)) {
85
- path = value.text;
86
- }
87
- else if (fieldKey === 'title' && ts.isStringLiteral(value)) {
88
- screenTitle = value.text;
89
- }
90
- else if (fieldKey === 'nav' &&
91
- (value.kind === ts.SyntaxKind.TrueKeyword ||
92
- value.kind === ts.SyntaxKind.FalseKeyword)) {
93
- nav = value.kind === ts.SyntaxKind.TrueKeyword;
94
- }
95
- else if (fieldKey === 'scopes') {
96
- scopes = stringArray(value);
97
- if (!scopes) {
98
- fail(`A screen's scopes must be an array of string literals, got: ${value.getText()}`);
99
- }
100
- }
101
- else if (fieldKey === 'component') {
102
- component = lazyImportSpecifier(value);
103
- if (!component) {
104
- fail(`A screen's component must be () => import('<literal>'), got: ${value.getText()}`);
105
- }
106
- }
107
- else if (fieldKey === 'app' && ts.isStringLiteral(value)) {
108
- app = value.text;
109
- }
110
- }
111
- if (!path || !screenTitle || (!component && !app)) {
112
- fail(`A defineScreens screen needs a literal path and title, and either a lazy component or an app: ${element.getText()}`);
113
- continue;
114
- }
115
- if (component && app) {
116
- fail(`Screen '${path}' sets both component and app; a screen is one or the other`);
117
- continue;
118
- }
119
- screens.push({
120
- path,
121
- title: screenTitle,
122
- ...(nav !== undefined ? { nav } : {}),
123
- ...(scopes ? { scopes } : {}),
124
- ...(component ? { component } : { app: app }),
125
- });
126
- }
127
- }
128
- }
129
- if (!title) {
130
- fail(`defineScreens needs a literal title`);
131
- return;
132
- }
133
- logger.debug(`• Found defineScreens: ${title} (${screens.length} screens)`);
134
- state.screensManifest = {
135
- title,
136
- ...(icon ? { icon } : {}),
137
- file: node.getSourceFile().fileName,
138
- screens,
139
- };
140
- }