@pikku/core 0.12.147 → 0.12.148
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/contributors/contributors.types.d.ts +77 -0
- package/dist/contributors/contributors.types.js +1 -0
- package/dist/contributors/define-contributors.d.ts +7 -0
- package/dist/contributors/define-contributors.js +6 -0
- package/dist/contributors/index.d.ts +4 -0
- package/dist/contributors/index.js +2 -0
- package/dist/contributors/resolve-contributors.d.ts +51 -0
- package/dist/contributors/resolve-contributors.js +158 -0
- package/dist/lifecycle.d.ts +19 -0
- package/dist/lifecycle.js +54 -0
- package/dist/wirings/addon/addon-runner.js +11 -0
- package/dist/wirings/addon/{pikku-studio.d.ts → define-plugin.d.ts} +4 -2
- package/dist/wirings/addon/{pikku-studio.js → define-plugin.js} +3 -1
- package/dist/wirings/addon/index.d.ts +2 -2
- package/dist/wirings/addon/index.js +1 -1
- package/dist/wirings/addon/wire-addon.d.ts +7 -1
- package/package.json +3 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a target gets a contributor's bindings from. `env` is plain
|
|
3
|
+
* environment variables, which every target has; `cloudflare` is Cloudflare
|
|
4
|
+
* worker bindings, which only the Cloudflare deploy provides.
|
|
5
|
+
*/
|
|
6
|
+
export type ContributorBindingSource = 'env' | 'cloudflare';
|
|
7
|
+
/** The database the generated app uses, as contributors see it at build time. */
|
|
8
|
+
export type ContributorDb = {
|
|
9
|
+
/** The SQL engine the app's database runs on. MySQL is not represented. */
|
|
10
|
+
engine: 'sqlite' | 'postgres';
|
|
11
|
+
/** Column coercion map from `pikku db generate`, keyed by table then column. */
|
|
12
|
+
coercionMap?: Record<string, Record<string, 'date' | 'bool' | 'json'>>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build-time facts about the app being generated. Handed to `when` while the
|
|
16
|
+
* generator selects contributors and, as plain data, to `run`.
|
|
17
|
+
*/
|
|
18
|
+
export interface ContributorPlatform {
|
|
19
|
+
/** Service names the app (dev) or the deploy unit requires. */
|
|
20
|
+
serviceNames: string[];
|
|
21
|
+
/** True when the app uses the queue, so a contributor can provide or skip a queue service. */
|
|
22
|
+
needsQueue: boolean;
|
|
23
|
+
/** True when the app uses workflows, so a contributor can provide or skip workflow services. */
|
|
24
|
+
needsWorkflow: boolean;
|
|
25
|
+
/** True when the app uses AI agents, so a contributor can provide or skip agent services. */
|
|
26
|
+
needsAgent: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Baseline services the generated entry already holds before any
|
|
29
|
+
* contributor runs. They satisfy a contributor's `needs`, are read as
|
|
30
|
+
* `runtime.<name>`, and a contributor may provide the same key to replace
|
|
31
|
+
* them. Undefined means none.
|
|
32
|
+
*/
|
|
33
|
+
runtimeServices?: string[];
|
|
34
|
+
/** The app's database, when it uses SQLite or Postgres. Unset otherwise. */
|
|
35
|
+
db?: ContributorDb;
|
|
36
|
+
}
|
|
37
|
+
export interface Contributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
38
|
+
/**
|
|
39
|
+
* Service keys `run` sets on `services`. May be empty for a contributor that
|
|
40
|
+
* only decorates services another contributor provides.
|
|
41
|
+
*/
|
|
42
|
+
provides?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Service keys this contributor reads, from `runtime` or from `services`
|
|
45
|
+
* set by another contributor. A dotted key (`logSources.stage`) is met by a
|
|
46
|
+
* contributor providing it exactly or providing a prefix of it
|
|
47
|
+
* (`logSources`). Contributors that provide a need run first.
|
|
48
|
+
*/
|
|
49
|
+
needs?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Binding sources the target must be able to supply for this contributor to
|
|
52
|
+
* apply. Defaults to `['env']`. A contributor the project opted in to whose
|
|
53
|
+
* `requires` the target cannot meet is a build error.
|
|
54
|
+
*/
|
|
55
|
+
requires?: ContributorBindingSource[];
|
|
56
|
+
/** Build-time only. A contributor returning false is left out entirely. */
|
|
57
|
+
when?(platform: TPlatform): boolean;
|
|
58
|
+
/** Constructs services. Runtime `env` guards belong here, not in `when`. */
|
|
59
|
+
run(services: TServices, env: Record<string, string | undefined>, runtime: Record<string, any>, platform: TPlatform): void | Promise<void>;
|
|
60
|
+
/** Runtime setup, called where the server lifecycle runs. */
|
|
61
|
+
onServerStart?(services: TServices): void | Promise<void>;
|
|
62
|
+
/** Teardown, called in reverse `needs` order where the server lifecycle runs. */
|
|
63
|
+
onServerStop?(services: TServices): void | Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** The per-target forms of one named contributor: a `dev` variant and a `deploy` variant. */
|
|
66
|
+
export interface ContributorVariants<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
67
|
+
/** The contributor `pikku dev` applies. Omit it to leave this contributor out of dev. */
|
|
68
|
+
dev?: Contributor<TServices, TPlatform>;
|
|
69
|
+
/** The contributor a deploy build applies. Omit it to leave this contributor out of deploys. */
|
|
70
|
+
deploy?: Contributor<TServices, TPlatform>;
|
|
71
|
+
}
|
|
72
|
+
/** The build a contributor is selected for: `pikku dev`, or a deploy. */
|
|
73
|
+
export type ContributorTarget = 'dev' | 'deploy';
|
|
74
|
+
export interface NamedContributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
75
|
+
name: string;
|
|
76
|
+
contributor: Contributor<TServices, TPlatform>;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContributorPlatform, ContributorVariants } from './contributors.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registers an addon's contributors by name. A project opts in to a name with
|
|
4
|
+
* `wireAddon({ contributors: ['name'] })`; only the selected variant for the
|
|
5
|
+
* build target is bundled.
|
|
6
|
+
*/
|
|
7
|
+
export declare const defineContributors: <const T extends Record<string, ContributorVariants<any, ContributorPlatform>>>(contributors: T) => T;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineContributors } from './define-contributors.js';
|
|
2
|
+
export { ContributorDependencyError, DEFAULT_CONTRIBUTOR_BINDING_SOURCES, orderContributors, resolveContributors, selectContributors, } from './resolve-contributors.js';
|
|
3
|
+
export type { SkippedContributor } from './resolve-contributors.js';
|
|
4
|
+
export type { Contributor, ContributorBindingSource, ContributorDb, ContributorPlatform, ContributorTarget, ContributorVariants, NamedContributor, } from './contributors.types.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ContributorBindingSource, ContributorPlatform, NamedContributor } from './contributors.types.js';
|
|
2
|
+
export declare class ContributorDependencyError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const DEFAULT_CONTRIBUTOR_BINDING_SOURCES: readonly ContributorBindingSource[];
|
|
6
|
+
export interface SkippedContributor {
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* `when` returned false; the target cannot supply its `requires` (checked
|
|
10
|
+
* only once `when` passed, so this one is a contributor the build wanted);
|
|
11
|
+
* or it was `optional` and nothing meets a need of it.
|
|
12
|
+
*/
|
|
13
|
+
reason: 'when' | 'requires' | 'needs';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Drops contributors whose `when` is false or whose `requires` the target
|
|
17
|
+
* cannot supply. The clash and ordering checks run on what is left, so an
|
|
18
|
+
* exclusive pair of `when`s never clashes.
|
|
19
|
+
*/
|
|
20
|
+
export declare const selectContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
|
|
21
|
+
platform: ContributorPlatform;
|
|
22
|
+
supported?: readonly ContributorBindingSource[];
|
|
23
|
+
}) => {
|
|
24
|
+
selected: T[];
|
|
25
|
+
skipped: SkippedContributor[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Orders contributors so each runs after every contributor providing one of
|
|
29
|
+
* its `needs`. Contributors without needs keep their input order.
|
|
30
|
+
*
|
|
31
|
+
* Throws a `ContributorDependencyError` for a repeated name, a need nothing
|
|
32
|
+
* provides, a key two contributors both provide, or a cycle. A key listed in
|
|
33
|
+
* `options.runtime` is a baseline service: it meets a need and a contributor
|
|
34
|
+
* may provide it too, replacing it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const orderContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options?: {
|
|
37
|
+
runtime?: readonly string[];
|
|
38
|
+
}) => T[];
|
|
39
|
+
/**
|
|
40
|
+
* Filters by `when`/`requires`, then checks clashes and orders what is left.
|
|
41
|
+
* Names in `optional` are dropped, rather than failing the build, when a need
|
|
42
|
+
* of theirs is met by nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare const resolveContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
|
|
45
|
+
platform: ContributorPlatform;
|
|
46
|
+
supported?: readonly ContributorBindingSource[];
|
|
47
|
+
optional?: ReadonlySet<string>;
|
|
48
|
+
}) => {
|
|
49
|
+
ordered: T[];
|
|
50
|
+
skipped: SkippedContributor[];
|
|
51
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export class ContributorDependencyError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ContributorDependencyError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export const DEFAULT_CONTRIBUTOR_BINDING_SOURCES = ['env'];
|
|
8
|
+
const satisfies = (provided, need) => provided === need || need.startsWith(`${provided}.`);
|
|
9
|
+
/**
|
|
10
|
+
* Drops contributors whose `when` is false or whose `requires` the target
|
|
11
|
+
* cannot supply. The clash and ordering checks run on what is left, so an
|
|
12
|
+
* exclusive pair of `when`s never clashes.
|
|
13
|
+
*/
|
|
14
|
+
export const selectContributors = (contributors, options) => {
|
|
15
|
+
const supported = options.supported ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
|
|
16
|
+
const selected = [];
|
|
17
|
+
const skipped = [];
|
|
18
|
+
for (const entry of contributors) {
|
|
19
|
+
const requires = entry.contributor.requires ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
|
|
20
|
+
// `when` first: a contributor that is not meant for this build is never
|
|
21
|
+
// reported as one the target failed to support.
|
|
22
|
+
if (entry.contributor.when?.(options.platform) === false) {
|
|
23
|
+
skipped.push({ name: entry.name, reason: 'when' });
|
|
24
|
+
}
|
|
25
|
+
else if (!requires.every((source) => supported.includes(source))) {
|
|
26
|
+
skipped.push({ name: entry.name, reason: 'requires' });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
selected.push(entry);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { selected, skipped };
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Orders contributors so each runs after every contributor providing one of
|
|
36
|
+
* its `needs`. Contributors without needs keep their input order.
|
|
37
|
+
*
|
|
38
|
+
* Throws a `ContributorDependencyError` for a repeated name, a need nothing
|
|
39
|
+
* provides, a key two contributors both provide, or a cycle. A key listed in
|
|
40
|
+
* `options.runtime` is a baseline service: it meets a need and a contributor
|
|
41
|
+
* may provide it too, replacing it.
|
|
42
|
+
*/
|
|
43
|
+
export const orderContributors = (contributors, options = {}) => {
|
|
44
|
+
const runtime = options.runtime ?? [];
|
|
45
|
+
const names = new Set();
|
|
46
|
+
for (const { name } of contributors) {
|
|
47
|
+
if (names.has(name)) {
|
|
48
|
+
throw new ContributorDependencyError(`Contributor '${name}' is selected more than once.`);
|
|
49
|
+
}
|
|
50
|
+
names.add(name);
|
|
51
|
+
}
|
|
52
|
+
const owners = new Map();
|
|
53
|
+
for (const entry of contributors) {
|
|
54
|
+
for (const key of entry.contributor.provides ?? []) {
|
|
55
|
+
const owner = owners.get(key);
|
|
56
|
+
if (owner && owner !== entry) {
|
|
57
|
+
throw new ContributorDependencyError(`Contributors '${owner.name}' and '${entry.name}' both provide '${key}'. Each service key has one provider.`);
|
|
58
|
+
}
|
|
59
|
+
owners.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const dependencies = new Map();
|
|
63
|
+
for (const entry of contributors) {
|
|
64
|
+
const deps = new Set();
|
|
65
|
+
// A contributor that provides what it needs (`provides: ['x'], needs: ['x']`,
|
|
66
|
+
// even for a baseline x) depends on itself, which is a cycle; a decorator
|
|
67
|
+
// declares empty `provides` and reads x from `services`/`runtime` instead.
|
|
68
|
+
for (const need of entry.contributor.needs ?? []) {
|
|
69
|
+
const providers = contributors.filter((other) => (other.contributor.provides ?? []).some((key) => satisfies(key, need)));
|
|
70
|
+
const fromRuntime = runtime.some((key) => satisfies(key, need));
|
|
71
|
+
if (providers.length === 0 && !fromRuntime) {
|
|
72
|
+
const near = contributors
|
|
73
|
+
.filter((other) => (other.contributor.provides ?? []).some((key) => key.startsWith(`${need}.`)))
|
|
74
|
+
.map((other) => other.name);
|
|
75
|
+
const hint = near.length > 0
|
|
76
|
+
? ` ${near.join(', ')} provide${near.length === 1 ? 's' : ''} keys under it, but not '${need}' itself.`
|
|
77
|
+
: runtime.length > 0
|
|
78
|
+
? ` No contributor provides it, and the runtime only has: ${runtime.join(', ')}.`
|
|
79
|
+
: ` No contributor provides it, and this runtime provides none.`;
|
|
80
|
+
throw new ContributorDependencyError(`Contributor '${entry.name}' needs '${need}', which nothing provides.${hint}`);
|
|
81
|
+
}
|
|
82
|
+
for (const provider of providers)
|
|
83
|
+
deps.add(provider);
|
|
84
|
+
}
|
|
85
|
+
dependencies.set(entry, [...deps]);
|
|
86
|
+
}
|
|
87
|
+
const ordered = [];
|
|
88
|
+
const placed = new Set();
|
|
89
|
+
while (ordered.length < contributors.length) {
|
|
90
|
+
const next = contributors.find((entry) => !placed.has(entry) &&
|
|
91
|
+
dependencies.get(entry).every((dep) => placed.has(dep)));
|
|
92
|
+
if (!next) {
|
|
93
|
+
throw new ContributorDependencyError(`Contributors depend on each other in a cycle: ${findCycle(contributors.filter((c) => !placed.has(c)), dependencies)
|
|
94
|
+
.map((c) => c.name)
|
|
95
|
+
.join(' -> ')}`);
|
|
96
|
+
}
|
|
97
|
+
placed.add(next);
|
|
98
|
+
ordered.push(next);
|
|
99
|
+
}
|
|
100
|
+
return ordered;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Drops, repeatedly, the `optional` contributors whose `needs` nothing left
|
|
104
|
+
* provides, so one that only decorates a service (a wrapper around
|
|
105
|
+
* `services.kysely`) applies when the service exists and is not an error when
|
|
106
|
+
* it does not.
|
|
107
|
+
*/
|
|
108
|
+
const dropUnmetOptional = (selected, optional, runtime) => {
|
|
109
|
+
let kept = [...selected];
|
|
110
|
+
const dropped = [];
|
|
111
|
+
for (;;) {
|
|
112
|
+
const unmet = kept.find((entry) => optional.has(entry.name) &&
|
|
113
|
+
(entry.contributor.needs ?? []).some((need) => !runtime.some((key) => satisfies(key, need)) &&
|
|
114
|
+
!kept.some((other) => other !== entry &&
|
|
115
|
+
(other.contributor.provides ?? []).some((key) => satisfies(key, need)))));
|
|
116
|
+
if (!unmet)
|
|
117
|
+
return { kept, dropped };
|
|
118
|
+
kept = kept.filter((entry) => entry !== unmet);
|
|
119
|
+
dropped.push({ name: unmet.name, reason: 'needs' });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Filters by `when`/`requires`, then checks clashes and orders what is left.
|
|
124
|
+
* Names in `optional` are dropped, rather than failing the build, when a need
|
|
125
|
+
* of theirs is met by nothing.
|
|
126
|
+
*/
|
|
127
|
+
export const resolveContributors = (contributors, options) => {
|
|
128
|
+
const { selected, skipped } = selectContributors(contributors, options);
|
|
129
|
+
const { kept, dropped } = dropUnmetOptional(selected, options.optional ?? new Set(), options.platform.runtimeServices ?? []);
|
|
130
|
+
return {
|
|
131
|
+
ordered: orderContributors(kept, {
|
|
132
|
+
runtime: options.platform.runtimeServices,
|
|
133
|
+
}),
|
|
134
|
+
skipped: [...skipped, ...dropped],
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const findCycle = (remaining, dependencies) => {
|
|
138
|
+
const pending = new Set(remaining);
|
|
139
|
+
const visit = (node, path) => {
|
|
140
|
+
const at = path.indexOf(node);
|
|
141
|
+
if (at >= 0)
|
|
142
|
+
return [...path.slice(at), node];
|
|
143
|
+
for (const dep of dependencies.get(node) ?? []) {
|
|
144
|
+
if (!pending.has(dep))
|
|
145
|
+
continue;
|
|
146
|
+
const cycle = visit(dep, [...path, node]);
|
|
147
|
+
if (cycle)
|
|
148
|
+
return cycle;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
};
|
|
152
|
+
for (const node of remaining) {
|
|
153
|
+
const cycle = visit(node, []);
|
|
154
|
+
if (cycle)
|
|
155
|
+
return cycle;
|
|
156
|
+
}
|
|
157
|
+
return remaining;
|
|
158
|
+
};
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -15,3 +15,22 @@ export declare const pikkuServerLifecycle: <SS extends CoreSingletonServices = C
|
|
|
15
15
|
* @returns A function that removes the handlers.
|
|
16
16
|
*/
|
|
17
17
|
export declare const onShutdownSignals: (run: (signal: NodeJS.Signals) => void | Promise<void>, signals?: NodeJS.Signals[]) => (() => void);
|
|
18
|
+
/** A contributor's `onServerStart` / `onServerStop`, as the generated platform-services file lists it. */
|
|
19
|
+
export interface ContributorServerHook {
|
|
20
|
+
name: string;
|
|
21
|
+
run: (services: any) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Composes the server lifecycle around the app's own: contributor start hooks
|
|
25
|
+
* run first, in order, then the app's `beforeStart`; the app's `afterStart` and
|
|
26
|
+
* `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
|
|
27
|
+
* (already in reverse run order, as the generated file lists them).
|
|
28
|
+
*
|
|
29
|
+
* A start hook that throws aborts startup with an error naming the contributor.
|
|
30
|
+
* Every stop hook is attempted even if an earlier one (or the app's
|
|
31
|
+
* `afterStop`) threw; the failures are then thrown together, each named.
|
|
32
|
+
*/
|
|
33
|
+
export declare const composeServerLifecycle: <SS extends CoreSingletonServices = CoreSingletonServices>(app: ServerLifecycle<SS> | undefined, hooks: {
|
|
34
|
+
start?: readonly ContributorServerHook[];
|
|
35
|
+
stop?: readonly ContributorServerHook[];
|
|
36
|
+
}) => ServerLifecycle<SS>;
|
package/dist/lifecycle.js
CHANGED
|
@@ -67,3 +67,57 @@ export const onShutdownSignals = (run, signals = ['SIGINT', 'SIGTERM']) => {
|
|
|
67
67
|
proc.off(signal, handler);
|
|
68
68
|
};
|
|
69
69
|
};
|
|
70
|
+
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
71
|
+
/**
|
|
72
|
+
* Composes the server lifecycle around the app's own: contributor start hooks
|
|
73
|
+
* run first, in order, then the app's `beforeStart`; the app's `afterStart` and
|
|
74
|
+
* `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
|
|
75
|
+
* (already in reverse run order, as the generated file lists them).
|
|
76
|
+
*
|
|
77
|
+
* A start hook that throws aborts startup with an error naming the contributor.
|
|
78
|
+
* Every stop hook is attempted even if an earlier one (or the app's
|
|
79
|
+
* `afterStop`) threw; the failures are then thrown together, each named.
|
|
80
|
+
*/
|
|
81
|
+
export const composeServerLifecycle = (app, hooks) => ({
|
|
82
|
+
beforeStart: async (services) => {
|
|
83
|
+
for (const hook of hooks.start ?? []) {
|
|
84
|
+
try {
|
|
85
|
+
await hook.run(services);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`Contributor '${hook.name}' failed in onServerStart: ${describeError(error)}`, { cause: error });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await app?.beforeStart?.(services);
|
|
92
|
+
},
|
|
93
|
+
afterStart: (services) => app?.afterStart?.(services),
|
|
94
|
+
beforeStop: (services) => app?.beforeStop?.(services),
|
|
95
|
+
afterStop: async (services) => {
|
|
96
|
+
const failures = [];
|
|
97
|
+
const errors = [];
|
|
98
|
+
let hookFailed = false;
|
|
99
|
+
try {
|
|
100
|
+
await app?.afterStop?.(services);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
failures.push(`the app's afterStop: ${describeError(error)}`);
|
|
104
|
+
errors.push(error);
|
|
105
|
+
}
|
|
106
|
+
for (const hook of hooks.stop ?? []) {
|
|
107
|
+
try {
|
|
108
|
+
await hook.run(services);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
hookFailed = true;
|
|
112
|
+
failures.push(`contributor '${hook.name}' onServerStop: ${describeError(error)}`);
|
|
113
|
+
errors.push(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (errors.length === 1 && failures.length === 1 && !hookFailed) {
|
|
117
|
+
throw errors[0];
|
|
118
|
+
}
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
throw new AggregateError(errors, `Server stop failed: ${failures.join('; ')}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -105,6 +105,13 @@ const scopeWebhookService = (service, namespace) => {
|
|
|
105
105
|
scopedWebhookServices.add(scoped);
|
|
106
106
|
return scoped;
|
|
107
107
|
};
|
|
108
|
+
const authFromAppServices = () => {
|
|
109
|
+
const auth = pikkuState(null, 'package', 'singletonServices')?.auth;
|
|
110
|
+
if (typeof auth !== 'function') {
|
|
111
|
+
throw new Error('auth was called before the app had finished creating its singleton services');
|
|
112
|
+
}
|
|
113
|
+
return auth();
|
|
114
|
+
};
|
|
108
115
|
export const getOrCreatePackageSingletonServices = async (packageName, parentServices, addonInstance) => {
|
|
109
116
|
addonInstance ??= addonInstanceForNamespace(findAddonNamespaceForPackage(packageName) ?? undefined, packageName);
|
|
110
117
|
const cacheKey = addonInstance?.namespace ?? packageName;
|
|
@@ -114,6 +121,10 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
|
|
|
114
121
|
}
|
|
115
122
|
const factories = pikkuState(packageName, 'package', 'factories');
|
|
116
123
|
let existingServices = parentServices;
|
|
124
|
+
if (typeof existingServices.auth !== 'function' &&
|
|
125
|
+
pikkuState(null, 'package', 'authFactory')) {
|
|
126
|
+
existingServices = { ...existingServices, auth: authFromAppServices };
|
|
127
|
+
}
|
|
117
128
|
if (addonInstance?.secretOverrides && parentServices.secrets) {
|
|
118
129
|
existingServices = {
|
|
119
130
|
...existingServices,
|
|
@@ -45,7 +45,7 @@ export type StudioAppScreen = StudioScreenBase & {
|
|
|
45
45
|
};
|
|
46
46
|
/** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
|
|
47
47
|
export type StudioScreen = StudioComponentScreen | StudioAppScreen;
|
|
48
|
-
/** The manifest passed to `
|
|
48
|
+
/** The manifest passed to `definePlugin`: the addon's title, a message key, and its screens. */
|
|
49
49
|
export type StudioManifest = {
|
|
50
50
|
title: string;
|
|
51
51
|
screens: StudioScreen[];
|
|
@@ -56,5 +56,7 @@ export type StudioManifest = {
|
|
|
56
56
|
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
57
57
|
* missing from `messages/en.json` fails the build.
|
|
58
58
|
*/
|
|
59
|
-
export declare const
|
|
59
|
+
export declare const definePlugin: (manifest: StudioManifest) => StudioManifest;
|
|
60
|
+
/** @deprecated Renamed to `definePlugin`. */
|
|
61
|
+
export declare const pikkuStudio: typeof definePlugin;
|
|
60
62
|
export {};
|
|
@@ -4,4 +4,6 @@
|
|
|
4
4
|
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
5
5
|
* missing from `messages/en.json` fails the build.
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
7
|
+
export const definePlugin = (manifest) => manifest;
|
|
8
|
+
/** @deprecated Renamed to `definePlugin`. */
|
|
9
|
+
export const pikkuStudio = definePlugin;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
2
|
export type { WireAddonConfig } from './wire-addon.js';
|
|
3
|
-
export { pikkuStudio } from './
|
|
4
|
-
export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './
|
|
3
|
+
export { definePlugin, pikkuStudio } from './define-plugin.js';
|
|
4
|
+
export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './define-plugin.js';
|
|
5
5
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
6
6
|
export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wire-remote-addon.js';
|
|
7
7
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
|
-
export { pikkuStudio } from './
|
|
2
|
+
export { definePlugin, pikkuStudio } from './define-plugin.js';
|
|
3
3
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
4
4
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -46,7 +46,7 @@ export type WireAddonConfig = {
|
|
|
46
46
|
/** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
47
47
|
tags?: string[];
|
|
48
48
|
/**
|
|
49
|
-
* Mounts the screens the addon ships (see `
|
|
49
|
+
* Mounts the screens the addon ships (see `definePlugin`) in the host, each at
|
|
50
50
|
* the path its `scope` gives it. Without it the addon's screens are ignored.
|
|
51
51
|
* The package must declare at least one, or `pikku all` rejects the wiring.
|
|
52
52
|
*/
|
|
@@ -100,6 +100,12 @@ export type WireAddonConfig = {
|
|
|
100
100
|
contentGrants?: Record<string, 'read' | 'write'>;
|
|
101
101
|
/** Hands over the whole `ContentService` unscoped. The value is the reason, recorded in the deploy manifest. */
|
|
102
102
|
globalContent?: string;
|
|
103
|
+
/**
|
|
104
|
+
* The contributors this project takes from the addon (those it registers with
|
|
105
|
+
* `defineContributors`), by name. None unless listed: an addon never injects
|
|
106
|
+
* services into a project that did not ask for them.
|
|
107
|
+
*/
|
|
108
|
+
contributors?: string[];
|
|
103
109
|
};
|
|
104
110
|
/**
|
|
105
111
|
* Installs an addon into this project: its functions, wirings and scopes become
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.148",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"./state": "./dist/pikku-state.js",
|
|
94
94
|
"./classification": "./dist/classification/index.js",
|
|
95
95
|
"./utils": "./dist/utils.js",
|
|
96
|
+
"./contributors": "./dist/contributors/index.js",
|
|
96
97
|
"./lifecycle": "./dist/lifecycle.js",
|
|
97
98
|
"./schema": "./dist/schema.js",
|
|
98
99
|
"./testing": "./dist/testing/index.js",
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
},
|
|
120
121
|
"devDependencies": {
|
|
121
122
|
"@ag-ui/core": "0.0.59",
|
|
122
|
-
"@pikku/deploy": "^0.12.
|
|
123
|
+
"@pikku/deploy": "^0.12.18",
|
|
123
124
|
"@types/node": "^26.6.5",
|
|
124
125
|
"esbuild": "~0.28.2",
|
|
125
126
|
"tsx": "^4.23.15"
|