@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.
@@ -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, parse, relative } from 'path';
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 loadScreensManifest = async (require, packageName) => {
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
- for (const needed of await readAddonNeeds(dirs, decl.package)) {
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 loadScreensManifest(require, decl.package);
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 defineScreens call to the package, or drop ui.`);
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.addonScreens ??= {})[namespace] = {
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
- if (hasWebhookSources ||
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
- requiredServices.add(service);
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 [pkg, target] of Object.entries(addonDecl.uses ?? {})) {
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 '${pkg}' as '${target}', but no addon is wired under the name '${target}'. Wired: ${Array.from(wireAddonDeclarations.keys()).join(', ')}`);
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 !== pkg) {
434
- logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, `Addon '${namespace}' uses '${pkg}' as '${target}', but '${target}' is wired from '${wired.package}'.`);
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
- screensManifest?: InspectorState['screensManifest'];
332
- addonScreens?: InspectorState['addonScreens'];
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
- screensManifest: state.screensManifest,
195
- addonScreens: state.addonScreens,
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
- screensManifest: data.screensManifest,
404
- addonScreens: data.addonScreens,
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 { 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.110",
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.146"
73
+ "@pikku/core": "^0.12.147"
74
74
  },
75
75
  "devDependencies": {
76
- "@pikku/core": "^0.12.146",
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
- }