@pikku/inspector 0.12.111 → 0.12.112

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,10 @@
1
+ import * as ts from 'typescript';
2
+ import type { InspectorLogger, InspectorState } from '../types.js';
3
+ /**
4
+ * Detect `defineContributors({ [name]: { dev?, deploy? } })` and record, per
5
+ * name, the module and export of each variant. A name defined twice, or a
6
+ * variant a host could not import by itself, is a critical error: a contributor
7
+ * that silently drops out would leave the host without the service it opted in
8
+ * to.
9
+ */
10
+ export declare function addDefineContributors(logger: InspectorLogger, node: ts.Node, checker: ts.TypeChecker, state: InspectorState): void;
@@ -0,0 +1,98 @@
1
+ import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ import { callsImportedDefiner, exportedName } from './add-analytics.js';
4
+ const DEFINE_CONTRIBUTORS = 'defineContributors';
5
+ const VARIANTS = ['dev', 'deploy'];
6
+ const propertyKey = (name) => ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
7
+ /**
8
+ * Where an exported contributor const lives and what it is exported as. A
9
+ * host imports exactly this, so a variant that is inline, not exported, or
10
+ * built by a call has no answer and is reported instead.
11
+ */
12
+ const resolveVariant = (value, checker) => {
13
+ if (!ts.isIdentifier(value)) {
14
+ return `must be a reference to an exported const so a host can import just that variant, got: ${value.getText()}`;
15
+ }
16
+ let symbol = checker.getSymbolAtLocation(value);
17
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
18
+ symbol = checker.getAliasedSymbol(symbol);
19
+ }
20
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
21
+ if (!declaration ||
22
+ !ts.isVariableDeclaration(declaration) ||
23
+ !ts.isIdentifier(declaration.name)) {
24
+ return `'${value.text}' does not resolve to a const declaration`;
25
+ }
26
+ const exported = exportedName(declaration, declaration.name, checker);
27
+ if (!exported) {
28
+ return `'${value.text}' is not exported by ${declaration.getSourceFile().fileName}`;
29
+ }
30
+ return { module: declaration.getSourceFile().fileName, export: exported };
31
+ };
32
+ /**
33
+ * Detect `defineContributors({ [name]: { dev?, deploy? } })` and record, per
34
+ * name, the module and export of each variant. A name defined twice, or a
35
+ * variant a host could not import by itself, is a critical error: a contributor
36
+ * that silently drops out would leave the host without the service it opted in
37
+ * to.
38
+ */
39
+ export function addDefineContributors(logger, node, checker, state) {
40
+ if (!ts.isCallExpression(node))
41
+ return;
42
+ if (!callsImportedDefiner(node.expression, checker, DEFINE_CONTRIBUTORS))
43
+ return;
44
+ const fail = (code, message) => logger.critical(code, message);
45
+ const notStatic = (message) => fail(ErrorCode.CONTRIBUTORS_NOT_STATIC, message);
46
+ const [argument] = node.arguments;
47
+ if (!argument || !ts.isObjectLiteralExpression(argument)) {
48
+ notStatic(`defineContributors must be called with an object literal keyed by contributor name${argument ? `, got: ${argument.getText()}` : ''}`);
49
+ return;
50
+ }
51
+ for (const property of argument.properties) {
52
+ const name = ts.isPropertyAssignment(property)
53
+ ? propertyKey(property.name)
54
+ : undefined;
55
+ if (!name || !ts.isPropertyAssignment(property)) {
56
+ notStatic(`defineContributors takes { name: { dev?, deploy? } } entries, got: ${property.getText()}`);
57
+ continue;
58
+ }
59
+ if (!ts.isObjectLiteralExpression(property.initializer)) {
60
+ notStatic(`Contributor '${name}' must be an object literal with dev and/or deploy, got: ${property.initializer.getText()}`);
61
+ continue;
62
+ }
63
+ const variants = {};
64
+ let valid = true;
65
+ for (const variant of property.initializer.properties) {
66
+ const key = ts.isPropertyAssignment(variant)
67
+ ? propertyKey(variant.name)
68
+ : undefined;
69
+ if (!key ||
70
+ !ts.isPropertyAssignment(variant) ||
71
+ !VARIANTS.includes(key)) {
72
+ notStatic(`Contributor '${name}' takes only dev and deploy, got: ${variant.getText()}`);
73
+ valid = false;
74
+ continue;
75
+ }
76
+ const ref = resolveVariant(variant.initializer, checker);
77
+ if (typeof ref === 'string') {
78
+ notStatic(`Contributor '${name}' ${key} ${ref}`);
79
+ valid = false;
80
+ continue;
81
+ }
82
+ variants[key] = ref;
83
+ }
84
+ if (!valid)
85
+ continue;
86
+ if (!variants.dev && !variants.deploy) {
87
+ notStatic(`Contributor '${name}' declares neither dev nor deploy`);
88
+ continue;
89
+ }
90
+ const manifest = (state.contributorsManifest ??= {});
91
+ if (manifest[name]) {
92
+ fail(ErrorCode.CONTRIBUTOR_NAME_DUPLICATED, `Contributor '${name}' is defined twice. A contributor name belongs to one defineContributors entry.`);
93
+ continue;
94
+ }
95
+ logger.debug(`• Found ${DEFINE_CONTRIBUTORS}: ${name}`);
96
+ manifest[name] = variants;
97
+ }
98
+ }
@@ -1,14 +1,14 @@
1
1
  import * as ts from 'typescript';
