@pikku/inspector 0.12.110 → 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.
- package/dist/add/add-channel.js +8 -0
- package/dist/add/add-file-with-factory.d.ts +7 -1
- package/dist/add/add-file-with-factory.js +63 -2
- package/dist/add/add-functions.js +18 -1
- package/dist/add/add-pikku-studio.d.ts +15 -0
- package/dist/add/add-pikku-studio.js +307 -0
- package/dist/add/add-rpc-invocations.js +12 -0
- package/dist/add/check-disabled-references.d.ts +27 -0
- package/dist/add/check-disabled-references.js +226 -0
- package/dist/error-codes.d.ts +9 -0
- package/dist/error-codes.js +9 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/initial-state.js +1 -0
- package/dist/inspector.js +4 -0
- package/dist/lite.d.ts +3 -0
- package/dist/lite.js +2 -0
- package/dist/types.d.ts +49 -13
- package/dist/utils/addon-dependencies.d.ts +43 -0
- package/dist/utils/addon-dependencies.js +91 -0
- package/dist/utils/find-unused-functions.d.ts +20 -0
- package/dist/utils/find-unused-functions.js +132 -0
- package/dist/utils/get-property-value.js +4 -1
- package/dist/utils/load-addon-functions-meta.js +37 -43
- package/dist/utils/post-process.js +31 -7
- package/dist/utils/serialize-inspector-state.d.ts +3 -2
- package/dist/utils/serialize-inspector-state.js +8 -4
- package/dist/visit.js +6 -3
- package/package.json +3 -3
- package/dist/add/add-define-screens.d.ts +0 -8
- package/dist/add/add-define-screens.js +0 -140
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Scenarios and their steps are run by the scenario runner, never wired or called. */
|
|
2
|
+
const RUN_BY_SCENARIOS = new Set([
|
|
3
|
+
'pikkuScenario',
|
|
4
|
+
'pikkuScenarioStep',
|
|
5
|
+
'pikkuPlatformScenarioStep',
|
|
6
|
+
'pikkuAddonScenarioStep',
|
|
7
|
+
]);
|
|
8
|
+
/** Sections that describe functions rather than use them. */
|
|
9
|
+
const NOT_USES = new Set([
|
|
10
|
+
'functions',
|
|
11
|
+
'schemas',
|
|
12
|
+
'schemaLookup',
|
|
13
|
+
'serviceAggregation',
|
|
14
|
+
'nodes',
|
|
15
|
+
'resolvedIOTypes',
|
|
16
|
+
'addonFunctions',
|
|
17
|
+
'remoteAddonFunctions',
|
|
18
|
+
'program',
|
|
19
|
+
'stats',
|
|
20
|
+
]);
|
|
21
|
+
/** Keys under which a wiring names the function it runs. */
|
|
22
|
+
const FUNCTION_KEYS = new Set(['pikkuFuncId', 'rpcName', 'receive']);
|
|
23
|
+
const collectUses = (value, into, seen, key) => {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
if (key && FUNCTION_KEYS.has(key))
|
|
26
|
+
into.add(value);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object' || seen.has(value))
|
|
30
|
+
return;
|
|
31
|
+
seen.add(value);
|
|
32
|
+
if (value instanceof Map) {
|
|
33
|
+
for (const child of value.values())
|
|
34
|
+
collectUses(child, into, seen);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof Set)
|
|
38
|
+
return;
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
for (const child of value) {
|
|
41
|
+
if (key === 'tools' && typeof child === 'string')
|
|
42
|
+
into.add(child);
|
|
43
|
+
else
|
|
44
|
+
collectUses(child, into, seen);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Only plain data: the TypeScript inspector's state also holds compiler
|
|
49
|
+
// objects (types, nodes) whose getters throw when read.
|
|
50
|
+
const proto = Object.getPrototypeOf(value);
|
|
51
|
+
if (proto !== Object.prototype && proto !== null)
|
|
52
|
+
return;
|
|
53
|
+
for (const [childKey, child] of Object.entries(value)) {
|
|
54
|
+
collectUses(child, into, seen, childKey);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The app's own functions that nothing uses: not wired to any transport, not
|
|
59
|
+
* exposed, published or offered to MCP, and not called by name. Addon
|
|
60
|
+
* functions are never judged: an addon project's own functions are all addon
|
|
61
|
+
* functions, and a consumer's state holds no addon functions in
|
|
62
|
+
* `functions.meta`. Neither are scenarios, inline wiring functions or disabled
|
|
63
|
+
* functions. A workflow is a function like any other: it is used when it is
|
|
64
|
+
* wired, exposed, or started by a literal name — `rpc.startWorkflow`,
|
|
65
|
+
* `workflowService.startWorkflow` / `runToCompletion`, `rpc.invoke`, another
|
|
66
|
+
* workflow's step, or the generated client helpers.
|
|
67
|
+
*/
|
|
68
|
+
export const findUnusedFunctions = (state, { addon = false } = {}) => {
|
|
69
|
+
if (addon)
|
|
70
|
+
return [];
|
|
71
|
+
const uses = new Set(state.serviceAggregation.usedFunctions);
|
|
72
|
+
for (const name of state.rpc.invokedFunctions)
|
|
73
|
+
uses.add(name);
|
|
74
|
+
for (const id of Object.values(state.rpc.exposedMeta))
|
|
75
|
+
uses.add(id);
|
|
76
|
+
const seen = new WeakSet();
|
|
77
|
+
for (const [section, value] of Object.entries(state)) {
|
|
78
|
+
if (NOT_USES.has(section) || section === 'rpc' || section === 'workflows')
|
|
79
|
+
continue;
|
|
80
|
+
collectUses(value, uses, seen);
|
|
81
|
+
}
|
|
82
|
+
// A workflow's own meta and graph name the workflow itself; only what its
|
|
83
|
+
// steps call counts as a use.
|
|
84
|
+
const { meta: workflowsMeta, graphMeta } = state.workflows;
|
|
85
|
+
for (const entries of [workflowsMeta, graphMeta]) {
|
|
86
|
+
for (const [workflowName, entry] of Object.entries(entries ?? {})) {
|
|
87
|
+
const calls = new Set();
|
|
88
|
+
collectUses(entry, calls, seen);
|
|
89
|
+
calls.delete(workflowName);
|
|
90
|
+
if (entry?.pikkuFuncId)
|
|
91
|
+
calls.delete(entry.pikkuFuncId);
|
|
92
|
+
for (const call of calls)
|
|
93
|
+
uses.add(call);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Workflows started by name: client helpers, `startWorkflow` and
|
|
97
|
+
// `runToCompletion` calls, and each function's own `rpc.startWorkflow`.
|
|
98
|
+
const startedWorkflows = new Set(state.workflows.invokedWorkflows);
|
|
99
|
+
for (const meta of Object.values(state.functions.meta)) {
|
|
100
|
+
for (const name of meta.startsWorkflows ?? [])
|
|
101
|
+
startedWorkflows.add(name);
|
|
102
|
+
}
|
|
103
|
+
for (const workflowName of startedWorkflows)
|
|
104
|
+
uses.add(workflowName);
|
|
105
|
+
// A workflow named by its workflow name, by any of the above or by
|
|
106
|
+
// `rpc.invoke` and step calls, uses the function behind it.
|
|
107
|
+
for (const [workflowName, entry] of Object.entries(workflowsMeta)) {
|
|
108
|
+
if (uses.has(workflowName) && entry?.pikkuFuncId)
|
|
109
|
+
uses.add(entry.pikkuFuncId);
|
|
110
|
+
}
|
|
111
|
+
const unused = [];
|
|
112
|
+
for (const [pikkuFuncId, meta] of Object.entries(state.functions.meta)) {
|
|
113
|
+
if (meta.functionType !== 'user')
|
|
114
|
+
continue;
|
|
115
|
+
if (meta.funcWrapper && RUN_BY_SCENARIOS.has(meta.funcWrapper))
|
|
116
|
+
continue;
|
|
117
|
+
if (meta.expose || meta.remote || meta.mcp)
|
|
118
|
+
continue;
|
|
119
|
+
// A generated scaffold is rewritten on every run, so `disabled` cannot be
|
|
120
|
+
// added to it; what it leaves unwired it leaves for the project to wire.
|
|
121
|
+
if (meta.sourceFile?.endsWith('.gen.ts'))
|
|
122
|
+
continue;
|
|
123
|
+
if (uses.has(pikkuFuncId) || (meta.name && uses.has(meta.name)))
|
|
124
|
+
continue;
|
|
125
|
+
unused.push({
|
|
126
|
+
pikkuFuncId,
|
|
127
|
+
...(meta.name ? { name: meta.name } : {}),
|
|
128
|
+
...(meta.sourceFile ? { sourceFile: meta.sourceFile } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return unused;
|
|
132
|
+
};
|
|
@@ -202,7 +202,10 @@ export const getCommonWireMetaData = (obj, wiringType, wiringName, logger, check
|
|
|
202
202
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
203
203
|
const propName = prop.name.text;
|
|
204
204
|
if (propName === 'disabled' &&
|
|
205
|
-
prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
205
|
+
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
206
|
+
((ts.isStringLiteral(prop.initializer) ||
|
|
207
|
+
ts.isNoSubstitutionTemplateLiteral(prop.initializer)) &&
|
|
208
|
+
prop.initializer.text !== ''))) {
|
|
206
209
|
metadata.disabled = true;
|
|
207
210
|
}
|
|
208
211
|
else if (propName === 'title') {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
|
-
import { join, dirname,
|
|
3
|
+
import { join, dirname, relative } from 'path';
|
|
4
4
|
import { addonResolutionDirs, createAddonResolver, } from './addon-resolution.js';
|
|
5
5
|
import { ErrorCode } from '../error-codes.js';
|
|
6
6
|
import { checkLockedSdk } from './remote-addon-sdk.js';
|
|
7
|
+
import { declaredAddonNames, findInstalledPackageDir, readAddonDependencies, } from './addon-dependencies.js';
|
|
7
8
|
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
8
9
|
value !== null &&
|
|
9
10
|
'method' in value &&
|
|
@@ -92,24 +93,6 @@ const resolveAddonMeta = (require, packageName, baseName) => {
|
|
|
92
93
|
}
|
|
93
94
|
return null;
|
|
94
95
|
};
|
|
95
|
-
const findInstalledPackageDir = (dirs, packageName) => {
|
|
96
|
-
for (const start of dirs) {
|
|
97
|
-
const root = parse(start).root;
|
|
98
|
-
let dir = start;
|
|
99
|
-
while (dir) {
|
|
100
|
-
const candidate = join(dir, 'node_modules', packageName);
|
|
101
|
-
if (existsSync(join(candidate, 'package.json')))
|
|
102
|
-
return candidate;
|
|
103
|
-
if (dir === root)
|
|
104
|
-
break;
|
|
105
|
-
const parent = dirname(dir);
|
|
106
|
-
if (parent === dir)
|
|
107
|
-
break;
|
|
108
|
-
dir = parent;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
};
|
|
113
96
|
export const describeMissingAddonMeta = (namespace, packageName, dirs, declFile) => {
|
|
114
97
|
const caller = declFile
|
|
115
98
|
? relative(dirs[dirs.length - 1], declFile) || declFile
|
|
@@ -183,27 +166,12 @@ export const namespaceAddonWebhookSources = (sources, namespace) => {
|
|
|
183
166
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
184
167
|
* schedules, etc.) can look up addon function types during the routes sweep.
|
|
185
168
|
*/
|
|
186
|
-
const
|
|
169
|
+
const loadStudioMeta = async (require, packageName) => {
|
|
187
170
|
const path = resolveAddonMeta(require, packageName, 'screens/pikku-screens-meta');
|
|
188
171
|
if (!path)
|
|
189
172
|
return null;
|
|
190
173
|
return JSON.parse(await readFile(path, 'utf-8'));
|
|
191
174
|
};
|
|
192
|
-
const readAddonNeeds = async (dirs, packageName) => {
|
|
193
|
-
const dir = findInstalledPackageDir(dirs, packageName);
|
|
194
|
-
if (!dir)
|
|
195
|
-
return [];
|
|
196
|
-
try {
|
|
197
|
-
const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
|
|
198
|
-
const uses = pkg?.pikku?.uses;
|
|
199
|
-
return Array.isArray(uses)
|
|
200
|
-
? uses.filter((u) => typeof u === 'string')
|
|
201
|
-
: [];
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
return [];
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
175
|
const isContributorRef = (value) => !!value &&
|
|
208
176
|
typeof value === 'object' &&
|
|
209
177
|
typeof value.module === 'string' &&
|
|
@@ -260,6 +228,36 @@ const loadRemoteAddonSdkMeta = async (state, namespace, packageName, range) => {
|
|
|
260
228
|
return;
|
|
261
229
|
(state.remoteAddonFunctions ??= {})[namespace] = JSON.parse(await readFile(metaPath, 'utf-8'));
|
|
262
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Reads the addons a wired package says it calls (`pikku.uses` in its
|
|
233
|
+
* package.json) onto its declaration, and reports each one this wiring leaves
|
|
234
|
+
* unmapped. Whether a mapping points at the right package, and whether the
|
|
235
|
+
* wiring maps a name the package never declared, is `validateAddonUses`' call
|
|
236
|
+
* once every wiring is known.
|
|
237
|
+
*/
|
|
238
|
+
const recordDeclaredUses = (logger, namespace, decl, dirs) => {
|
|
239
|
+
const packageDir = findInstalledPackageDir(dirs, decl.package);
|
|
240
|
+
if (!packageDir)
|
|
241
|
+
return;
|
|
242
|
+
const declared = readAddonDependencies(packageDir);
|
|
243
|
+
if (declared.form === 'none')
|
|
244
|
+
return;
|
|
245
|
+
if (declared.form === 'map') {
|
|
246
|
+
for (const message of declared.invalid) {
|
|
247
|
+
logger.critical(ErrorCode.ADDON_DEPENDENCY_INVALID, `${decl.package} (wired as '${namespace}'): ${message}`);
|
|
248
|
+
}
|
|
249
|
+
decl.dependencies = declared.dependencies;
|
|
250
|
+
}
|
|
251
|
+
decl.declaredUses = declaredAddonNames(declared);
|
|
252
|
+
for (const needed of decl.declaredUses) {
|
|
253
|
+
if (decl.uses?.[needed])
|
|
254
|
+
continue;
|
|
255
|
+
const dependency = decl.dependencies?.find((d) => d.alias === needed);
|
|
256
|
+
logger.critical(ErrorCode.ADDON_NEEDS_NOT_MAPPED, dependency
|
|
257
|
+
? `${decl.package} calls ${dependency.package}@${dependency.range} as '${needed}', but wireAddon('${namespace}') does not map it. Wire ${dependency.package} and add uses: { '${needed}': '<name>' } to this wireAddon.`
|
|
258
|
+
: `${decl.package} needs ${needed}, but wireAddon('${namespace}') does not map it. Wire ${needed} and add uses: { '${needed}': '<name>' } to this wireAddon.`);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
263
261
|
export async function loadAddonFunctionsMeta(logger, state) {
|
|
264
262
|
const { wireAddonDeclarations } = state.rpc;
|
|
265
263
|
if (wireAddonDeclarations.size === 0)
|
|
@@ -287,16 +285,12 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
287
285
|
catch (error) {
|
|
288
286
|
logger.error(`wireAddon('${namespace}'): ${error instanceof Error ? error.message : error}`);
|
|
289
287
|
}
|
|
290
|
-
|
|
291
|
-
if (!decl.uses?.[needed]) {
|
|
292
|
-
logger.critical(ErrorCode.ADDON_NEEDS_NOT_MAPPED, `${decl.package} needs ${needed}, but wireAddon('${namespace}') does not map it. Wire ${needed} and add uses: { '${needed}': '<name>' } to this wireAddon.`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
288
|
+
recordDeclaredUses(logger, namespace, decl, dirs);
|
|
295
289
|
if (decl.ui) {
|
|
296
290
|
let manifest;
|
|
297
291
|
let unreadable = false;
|
|
298
292
|
try {
|
|
299
|
-
manifest = await
|
|
293
|
+
manifest = await loadStudioMeta(require, decl.package);
|
|
300
294
|
}
|
|
301
295
|
catch (e) {
|
|
302
296
|
logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but the screens manifest of ${decl.package} could not be read: ${e instanceof Error ? e.message : e}`);
|
|
@@ -306,11 +300,11 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
306
300
|
// reported above; the addon's functions and scopes still load
|
|
307
301
|
}
|
|
308
302
|
else if (!manifest || manifest.screens.length === 0) {
|
|
309
|
-
logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but ${decl.package} declares no screens. Add a
|
|
303
|
+
logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but ${decl.package} declares no screens. Add a pikkuStudio call to the package, or drop ui.`);
|
|
310
304
|
}
|
|
311
305
|
else {
|
|
312
306
|
;
|
|
313
|
-
(state.
|
|
307
|
+
(state.addonStudio ??= {})[namespace] = {
|
|
314
308
|
...manifest,
|
|
315
309
|
scopes: [
|
|
316
310
|
...new Set([...(manifest.scopes ?? []), ...(decl.scopes ?? [])]),
|
|
@@ -239,8 +239,9 @@ export function aggregateRequiredServices(state) {
|
|
|
239
239
|
// nothing, so they are found by id: the route accepts into the queue through
|
|
240
240
|
// incomingWebhookService and the worker records attempts through it.
|
|
241
241
|
const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
|
|
242
|
-
|
|
243
|
-
state.functions.meta['queue:pikku-incoming-webhooks'] !== undefined
|
|
242
|
+
const wiresIncomingWebhooks = hasWebhookSources ||
|
|
243
|
+
state.functions.meta['queue:pikku-incoming-webhooks'] !== undefined;
|
|
244
|
+
if (wiresIncomingWebhooks) {
|
|
244
245
|
requiredServices.add('incomingWebhookService');
|
|
245
246
|
requiredServices.add('queueService');
|
|
246
247
|
}
|
|
@@ -333,6 +334,12 @@ export function aggregateRequiredServices(state) {
|
|
|
333
334
|
addonFnServices.set(`${namespace}:${id}`, meta?.services?.services);
|
|
334
335
|
}
|
|
335
336
|
}
|
|
337
|
+
// An addon forwarding `incomingWebhookService` (pikku-admin lists receipts)
|
|
338
|
+
// reads it when the app has one; it never makes the app keep receipts. Only
|
|
339
|
+
// the app's own webhook sources do, so only they bring in the service and its
|
|
340
|
+
// tables — otherwise an app with no webhook wired fails to boot on a
|
|
341
|
+
// `webhook_receipt` table nothing writes to.
|
|
342
|
+
const readOnlyIfPresent = (service) => service === 'incomingWebhookService' && !wiresIncomingWebhooks;
|
|
336
343
|
const parentDeclared = state.addonRequiredParentServices ?? [];
|
|
337
344
|
const parentDeclaredSet = new Set(parentDeclared);
|
|
338
345
|
const defaultServices = new Set([
|
|
@@ -354,6 +361,9 @@ export function aggregateRequiredServices(state) {
|
|
|
354
361
|
continue;
|
|
355
362
|
}
|
|
356
363
|
for (const service of services) {
|
|
364
|
+
if (readOnlyIfPresent(service)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
357
367
|
if (parentDeclaredSet.has(service)) {
|
|
358
368
|
requiredServices.add(service);
|
|
359
369
|
}
|
|
@@ -365,7 +375,9 @@ export function aggregateRequiredServices(state) {
|
|
|
365
375
|
}
|
|
366
376
|
if (usesAddonFn && addonFactoryNeeded) {
|
|
367
377
|
for (const service of parentDeclared) {
|
|
368
|
-
|
|
378
|
+
if (!readOnlyIfPresent(service)) {
|
|
379
|
+
requiredServices.add(service);
|
|
380
|
+
}
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
// 8. When this project IS an addon, what it needs from its parent is whatever
|
|
@@ -402,6 +414,11 @@ export function validateSecretOverrides(logger, state) {
|
|
|
402
414
|
// passes to getSecret — so validate against secretId, falling back to name for
|
|
403
415
|
// older meta without a secretId field.
|
|
404
416
|
const secretIds = new Set(state.secrets.definitions.map((d) => d.secretId ?? d.name));
|
|
417
|
+
// auth-secrets.gen.ts declares it, but that file is written after the first
|
|
418
|
+
// inspection of a fresh project, so its defineSecret is not seen yet — the
|
|
419
|
+
// same fallback validateSecretUsage takes.
|
|
420
|
+
if (state.auth?.definition)
|
|
421
|
+
secretIds.add('BETTER_AUTH_SECRET');
|
|
405
422
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
406
423
|
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
|
|
407
424
|
if (!secretIds.has(resolvedName)) {
|
|
@@ -425,13 +442,20 @@ export function validateAddonUses(logger, state) {
|
|
|
425
442
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
426
443
|
return;
|
|
427
444
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
428
|
-
for (const [
|
|
445
|
+
for (const [name, target] of Object.entries(addonDecl.uses ?? {})) {
|
|
446
|
+
if (addonDecl.declaredUses && !addonDecl.declaredUses.includes(name)) {
|
|
447
|
+
logger.critical(ErrorCode.ADDON_USES_NOT_DECLARED, `Addon '${namespace}' maps '${name}' to '${target}', but ${addonDecl.package} does not declare '${name}' in the pikku.uses of its package.json. It declares: ${addonDecl.declaredUses.join(', ') || 'nothing'}. Remove '${name}' from uses, or have the addon declare it.`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const expected = addonDecl.dependencies?.find((d) => d.alias === name)?.package ?? name;
|
|
429
451
|
const wired = wireAddonDeclarations.get(target);
|
|
430
452
|
if (!wired) {
|
|
431
|
-
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, `Addon '${namespace}' uses '${
|
|
453
|
+
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, `Addon '${namespace}' uses '${name}' as '${target}', but no addon is wired under the name '${target}'. Wired: ${Array.from(wireAddonDeclarations.keys()).join(', ')}`);
|
|
432
454
|
}
|
|
433
|
-
else if (wired.package !==
|
|
434
|
-
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED,
|
|
455
|
+
else if (wired.package !== expected) {
|
|
456
|
+
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, expected === name
|
|
457
|
+
? `Addon '${namespace}' uses '${name}' as '${target}', but '${target}' is wired from '${wired.package}'.`
|
|
458
|
+
: `Addon '${namespace}' calls ${expected} as '${name}', mapped to '${target}', but '${target}' is wired from '${wired.package}'.`);
|
|
435
459
|
}
|
|
436
460
|
}
|
|
437
461
|
}
|
|
@@ -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<[
|
|
@@ -328,8 +329,8 @@ export interface SerializableInspectorState {
|
|
|
328
329
|
diagnostics: InspectorDiagnostic[];
|
|
329
330
|
addonFunctions: InspectorState['addonFunctions'];
|
|
330
331
|
remoteAddonFunctions?: InspectorState['remoteAddonFunctions'];
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
studio?: InspectorState['studio'];
|
|
333
|
+
addonStudio?: InspectorState['addonStudio'];
|
|
333
334
|
addonContributors?: InspectorState['addonContributors'];
|
|
334
335
|
exportedContracts: InspectorState['exportedContracts'];
|
|
335
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()),
|
|
@@ -191,8 +194,8 @@ export function serializeInspectorState(state) {
|
|
|
191
194
|
diagnostics: state.diagnostics,
|
|
192
195
|
addonFunctions: state.addonFunctions,
|
|
193
196
|
remoteAddonFunctions: state.remoteAddonFunctions,
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
studio: state.studio,
|
|
198
|
+
addonStudio: state.addonStudio,
|
|
196
199
|
addonContributors: state.addonContributors,
|
|
197
200
|
exportedContracts: state.exportedContracts,
|
|
198
201
|
};
|
|
@@ -240,6 +243,7 @@ export function deserializeInspectorState(data) {
|
|
|
240
243
|
files: new Map(data.functions.files),
|
|
241
244
|
approvalDescriptions: data.functions.approvalDescriptions || {},
|
|
242
245
|
dynamicImportIds: new Set(data.functions.dynamicImportIds ?? []),
|
|
246
|
+
disabled: data.functions.disabled ?? {},
|
|
243
247
|
},
|
|
244
248
|
http: {
|
|
245
249
|
metaInputTypes: new Map(data.http.metaInputTypes),
|
|
@@ -400,8 +404,8 @@ export function deserializeInspectorState(data) {
|
|
|
400
404
|
...(data.remoteAddonFunctions
|
|
401
405
|
? { remoteAddonFunctions: data.remoteAddonFunctions }
|
|
402
406
|
: {}),
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
studio: data.studio,
|
|
408
|
+
addonStudio: data.addonStudio,
|
|
405
409
|
addonContributors: data.addonContributors,
|
|
406
410
|
exportedContracts: data.exportedContracts || {
|
|
407
411
|
http: {},
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
73
|
+
"@pikku/core": "^0.12.147"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@pikku/core": "^0.12.
|
|
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
|
-
}
|