2
2
  import type { InspectorState, InspectorLogger } from '../types.js';
3
3
  /**
4
- * Detect `pikkuStudio({ title, screens })` and record it as the package's
4
+ * Detect `definePlugin({ title, screens })` and record it as the package's
5
5
  * `studio` metadata. Every field must be a literal: one that is not is
6
6
  * reported and its screen left out, since a screen the host cannot read would
7
7
  * otherwise vanish with no sign.
8
8
  */
9
9
  export declare function addPikkuStudio(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
10
10
  /**
11
- * The checks on `pikkuStudio` that need the whole package: every title is a
11
+ * The checks on `definePlugin` that need the whole package: every title is a
12
12
  * key in the addon's `messages/en.json` (a key missing from another locale is
13
13
  * a warning), and every nav badge names one of the addon's functions.
14
14
  */
@@ -97,7 +97,7 @@ const readNav = (node, fail) => {
97
97
  };
98
98
  const readScreen = (element, fail) => {
99
99
  if (!ts.isObjectLiteralExpression(element)) {
100
- fail(`A pikkuStudio screen must be an object literal, got: ${element.getText()}`);
100
+ fail(`A definePlugin screen must be an object literal, got: ${element.getText()}`);
101
101
  return undefined;
102
102
  }
103
103
  let path;
@@ -166,7 +166,7 @@ const readScreen = (element, fail) => {
166
166
  if (!ok)
167
167
  return undefined;
168
168
  if (!path || !title || !scope || (!component && !app)) {
169
- fail(`A pikkuStudio screen needs a literal path, title and scope, and either a lazy component or an app: ${element.getText()}`);
169
+ fail(`A definePlugin screen needs a literal path, title and scope, and either a lazy component or an app: ${element.getText()}`);
170
170
  return undefined;
171
171
  }
172
172
  if (component && app) {
@@ -186,7 +186,7 @@ const readScreen = (element, fail) => {
186
186
  };
187
187
  };
188
188
  /**
189
- * Detect `pikkuStudio({ title, screens })` and record it as the package's
189
+ * Detect `definePlugin({ title, screens })` and record it as the package's
190
190
  * `studio` metadata. Every field must be a literal: one that is not is
191
191
  * reported and its screen left out, since a screen the host cannot read would
192
192
  * otherwise vanish with no sign.
@@ -195,7 +195,7 @@ export function addPikkuStudio(node, state, logger) {
195
195
  if (!ts.isCallExpression(node))
196
196
  return;
197
197
  const { expression, arguments: args } = node;
198
- if (!ts.isIdentifier(expression) || expression.text !== 'pikkuStudio')
198
+ if (!ts.isIdentifier(expression) || expression.text !== 'definePlugin' && expression.text !== 'pikkuStudio')
199
199
  return;
200
200
  const [firstArg] = args;
201
201
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
@@ -206,7 +206,7 @@ export function addPikkuStudio(node, state, logger) {
206
206
  for (const prop of firstArg.properties) {
207
207
  const key = propName(prop);
208
208
  if (!key || !ts.isPropertyAssignment(prop)) {
209
- fail(`A pikkuStudio field must be a literal, got: ${prop.getText()}`);
209
+ fail(`A definePlugin field must be a literal, got: ${prop.getText()}`);
210
210
  continue;
211
211
  }
212
212
  if (key === 'title') {
@@ -214,12 +214,12 @@ export function addPikkuStudio(node, state, logger) {
214
214
  title = prop.initializer.text;
215
215
  }
216
216
  else {
217
- fail(`pikkuStudio's title must be a literal, got: ${prop.initializer.getText()}`);
217
+ fail(`definePlugin's title must be a literal, got: ${prop.initializer.getText()}`);
218
218
  }
219
219
  }
220
220
  else if (key === 'screens') {
221
221
  if (!ts.isArrayLiteralExpression(prop.initializer)) {
222
- fail(`pikkuStudio's screens must be an array literal, got: ${prop.initializer.getText()}`);
222
+ fail(`definePlugin's screens must be an array literal, got: ${prop.initializer.getText()}`);
223
223
  continue;
224
224
  }
225
225
  for (const element of prop.initializer.elements) {
@@ -230,17 +230,17 @@ export function addPikkuStudio(node, state, logger) {
230
230
  }
231
231
  }
232
232
  if (!title) {
233
- fail(`pikkuStudio needs a literal title`);
233
+ fail(`definePlugin needs a literal title`);
234
234
  return;
235
235
  }
236
236
  const seen = new Set();
237
237
  for (const screen of screens) {
238
238
  if (seen.has(screen.path)) {
239
- logger.critical(ErrorCode.STUDIO_SCREEN_PATH_DUPLICATE, `pikkuStudio declares two screens at '${screen.path}'; each screen needs its own path`);
239
+ logger.critical(ErrorCode.STUDIO_SCREEN_PATH_DUPLICATE, `definePlugin declares two screens at '${screen.path}'; each screen needs its own path`);
240
240
  }
241
241
  seen.add(screen.path);
242
242
  }
243
- logger.debug(`• Found pikkuStudio: ${title} (${screens.length} screens)`);
243
+ logger.debug(`• Found definePlugin: ${title} (${screens.length} screens)`);
244
244
  state.studio = {
245
245
  title,
246
246
  file: node.getSourceFile().fileName,
@@ -268,7 +268,7 @@ const readCatalog = (file) => {
268
268
  }
269
269
  };
270
270
  /**
271
- * The checks on `pikkuStudio` that need the whole package: every title is a
271
+ * The checks on `definePlugin` that need the whole package: every title is a
272
272
  * key in the addon's `messages/en.json` (a key missing from another locale is
273
273
  * a warning), and every nav badge names one of the addon's functions.
274
274
  */
@@ -282,7 +282,7 @@ export function validatePikkuStudio(logger, state) {
282
282
  const en = messagesDir ? readCatalog(join(messagesDir, 'en.json')) : undefined;
283
283
  for (const key of new Set(keys)) {
284
284
  if (!en || !Object.hasOwn(en, key)) {
285
- logger.critical(ErrorCode.STUDIO_MESSAGE_KEY_MISSING, `pikkuStudio uses the message key '${key}', which is missing from the addon's messages/en.json`);
285
+ logger.critical(ErrorCode.STUDIO_MESSAGE_KEY_MISSING, `definePlugin uses the message key '${key}', which is missing from the addon's messages/en.json`);
286
286
  }
287
287
  }
288
288
  if (messagesDir && existsSync(messagesDir)) {
@@ -293,7 +293,7 @@ export function validatePikkuStudio(logger, state) {
293
293
  const catalog = readCatalog(join(messagesDir, name)) ?? {};
294
294
  for (const key of new Set(keys)) {
295
295
  if (!Object.hasOwn(catalog, key)) {
296
- logger.warn(`pikkuStudio uses the message key '${key}', which is missing from the addon's messages/${name}`);
296
+ logger.warn(`definePlugin uses the message key '${key}', which is missing from the addon's messages/${name}`);
297
297
  }
298
298
  }
299
299
  }
@@ -87,8 +87,7 @@ export function addWireAddon(node, state, logger) {
87
87
  if (!ts.isCallExpression(node))
88
88
  return;
89
89
  const { expression, arguments: args } = node;
90
- if (!ts.isIdentifier(expression) ||
91
- expression.text !== 'wireAddon')
90
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireAddon')
92
91
  return;
93
92
  const [firstArg] = args;
94
93
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
@@ -114,6 +113,7 @@ export function addWireAddon(node, state, logger) {
114
113
  let contentBucket;
115
114
  let contentGrants;
116
115
  let globalContent;
116
+ let contributors;
117
117
  for (const prop of firstArg.properties) {
118
118
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
119
119
  continue;
@@ -217,6 +217,14 @@ export function addWireAddon(node, state, logger) {
217
217
  ts.isObjectLiteralExpression(prop.initializer)) {
218
218
  contentGrants = parseStringRecord(prop.initializer);
219
219
  }
220
+ else if (key === 'contributors') {
221
+ contributors = parseStringArray(prop.initializer);
222
+ // An unreadable list read as "none" would silently drop services the
223
+ // project asked for.
224
+ if (contributors === undefined) {
225
+ logger.critical(ErrorCode.ADDON_CONTRIBUTORS_NOT_STATIC, `wireAddon's contributors must be an array of string literals, got: ${prop.initializer.getText()}`);
226
+ }
227
+ }
220
228
  else if (key === 'globalContent') {
221
229
  globalContent = ts.isStringLiteral(prop.initializer)
222
230
  ? prop.initializer.text
@@ -248,6 +256,7 @@ export function addWireAddon(node, state, logger) {
248
256
  contentBucket,
249
257
  contentGrants,
250
258
  globalContent,
259
+ ...(contributors ? { contributors } : {}),
251
260
  });
252
261
  state.rpc.usedAddons.add(name);
253
262
  state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
@@ -52,6 +52,11 @@ export declare enum ErrorCode {
52
52
  STUDIO_SCREEN_PATH_DUPLICATE = "PKU357",
53
53
  STUDIO_MESSAGE_KEY_MISSING = "PKU358",
54
54
  STUDIO_BADGE_FUNCTION_NOT_FOUND = "PKU359",
55
+ CONTRIBUTORS_NOT_STATIC = "PKU360",
56
+ CONTRIBUTOR_NAME_DUPLICATED = "PKU361",
57
+ ADDON_CONTRIBUTORS_NOT_STATIC = "PKU362",
58
+ ADDON_CONTRIBUTOR_UNKNOWN = "PKU363",
59
+ ADDON_LEGACY_CONTRIBUTORS = "PKU364",
55
60
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
56
61
  DB_JSON_COLUMN_UNTYPED = "PKU481",
57
62
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -55,6 +55,11 @@ export var ErrorCode;
55
55
  ErrorCode["STUDIO_SCREEN_PATH_DUPLICATE"] = "PKU357";
56
56
  ErrorCode["STUDIO_MESSAGE_KEY_MISSING"] = "PKU358";
57
57
  ErrorCode["STUDIO_BADGE_FUNCTION_NOT_FOUND"] = "PKU359";
58
+ ErrorCode["CONTRIBUTORS_NOT_STATIC"] = "PKU360";
59
+ ErrorCode["CONTRIBUTOR_NAME_DUPLICATED"] = "PKU361";
60
+ ErrorCode["ADDON_CONTRIBUTORS_NOT_STATIC"] = "PKU362";
61
+ ErrorCode["ADDON_CONTRIBUTOR_UNKNOWN"] = "PKU363";
62
+ ErrorCode["ADDON_LEGACY_CONTRIBUTORS"] = "PKU364";
58
63
  // Database schema codegen warnings
59
64
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
60
65
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { inspect, analyzeProgram } from './inspector.js';
2
2
  export { getInitialInspectorState } from './initial-state.js';
3
3
  export { finalizeInspectorState } from './finalize.js';
4
4
  export type { TypesMap } from './types-map.js';
5
- export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, StudioMeta, StudioScreenMeta, StudioNavMeta, AddonContributorRef, AddonContributorsMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorServiceTypes, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
5
+ export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, StudioMeta, StudioScreenMeta, StudioNavMeta, ContributorRef, ContributorVariantsMeta, ContributorsManifestMeta, OptedInContributorsMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorServiceTypes, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
6
6
  export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
7
7
  export { ErrorCode } from './error-codes.js';
8
8
  export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
package/dist/types.d.ts CHANGED
@@ -531,14 +531,14 @@ export type CredentialOverrideMeta = string | {
531
531
  name?: string;
532
532
  mode?: 'singleton' | 'wire';
533
533
  };
534
- /** A screen's nav entry as `pikkuStudio` declares it, read statically. */
534
+ /** A screen's nav entry as `definePlugin` declares it, read statically. */
535
535
  export type StudioNavMeta = {
536
536
  area: 'rail' | 'team' | 'project' | 'account';
537
537
  icon: string;
538
538
  order: number;
539
539
  badge?: string;
540
540
  };
541
- /** A screen of an addon as `pikkuStudio` declares it, read statically. */
541
+ /** A screen of an addon as `definePlugin` declares it, read statically. */
542
542
  export type StudioScreenMeta = {
543
543
  path: string;
544
544
  title: string;
@@ -557,17 +557,40 @@ export type StudioScreenMeta = {
557
557
  });
558
558
  export type StudioMeta = {
559
559
  title: string;
560
- /** The source file whose `pikkuStudio` call declared this manifest. */
560
+ /** The source file whose `definePlugin` call declared this manifest. */
561
561
  file: string;
562
562
  screens: StudioScreenMeta[];
563
563
  /**
564
564
  * The addon's role: every scope its own functions require, derived by
565
- * `pikku all` and never declared. Absent from the raw `pikkuStudio` read.
565
+ * `pikku all` and never declared. Absent from the raw `definePlugin` read.
566
566
  * On a wired instance it also carries the host's `wireAddon` scopes, which
567
567
  * the runner demands of every function in the package.
568
568
  */
569
569
  scopes?: string[];
570
570
  };
571
+ /**
572
+ * Where a contributor variant lives, so a host can import only that variant.
573
+ * `module` is the source file that declares it (absolute while it is in
574
+ * inspector state, relative to the addon package in the published meta);
575
+ * `export` is the name that module exports it under.
576
+ */
577
+ export type ContributorRef = {
578
+ module: string;
579
+ export: string;
580
+ };
581
+ /** One `defineContributors` entry: each variant optional, each its own import. */
582
+ export type ContributorVariantsMeta = {
583
+ dev?: ContributorRef;
584
+ deploy?: ContributorRef;
585
+ };
586
+ /** The contributors an addon registers, keyed by name. */
587
+ export type ContributorsManifestMeta = Record<string, ContributorVariantsMeta>;
588
+ /** The contributors a project opted in to for one wired addon. */
589
+ export type OptedInContributorsMeta = {
590
+ packageName: string;
591
+ /** Each opted-in name with the variants the addon recorded; `module` is relative to the addon package. */
592
+ contributors: ContributorsManifestMeta;
593
+ };
571
594
  export interface InspectorServiceTypes {
572
595
  /** Property names of the project's SingletonServices type, sorted. */
573
596
  singletonServices: string[] | null;
@@ -576,26 +599,15 @@ export interface InspectorServiceTypes {
576
599
  /** Singleton services that are a `SecretService` under another name. */
577
600
  secretAliases: string[];
578
601
  }
579
- export interface AddonContributorRef {
580
- module: string;
581
- export?: string;
582
- }
583
- export interface AddonContributorsMeta {
584
- packageName: string;
585
- packageDir: string;
586
- contributors: AddonContributorRef[];
587
- }
588
602
  export interface InspectorState {
589
- /** Set when this package calls `pikkuStudio`: the screens it ships. */
603
+ /** Set when this package calls `definePlugin`: the screens it ships. */
590
604
  studio?: StudioMeta;
591
- /** The `pikkuStudio` metadata of the addons this project wires with `ui: true`, by namespace. */
605
+ /** The `definePlugin` metadata of the addons this project wires with `ui: true`, by namespace. */
592
606
  addonStudio?: Record<string, StudioMeta>;
593
- /**
594
- * Platform service contributors each wired, non-remote addon declares in its
595
- * `package.json` under `pikku.contributors`, by namespace. `module` resolves
596
- * from `packageDir`.
597
- */
598
- addonContributors?: Record<string, AddonContributorsMeta>;
607
+ /** Set when this package calls `defineContributors`: the contributors it registers, by name. */
608
+ contributorsManifest?: ContributorsManifestMeta;
609
+ /** Per wired addon namespace, the contributors `wireAddon({ contributors })` opted in to. */
610
+ optedInContributors?: Record<string, OptedInContributorsMeta>;
599
611
  rootDir: string;
600
612
  singletonServicesTypeImportMap: PathToNameAndType;
601
613
  wireServicesTypeImportMap: PathToNameAndType;
@@ -793,6 +805,8 @@ export interface InspectorState {
793
805
  * `ContentService` instead of one scoped to its own folder.
794
806
  */
795
807
  globalContent?: string;
808
+ /** The addon contributor names this project opted in to, as `wireAddon({ contributors })` listed them. */
809
+ contributors?: string[];
796
810
  }>;
797
811
  wireAddonFiles: Set<string>;
798
812
  };
@@ -1,4 +1,4 @@
1
- import type { AddonContributorsMeta, InspectorState, InspectorLogger } from '../types.js';
1
+ import type { InspectorState, InspectorLogger } from '../types.js';
2
2
  import type { WebhookSourcesMeta } from '@pikku/core/trigger';
3
3
  export declare const describeMissingAddonMeta: (namespace: string, packageName: string, dirs: string[], declFile?: string) => string;
4
4
  /**
@@ -8,13 +8,6 @@ export declare const describeMissingAddonMeta: (namespace: string, packageName:
8
8
  * Every source starts off: the app turns one on at runtime.
9
9
  */
10
10
  export declare const namespaceAddonWebhookSources: (sources: WebhookSourcesMeta, namespace: string) => WebhookSourcesMeta;
11
- /**
12
- * The platform service contributors an addon declares in its package.json,
13
- * `"pikku": { "contributors": [{ "module", "export" }] }` — the same shape as
14
- * `dev.contributors`. `wireAddon` has no place for them: a contributor emits
15
- * code into the host's entry, so it belongs to the package, not the wiring.
16
- */
17
- export declare const readAddonContributors: (dirs: string[], packageName: string) => Promise<AddonContributorsMeta | null>;
18
11
  export declare function loadAddonFunctionsMeta(logger: InspectorLogger, state: InspectorState): Promise<void>;
19
12
  /**
20
13
  * Load addon schemas into state.schemas. Called after generateAllSchemas
@@ -172,41 +172,62 @@ const loadStudioMeta = async (require, packageName) => {
172
172
  return null;
173
173
  return JSON.parse(await readFile(path, 'utf-8'));
174
174
  };
175
- const isContributorRef = (value) => !!value &&
176
- typeof value === 'object' &&
177
- typeof value.module === 'string' &&
178
- (value.export === undefined ||
179
- typeof value.export === 'string');
175
+ const loadContributorsManifest = async (require, packageName) => {
176
+ const path = resolveAddonMeta(require, packageName, 'contributors/pikku-contributors-meta');
177
+ if (!path)
178
+ return {};
179
+ const meta = JSON.parse(await readFile(path, 'utf-8'));
180
+ return (meta?.contributors ?? {});
181
+ };
180
182
  /**
181
- * The platform service contributors an addon declares in its package.json,
182
- * `"pikku": { "contributors": [{ "module", "export" }] }` — the same shape as
183
- * `dev.contributors`. `wireAddon` has no place for them: a contributor emits
184
- * code into the host's entry, so it belongs to the package, not the wiring.
183
+ * Resolves `wireAddon({ contributors })` against what the addon registered.
184
+ * A name the addon does not register is an error rather than a skip: the
185
+ * project asked for a service and would otherwise build without it.
185
186
  */
186
- export const readAddonContributors = async (dirs, packageName) => {
187
- const packageDir = findInstalledPackageDir(dirs, packageName);
188
- if (!packageDir)
189
- return null;
190
- let declared;
187
+ const recordOptedInContributors = async (logger, state, require, namespace, packageName, names) => {
188
+ let registered;
191
189
  try {
192
- const pkg = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf-8'));
193
- declared = pkg?.pikku?.contributors;
190
+ registered = await loadContributorsManifest(require, packageName);
194
191
  }
195
- catch {
196
- return null;
192
+ catch (e) {
193
+ logger.critical(ErrorCode.ADDON_CONTRIBUTOR_UNKNOWN, `wireAddon('${namespace}') opts in to contributors, but the contributors manifest of ${packageName} could not be read: ${e instanceof Error ? e.message : e}`);
194
+ return;
197
195
  }
198
- if (declared === undefined)
199
- return null;
200
- if (!Array.isArray(declared) || !declared.every(isContributorRef)) {
201
- throw new Error(`${packageName} declares pikku.contributors in its package.json, but it is not a list of { module, export? }.`);
196
+ const selected = {};
197
+ for (const name of names) {
198
+ if (!Object.hasOwn(registered, name)) {
199
+ const known = Object.keys(registered);
200
+ logger.critical(ErrorCode.ADDON_CONTRIBUTOR_UNKNOWN, `wireAddon('${namespace}') opts in to contributor '${name}', but ${packageName} registers no such contributor. ${known.length > 0
201
+ ? `It registers: ${known.join(', ')}.`
202
+ : 'It registers none; add a defineContributors call to the package and run pikku all in it.'}`);
203
+ continue;
204
+ }
205
+ selected[name] = registered[name];
206
+ }
207
+ if (Object.keys(selected).length > 0) {
208
+ ;
209
+ (state.optedInContributors ??= {})[namespace] = {
210
+ packageName,
211
+ contributors: selected,
212
+ };
213
+ }
214
+ };
215
+ /**
216
+ * `pikku.contributors` in an addon's package.json was the string-emitting
217
+ * contributor route. It is no longer read, so it is reported rather than left
218
+ * to stop applying silently.
219
+ */
220
+ const hasLegacyContributors = async (dirs, packageName) => {
221
+ const dir = findInstalledPackageDir(dirs, packageName);
222
+ if (!dir)
223
+ return false;
224
+ try {
225
+ const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
226
+ return pkg?.pikku?.contributors !== undefined;
227
+ }
228
+ catch {
229
+ return false;
202
230
  }
203
- if (declared.length === 0)
204
- return null;
205
- return {
206
- packageName,
207
- packageDir,
208
- contributors: declared.map(({ module, export: name }) => name === undefined ? { module } : { module, export: name }),
209
- };
210
231
  };
211
232
  /**
212
233
  * A versioned remote addon's function metadata comes from the SDK `pikku addon
@@ -275,17 +296,13 @@ export async function loadAddonFunctionsMeta(logger, state) {
275
296
  const dirs = addonResolutionDirs(state.rootDir, decl.file);
276
297
  const require = createAddonResolver(dirs);
277
298
  try {
278
- try {
279
- const contributors = await readAddonContributors(dirs, decl.package);
280
- if (contributors) {
281
- ;
282
- (state.addonContributors ??= {})[namespace] = contributors;
283
- }
299
+ recordDeclaredUses(logger, namespace, decl, dirs);
300
+ if (decl.contributors && decl.contributors.length > 0) {
301
+ await recordOptedInContributors(logger, state, require, namespace, decl.package, decl.contributors);
284
302
  }
285
- catch (error) {
286
- logger.error(`wireAddon('${namespace}'): ${error instanceof Error ? error.message : error}`);
303
+ if (await hasLegacyContributors(dirs, decl.package)) {
304
+ logger.critical(ErrorCode.ADDON_LEGACY_CONTRIBUTORS, `${decl.package} still declares "pikku.contributors" in its package.json, which is no longer read. Remove it, register the contributors with defineContributors in the addon's source, and opt in with wireAddon('${namespace}', { contributors: [...] }). See docs/contributors.md.`);
287
305
  }
288
- recordDeclaredUses(logger, namespace, decl, dirs);
289
306
  if (decl.ui) {
290
307
  let manifest;
291
308
  let unreadable = false;
@@ -300,7 +317,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
300
317
  // reported above; the addon's functions and scopes still load
301
318
  }
302
319
  else if (!manifest || manifest.screens.length === 0) {
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.`);
320
+ logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but ${decl.package} declares no screens. Add a definePlugin call to the package, or drop ui.`);
304
321
  }
305
322
  else {
306
323
  ;
@@ -212,6 +212,7 @@ export interface SerializableInspectorState {
212
212
  contentBucket?: string;
213
213
  contentGrants?: Record<string, string>;
214
214
  globalContent?: string;
215
+ contributors?: string[];
215
216
  }
216
217
  ]>;
217
218
  wireAddonFiles: string[];
@@ -331,7 +332,8 @@ export interface SerializableInspectorState {
331
332
  remoteAddonFunctions?: InspectorState['remoteAddonFunctions'];
332
333
  studio?: InspectorState['studio'];
333
334
  addonStudio?: InspectorState['addonStudio'];
334
- addonContributors?: InspectorState['addonContributors'];
335
+ contributorsManifest?: InspectorState['contributorsManifest'];
336
+ optedInContributors?: InspectorState['optedInContributors'];
335
337
  exportedContracts: InspectorState['exportedContracts'];
336
338
  }
337
339
  /**
@@ -196,7 +196,8 @@ export function serializeInspectorState(state) {
196
196
  remoteAddonFunctions: state.remoteAddonFunctions,
197
197
  studio: state.studio,
198
198
  addonStudio: state.addonStudio,
199
- addonContributors: state.addonContributors,
199
+ contributorsManifest: state.contributorsManifest,
200
+ optedInContributors: state.optedInContributors,
200
201
  exportedContracts: state.exportedContracts,
201
202
  };
202
203
  }
@@ -406,7 +407,8 @@ export function deserializeInspectorState(data) {
406
407
  : {}),
407
408
  studio: data.studio,
408
409
  addonStudio: data.addonStudio,
409
- addonContributors: data.addonContributors,
410
+ contributorsManifest: data.contributorsManifest,
411
+ optedInContributors: data.optedInContributors,
410
412
  exportedContracts: data.exportedContracts || {
411
413
  http: {},
412
414
  cli: {},
package/dist/visit.js CHANGED
@@ -16,6 +16,7 @@ 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
18
  import { addPikkuStudio } from './add/add-pikku-studio.js';
19
+ import { addDefineContributors } from './add/add-define-contributors.js';
19
20
  import { addWireRemoteAddon } from './add/add-wire-remote-addon.js';
20
21
  import { addMiddleware } from './add/add-middleware.js';
21
22
  import { addPermission } from './add/add-permission.js';
@@ -52,6 +53,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
52
53
  addRPCInvocations(node, state, logger);
53
54
  addWireAddon(node, state, logger);
54
55
  addPikkuStudio(node, state, logger);
56
+ addDefineContributors(logger, node, checker, state);
55
57
  addWireRemoteAddon(node, state, logger);
56
58
  addMiddleware(logger, node, checker, state, options);
57
59
  addPermission(logger, node, checker, state, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.111",
3
+ "version": "0.12.112",
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.147"
73
+ "@pikku/core": "^0.12.148"
74
74
  },
75
75
  "devDependencies": {
76
- "@pikku/core": "^0.12.147",
76
+ "@pikku/core": "^0.12.148",
77
77
  "@types/node": "^26.6.5"
78
78
  },
79
79
  "engines": {