gloomberb 0.12.0 → 0.13.2
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/README.md +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneDefinition,
|
|
3
|
+
HeadlessPaneLoadArgs,
|
|
4
|
+
} from "../../../types/plugin";
|
|
5
|
+
import {
|
|
6
|
+
curveAsOf,
|
|
7
|
+
isInverted,
|
|
8
|
+
loadYieldCurve,
|
|
9
|
+
parseYieldPoints,
|
|
10
|
+
spreadBasisPoints,
|
|
11
|
+
type YieldCurveLoader,
|
|
12
|
+
type YieldPoint,
|
|
13
|
+
} from "./treasury-data";
|
|
14
|
+
|
|
15
|
+
const COLUMNS = [
|
|
16
|
+
{ key: "maturity", header: "Maturity" },
|
|
17
|
+
{
|
|
18
|
+
key: "maturityYears",
|
|
19
|
+
header: "Years",
|
|
20
|
+
align: "right" as const,
|
|
21
|
+
format: (value: unknown) => Number(value).toFixed(2).replace(/\.00$/, ""),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "yield",
|
|
25
|
+
header: "Yield",
|
|
26
|
+
align: "right" as const,
|
|
27
|
+
format: (value: unknown) => value == null ? "-" : `${Number(value).toFixed(2)}%`,
|
|
28
|
+
},
|
|
29
|
+
{ key: "asOf", header: "As of" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export interface YieldCurveHeadlessDependencies {
|
|
33
|
+
load(args: HeadlessPaneLoadArgs, loader: YieldCurveLoader): Promise<YieldPoint[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultDependencies: YieldCurveHeadlessDependencies = {
|
|
37
|
+
load: (_args, loader) => loadYieldCurve(loader),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function createYieldCurveHeadless(
|
|
41
|
+
dependencies: YieldCurveHeadlessDependencies = defaultDependencies,
|
|
42
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
43
|
+
return {
|
|
44
|
+
shape: "rows",
|
|
45
|
+
argument: { kind: "none" },
|
|
46
|
+
options: [],
|
|
47
|
+
columns: COLUMNS,
|
|
48
|
+
describe: "US Treasury Yield Curve",
|
|
49
|
+
async load(args, ctx) {
|
|
50
|
+
const points = await dependencies.load(
|
|
51
|
+
args,
|
|
52
|
+
() => ctx.apiClient.getCloudYieldCurve(),
|
|
53
|
+
);
|
|
54
|
+
const rows = parseYieldPoints(points);
|
|
55
|
+
return {
|
|
56
|
+
rows: rows.map((point) => ({ ...point })),
|
|
57
|
+
metadata: {
|
|
58
|
+
asOf: curveAsOf(points),
|
|
59
|
+
inverted: isInverted(points),
|
|
60
|
+
spread2Y10YBasisPoints: spreadBasisPoints(points),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const yieldCurveHeadless = createYieldCurveHeadless();
|
|
@@ -12,11 +12,15 @@ import {
|
|
|
12
12
|
loadYieldCurve,
|
|
13
13
|
parseYieldPoints,
|
|
14
14
|
isInverted,
|
|
15
|
+
spreadBasisPoints,
|
|
15
16
|
TREASURY_MATURITIES,
|
|
16
17
|
type YieldPoint,
|
|
17
18
|
} from "./treasury-data";
|
|
18
19
|
import { usePaneStatusFooter } from "../shared/pane-footer";
|
|
19
20
|
import { useAutoRefresh, useUpdatedAgo } from "../shared/auto-refresh";
|
|
21
|
+
import { yieldCurveHeadless } from "./headless";
|
|
22
|
+
|
|
23
|
+
export { yieldCurveHeadless } from "./headless";
|
|
20
24
|
|
|
21
25
|
function formatYield(y: number | null): string {
|
|
22
26
|
if (y == null) return "—";
|
|
@@ -27,13 +31,6 @@ function formatYieldAxis(value: number): string {
|
|
|
27
31
|
return `${value.toFixed(2)}%`;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
function spreadBp(points: YieldPoint[]): number | null {
|
|
31
|
-
const y2 = points.find((p) => p.maturity === "2Y")?.yield;
|
|
32
|
-
const y10 = points.find((p) => p.maturity === "10Y")?.yield;
|
|
33
|
-
if (y2 == null || y10 == null) return null;
|
|
34
|
-
return Math.round((y10 - y2) * 100);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function YieldCurvePane({ focused, width, height }: PaneProps) {
|
|
38
35
|
const [points, setPoints] = useState<YieldPoint[]>([]);
|
|
39
36
|
const [loading, setLoading] = useState(false);
|
|
@@ -75,7 +72,7 @@ function YieldCurvePane({ focused, width, height }: PaneProps) {
|
|
|
75
72
|
});
|
|
76
73
|
|
|
77
74
|
const inverted = isInverted(points);
|
|
78
|
-
const bp =
|
|
75
|
+
const bp = spreadBasisPoints(points);
|
|
79
76
|
// Treasury series are daily closes, so which session the curve represents is
|
|
80
77
|
// status the user needs; "updated Xm ago" only says when we last fetched it.
|
|
81
78
|
const asOf = curveAsOf(points);
|
|
@@ -191,5 +188,6 @@ export const yieldCurveModule: PluginModule = {
|
|
|
191
188
|
description: "US Treasury yield curve charted from FRED data.",
|
|
192
189
|
keywords: ["yield", "curve", "treasury", "bonds", "rates", "gc", "interest"],
|
|
193
190
|
shortcut: { prefix: "GC" },
|
|
191
|
+
headless: yieldCurveHeadless,
|
|
194
192
|
}],
|
|
195
193
|
};
|
|
@@ -20,8 +20,12 @@ export const TREASURY_MATURITIES: Array<{ maturity: string; years: number; serie
|
|
|
20
20
|
{ maturity: "30Y", years: 30, seriesId: "DGS30" },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
export
|
|
24
|
-
|
|
23
|
+
export type YieldCurveLoader = () => Promise<YieldPoint[]>;
|
|
24
|
+
|
|
25
|
+
export async function loadYieldCurve(
|
|
26
|
+
loader: YieldCurveLoader = () => apiClient.getCloudYieldCurve(),
|
|
27
|
+
): Promise<YieldPoint[]> {
|
|
28
|
+
return loader();
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function parseYieldPoints(points: YieldPoint[]): YieldPoint[] {
|
|
@@ -45,8 +49,14 @@ export function curveAsOf(points: readonly YieldPoint[]): string | null {
|
|
|
45
49
|
return latest;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
export function
|
|
49
|
-
const y2 = points.find((
|
|
50
|
-
const y10 = points.find((
|
|
51
|
-
|
|
52
|
+
export function spreadBasisPoints(points: readonly YieldPoint[]): number | null {
|
|
53
|
+
const y2 = points.find((point) => point.maturity === "2Y")?.yield;
|
|
54
|
+
const y10 = points.find((point) => point.maturity === "10Y")?.yield;
|
|
55
|
+
if (y2 == null || y10 == null) return null;
|
|
56
|
+
return Math.round((y10 - y2) * 100);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isInverted(points: readonly YieldPoint[]): boolean {
|
|
60
|
+
const spread = spreadBasisPoints(points);
|
|
61
|
+
return spread != null && spread < 0;
|
|
52
62
|
}
|
|
@@ -22,7 +22,13 @@ function scratchPlugin(source: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const fakeExports = async (specifier: string) => (
|
|
25
|
-
specifier === "gloomberb/ui"
|
|
25
|
+
specifier === "gloomberb/ui"
|
|
26
|
+
? ["Box", "Text"]
|
|
27
|
+
: specifier === "react"
|
|
28
|
+
? ["useState"]
|
|
29
|
+
: specifier === "gloomberb/broker"
|
|
30
|
+
? ["PRESERVED_PASSWORD_HINT", "getBrokerRemoteClient"]
|
|
31
|
+
: []
|
|
26
32
|
);
|
|
27
33
|
|
|
28
34
|
describe("buildSharedModuleSource", () => {
|
|
@@ -97,6 +103,57 @@ describe("bundleExternalPlugin", () => {
|
|
|
97
103
|
}
|
|
98
104
|
});
|
|
99
105
|
|
|
106
|
+
test("shares gloomberb/broker rather than bundling the host's copy", async () => {
|
|
107
|
+
// That module owns the remote broker client the desktop view installs at
|
|
108
|
+
// startup. A plugin carrying its own copy reads an empty one and reports
|
|
109
|
+
// that the broker host is unavailable, which looks like a broken broker
|
|
110
|
+
// rather than a bundling mistake.
|
|
111
|
+
const dir = scratchPlugin(`
|
|
112
|
+
import { PRESERVED_PASSWORD_HINT } from "gloomberb/broker";
|
|
113
|
+
export default { id: "scratch", name: "Scratch", version: "1.0.0", hint: PRESERVED_PASSWORD_HINT };
|
|
114
|
+
`);
|
|
115
|
+
try {
|
|
116
|
+
const result = await bundleExternalPlugin(dir, join(dir, "out"), { exportNamesFor: fakeExports });
|
|
117
|
+
const code = await Bun.file(result.outputPath).text();
|
|
118
|
+
|
|
119
|
+
expect(result.shared).toEqual(["gloomberb/broker"]);
|
|
120
|
+
expect(code).toContain(PLUGIN_HOST_GLOBAL);
|
|
121
|
+
} finally {
|
|
122
|
+
rmSync(dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("compiles the browser entry so a plugin with a native half still loads", async () => {
|
|
127
|
+
// Bun's browser target rejects `node:*` imports even behind a dynamic
|
|
128
|
+
// import, so without this a broker that opens a socket or resolves DNS
|
|
129
|
+
// could not ship to the desktop view at all.
|
|
130
|
+
const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-browser-"));
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(dir, "package.json"),
|
|
133
|
+
JSON.stringify({ name: "scratch", main: "index.ts", browser: "index.browser.ts" }),
|
|
134
|
+
);
|
|
135
|
+
writeFileSync(join(dir, "native.ts"), `
|
|
136
|
+
import { Socket } from "node:net";
|
|
137
|
+
export const connect = () => new Socket();
|
|
138
|
+
`);
|
|
139
|
+
writeFileSync(join(dir, "index.ts"), `
|
|
140
|
+
export default { id: "scratch", name: "Scratch", version: "1.0.0", load: () => import("./native") };
|
|
141
|
+
`);
|
|
142
|
+
writeFileSync(join(dir, "index.browser.ts"), `
|
|
143
|
+
export default { id: "scratch", name: "Scratch", version: "1.0.0" };
|
|
144
|
+
`);
|
|
145
|
+
const out = join(dir, "out");
|
|
146
|
+
try {
|
|
147
|
+
const result = await bundleExternalPlugin(dir, out, { exportNamesFor: fakeExports });
|
|
148
|
+
const code = await Bun.file(result.outputPath).text();
|
|
149
|
+
|
|
150
|
+
expect(code).toContain("Scratch");
|
|
151
|
+
expect(code).not.toContain("node:net");
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(dir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
100
157
|
test("rejects a directory with no entry file", async () => {
|
|
101
158
|
const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-empty-"));
|
|
102
159
|
mkdirSync(join(dir, "src"), { recursive: true });
|
package/src/plugins/bundle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePluginBrowserEntry } from "./loader";
|
|
5
5
|
import { PLUGIN_HOST_GLOBAL, SHARED_SPECIFIERS } from "./host-contract";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -98,7 +98,9 @@ export async function bundleExternalPlugin(
|
|
|
98
98
|
outDir: string,
|
|
99
99
|
options: { exportNamesFor?: (specifier: string) => Promise<readonly string[]> } = {},
|
|
100
100
|
): Promise<BundlePluginResult> {
|
|
101
|
-
|
|
101
|
+
// The browser entry when the plugin ships one, so a plugin with a native
|
|
102
|
+
// half can still present its UI and metadata in the view.
|
|
103
|
+
const entry = await resolvePluginBrowserEntry(pluginDir);
|
|
102
104
|
if (!entry) throw new Error(`No plugin entry file in ${pluginDir}`);
|
|
103
105
|
|
|
104
106
|
await mkdir(outDir, { recursive: true });
|
|
@@ -18,4 +18,30 @@ describe("desktop backend plugin catalog", () => {
|
|
|
18
18
|
expect(plugin?.slots).toBeUndefined();
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
test("includes compatible external plugins in the native desktop backend", () => {
|
|
23
|
+
const externalPlugin = {
|
|
24
|
+
id: "external-broker",
|
|
25
|
+
name: "External broker",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
targets: ["cli", "tui", "desktop"] as const,
|
|
28
|
+
};
|
|
29
|
+
const backendPlugins = getDesktopBackendPlugins([
|
|
30
|
+
{ plugin: externalPlugin, path: "/plugins/external-broker" },
|
|
31
|
+
{
|
|
32
|
+
plugin: { id: "broken", name: "Broken", version: "1.0.0" },
|
|
33
|
+
path: "/plugins/broken",
|
|
34
|
+
error: "load failed",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
plugin: { id: "web-only", name: "Web only", version: "1.0.0" },
|
|
38
|
+
path: "/plugins/web-only",
|
|
39
|
+
unsupportedTarget: "desktop",
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
expect(backendPlugins).toContain(externalPlugin);
|
|
44
|
+
expect(backendPlugins.some((plugin) => plugin.id === "broken")).toBe(false);
|
|
45
|
+
expect(backendPlugins.some((plugin) => plugin.id === "web-only")).toBe(false);
|
|
46
|
+
});
|
|
21
47
|
});
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { GloomPlugin } from "../types/plugin";
|
|
2
2
|
import { tickerResearchBackendPlugin } from "./builtin/ticker-research-backend-plugin";
|
|
3
3
|
import { getLoadablePlugins } from "./catalog";
|
|
4
|
+
import { loadExternalPlugins, type LoadedExternalPlugin } from "./loader";
|
|
4
5
|
import { predictionMarketsBackendPlugin } from "./prediction-markets/backend-plugin";
|
|
5
6
|
|
|
6
|
-
export function getDesktopBackendPlugins(
|
|
7
|
-
|
|
7
|
+
export function getDesktopBackendPlugins(
|
|
8
|
+
externalPlugins: LoadedExternalPlugin[] = [],
|
|
9
|
+
): GloomPlugin[] {
|
|
10
|
+
return getLoadablePlugins(externalPlugins).map((plugin) => {
|
|
8
11
|
if (plugin.id === "ticker-research") return tickerResearchBackendPlugin;
|
|
9
12
|
if (plugin.id === "prediction-markets") return predictionMarketsBackendPlugin;
|
|
10
13
|
return plugin;
|
|
11
14
|
});
|
|
12
15
|
}
|
|
16
|
+
|
|
17
|
+
export async function loadDesktopBackendPlugins(): Promise<GloomPlugin[]> {
|
|
18
|
+
return getDesktopBackendPlugins(await loadExternalPlugins("desktop"));
|
|
19
|
+
}
|
|
@@ -7,19 +7,6 @@ const paneIds = browserBuiltinPlugins.flatMap((plugin) => plugin.panes?.map((pan
|
|
|
7
7
|
const templateIds = new Set(browserBuiltinPlugins.flatMap((plugin) => plugin.paneTemplates?.map((template) => template.id) ?? []));
|
|
8
8
|
|
|
9
9
|
describe("browser plugin catalog", () => {
|
|
10
|
-
test("contains the reviewed cloud, local, market, and research plugins", () => {
|
|
11
|
-
expect(ids).toEqual([
|
|
12
|
-
"gloomberb-cloud",
|
|
13
|
-
"portfolio",
|
|
14
|
-
"ticker-research",
|
|
15
|
-
"application",
|
|
16
|
-
"news",
|
|
17
|
-
"market-overview",
|
|
18
|
-
"macro",
|
|
19
|
-
"alerts",
|
|
20
|
-
]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
10
|
test("excludes native, filesystem, local AI, debug, updater, and external plugins", () => {
|
|
24
11
|
for (const forbidden of [
|
|
25
12
|
"broker",
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
} from "../data/fred-series";
|
|
5
5
|
import type { GloomPlugin } from "../types/plugin";
|
|
6
6
|
import { portfolioAnalyticsModule } from "./builtin/analytics";
|
|
7
|
+
import { earningsCallsModule } from "./builtin/earnings-calls";
|
|
8
|
+
import { researchSearchPlugin } from "./builtin/research-search";
|
|
7
9
|
import { alertsPlugin } from "./builtin/alerts";
|
|
8
10
|
import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
|
|
9
11
|
import { changelogModule } from "./builtin/changelog";
|
|
@@ -68,6 +70,7 @@ const browserTickerResearchPlugin = composeBuiltinPlugin({
|
|
|
68
70
|
optionsModule,
|
|
69
71
|
optionsCalculatorModule,
|
|
70
72
|
researchModule,
|
|
73
|
+
earningsCallsModule,
|
|
71
74
|
],
|
|
72
75
|
});
|
|
73
76
|
|
|
@@ -141,6 +144,7 @@ export const browserBuiltinPlugins: readonly GloomPlugin[] = [
|
|
|
141
144
|
browserMarketOverviewPlugin,
|
|
142
145
|
browserMacroPlugin,
|
|
143
146
|
alertsPlugin,
|
|
147
|
+
researchSearchPlugin,
|
|
144
148
|
];
|
|
145
149
|
|
|
146
150
|
export function getBrowserBuiltinPlugins(): readonly GloomPlugin[] {
|
|
@@ -34,6 +34,21 @@ describe("built-in plugin manifest", () => {
|
|
|
34
34
|
expect(featured.map((plugin) => plugin.id)).toEqual(["gloomberb-cloud"]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
test("every declared icon exists in the repo", async () => {
|
|
38
|
+
// The directory resolves these against this repo's HEAD, so a wrong path
|
|
39
|
+
// is a broken image in the plugin grid rather than a missing file here.
|
|
40
|
+
const icons = buildBuiltinManifest()
|
|
41
|
+
.plugins.map((plugin) => plugin.icon)
|
|
42
|
+
.filter((icon): icon is string => !!icon);
|
|
43
|
+
|
|
44
|
+
expect(icons.length).toBeGreaterThan(0);
|
|
45
|
+
|
|
46
|
+
for (const icon of icons) {
|
|
47
|
+
const file = Bun.file(new URL(`../../${icon}`, import.meta.url));
|
|
48
|
+
expect(await file.exists()).toBe(true);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
37
52
|
test("describes what each plugin contributes, which is what the directory renders", () => {
|
|
38
53
|
const cloud = buildBuiltinManifest().plugins.find((plugin) => plugin.id === "gloomberb-cloud");
|
|
39
54
|
|
|
@@ -18,6 +18,8 @@ export interface BuiltinPluginManifestEntry {
|
|
|
18
18
|
categories: string[];
|
|
19
19
|
toggleable: boolean;
|
|
20
20
|
featured?: true;
|
|
21
|
+
/** Path to artwork in this repo, resolved by the directory against HEAD. */
|
|
22
|
+
icon?: string;
|
|
21
23
|
contributes: {
|
|
22
24
|
panes: string[];
|
|
23
25
|
capabilities: string[];
|
|
@@ -30,8 +32,15 @@ export interface BuiltinPluginManifestEntry {
|
|
|
30
32
|
* is reported by `generate-plugin-manifest.ts` rather than silently defaulted,
|
|
31
33
|
* so a new built-in cannot slip into the directory uncategorised.
|
|
32
34
|
*/
|
|
33
|
-
const EDITORIAL: Record<
|
|
34
|
-
|
|
35
|
+
const EDITORIAL: Record<
|
|
36
|
+
string,
|
|
37
|
+
{ categories: string[]; featured?: true; icon?: string }
|
|
38
|
+
> = {
|
|
39
|
+
"gloomberb-cloud": {
|
|
40
|
+
categories: ["data", "cloud"],
|
|
41
|
+
featured: true,
|
|
42
|
+
icon: "plugin-icons/gloomberb-cloud.svg",
|
|
43
|
+
},
|
|
35
44
|
ai: { categories: ["ai"] },
|
|
36
45
|
alerts: { categories: ["alerts"] },
|
|
37
46
|
application: { categories: ["core"] },
|
|
@@ -44,10 +53,7 @@ const EDITORIAL: Record<string, { categories: string[]; featured?: true }> = {
|
|
|
44
53
|
polls: { categories: ["data"] },
|
|
45
54
|
portfolio: { categories: ["portfolio"] },
|
|
46
55
|
"prediction-markets": { categories: ["markets"] },
|
|
47
|
-
public: { categories: ["broker"] },
|
|
48
56
|
"research-search": { categories: ["research", "news"] },
|
|
49
|
-
robinhood: { categories: ["broker"] },
|
|
50
|
-
simplefin: { categories: ["broker"] },
|
|
51
57
|
"ticker-research": { categories: ["research"] },
|
|
52
58
|
yahoo: { categories: ["data"] },
|
|
53
59
|
};
|
|
@@ -74,6 +80,7 @@ export function buildBuiltinManifest(): BuiltinManifest {
|
|
|
74
80
|
categories: editorial?.categories ?? [],
|
|
75
81
|
toggleable: plugin.toggleable === true,
|
|
76
82
|
...(editorial?.featured ? { featured: true as const } : {}),
|
|
83
|
+
...(editorial?.icon ? { icon: editorial.icon } : {}),
|
|
77
84
|
contributes: {
|
|
78
85
|
panes: (plugin.panes ?? []).map((pane) => pane.id).sort(),
|
|
79
86
|
capabilities: (plugin.capabilities ?? [])
|
|
@@ -4,9 +4,6 @@ import { newsPlugin } from "./builtin/news";
|
|
|
4
4
|
import { notesPlugin } from "./builtin/notes";
|
|
5
5
|
import { aiPlugin } from "./builtin/ai";
|
|
6
6
|
import { gloomberbCloudPlugin } from "./builtin/cloud";
|
|
7
|
-
import { publicPlugin } from "./broker-sync/public";
|
|
8
|
-
import { robinhoodPlugin } from "./broker-sync/robinhood";
|
|
9
|
-
import { simpleFinPlugin } from "./broker-sync/simplefin";
|
|
10
7
|
import { predictionMarketsPlugin } from "./prediction-markets";
|
|
11
8
|
import { pollsPlugin } from "./builtin/polls";
|
|
12
9
|
import { alertsPlugin } from "./builtin/alerts";
|
|
@@ -25,9 +22,6 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
|
|
|
25
22
|
portfolioPlugin,
|
|
26
23
|
tickerResearchPlugin,
|
|
27
24
|
brokerPlugin,
|
|
28
|
-
publicPlugin,
|
|
29
|
-
robinhoodPlugin,
|
|
30
|
-
simpleFinPlugin,
|
|
31
25
|
applicationPlugin,
|
|
32
26
|
newsPlugin,
|
|
33
27
|
notesPlugin,
|
|
@@ -32,6 +32,15 @@ export const SHARED_SPECIFIERS = [
|
|
|
32
32
|
"gloomberb/capabilities",
|
|
33
33
|
"gloomberb/utils",
|
|
34
34
|
"gloomberb/react",
|
|
35
|
+
// Modules below hold state or reach the host's services, so a bundled copy
|
|
36
|
+
// is worse than a missing one. `gloomberb/broker` is the clearest case: it
|
|
37
|
+
// owns the remote broker client the desktop view sets at startup, and a
|
|
38
|
+
// plugin carrying its own copy reads an empty one and reports that the
|
|
39
|
+
// broker host is unavailable.
|
|
40
|
+
"gloomberb/broker",
|
|
41
|
+
"gloomberb/dialog",
|
|
42
|
+
"gloomberb/market-data",
|
|
43
|
+
"gloomberb/time-series",
|
|
35
44
|
] as const;
|
|
36
45
|
|
|
37
46
|
export type SharedSpecifier = (typeof SHARED_SPECIFIERS)[number];
|
|
@@ -24,6 +24,10 @@ export async function installPluginHostModules(): Promise<void> {
|
|
|
24
24
|
capabilities,
|
|
25
25
|
utils,
|
|
26
26
|
pluginReact,
|
|
27
|
+
broker,
|
|
28
|
+
dialog,
|
|
29
|
+
marketData,
|
|
30
|
+
timeSeries,
|
|
27
31
|
] = await Promise.all([
|
|
28
32
|
import("react"),
|
|
29
33
|
import("react/jsx-runtime"),
|
|
@@ -37,6 +41,10 @@ export async function installPluginHostModules(): Promise<void> {
|
|
|
37
41
|
import("../capabilities"),
|
|
38
42
|
import("../public/utils"),
|
|
39
43
|
import("../public/react"),
|
|
44
|
+
import("../public/broker"),
|
|
45
|
+
import("../ui/dialog"),
|
|
46
|
+
import("../public/market-data"),
|
|
47
|
+
import("../public/time-series"),
|
|
40
48
|
]);
|
|
41
49
|
|
|
42
50
|
const registry: Record<string, unknown> = {
|
|
@@ -51,6 +59,10 @@ export async function installPluginHostModules(): Promise<void> {
|
|
|
51
59
|
"gloomberb/capabilities": capabilities,
|
|
52
60
|
"gloomberb/utils": utils,
|
|
53
61
|
"gloomberb/react": pluginReact,
|
|
62
|
+
"gloomberb/broker": broker,
|
|
63
|
+
"gloomberb/dialog": dialog,
|
|
64
|
+
"gloomberb/market-data": marketData,
|
|
65
|
+
"gloomberb/time-series": timeSeries,
|
|
54
66
|
};
|
|
55
67
|
|
|
56
68
|
for (const specifier of SHARED_SPECIFIERS) {
|
package/src/plugins/loader.ts
CHANGED
|
@@ -9,6 +9,11 @@ import { linkHostPackages } from "./host-link";
|
|
|
9
9
|
const loaderLog = debugLog.createLogger("plugin-loader");
|
|
10
10
|
|
|
11
11
|
const PLUGINS_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugins");
|
|
12
|
+
/**
|
|
13
|
+
* Host-owned scratch space for plugins, kept beside the plugins folder rather
|
|
14
|
+
* than inside it so nothing the host writes can be mistaken for an install.
|
|
15
|
+
*/
|
|
16
|
+
const PLUGIN_CACHE_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugin-cache");
|
|
12
17
|
|
|
13
18
|
export interface LoadedExternalPlugin {
|
|
14
19
|
plugin: GloomPlugin;
|
|
@@ -22,27 +27,70 @@ export function getPluginsDir(): string {
|
|
|
22
27
|
return PLUGINS_DIR;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
export function getPluginCacheDir(): string {
|
|
31
|
+
return PLUGIN_CACHE_DIR;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a directory inside the plugins folder is a plugin at all.
|
|
36
|
+
*
|
|
37
|
+
* Dot-directories are bookkeeping, not plugins: the desktop bundle cache used
|
|
38
|
+
* to be written to `plugins/.cache`, and it showed up in `gloomberb plugins`
|
|
39
|
+
* as an installed plugin and in `gloomberb update` as a repo to pull. The
|
|
40
|
+
* cache has moved out, but old installs still have that directory, so this
|
|
41
|
+
* stays as the single rule every reader shares.
|
|
42
|
+
*/
|
|
43
|
+
export function isPluginDirectory(name: string): boolean {
|
|
44
|
+
return !name.startsWith(".");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readPluginPackageField(pluginDir: string, field: string): Promise<string | null> {
|
|
27
48
|
const pkgPath = join(pluginDir, "package.json");
|
|
28
|
-
if (existsSync(pkgPath))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
if (!existsSync(pkgPath)) return null;
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(await Bun.file(pkgPath).text());
|
|
52
|
+
const value = pkg[field];
|
|
53
|
+
if (typeof value !== "string" || !value) return null;
|
|
54
|
+
const resolved = join(pluginDir, value);
|
|
55
|
+
return existsSync(resolved) ? resolved : null;
|
|
56
|
+
} catch {
|
|
57
|
+
// Malformed package.json falls through to the index candidates.
|
|
58
|
+
return null;
|
|
38
59
|
}
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveIndexCandidate(pluginDir: string, prefix: string): string | null {
|
|
63
|
+
for (const extension of ["ts", "tsx", "js"]) {
|
|
64
|
+
const path = join(pluginDir, `${prefix}.${extension}`);
|
|
41
65
|
if (existsSync(path)) return path;
|
|
42
66
|
}
|
|
43
67
|
return null;
|
|
44
68
|
}
|
|
45
69
|
|
|
70
|
+
/** Resolves a plugin directory's entry file the way `bun install` would. */
|
|
71
|
+
export async function resolvePluginEntry(pluginDir: string): Promise<string | null> {
|
|
72
|
+
return await readPluginPackageField(pluginDir, "main")
|
|
73
|
+
?? resolveIndexCandidate(pluginDir, "index");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves the entry to compile for a browser renderer, preferring the standard
|
|
78
|
+
* `browser` field and falling back to the native entry.
|
|
79
|
+
*
|
|
80
|
+
* A plugin that touches sockets, DNS, or the filesystem cannot be compiled for
|
|
81
|
+
* the desktop view: Bun's browser target rejects `node:*` imports even behind a
|
|
82
|
+
* dynamic import. The app solves this for its own build with private alias
|
|
83
|
+
* rules that swap in stubs, which a plugin in its own repository cannot reach.
|
|
84
|
+
* The `browser` field is that same escape hatch, published: ship a
|
|
85
|
+
* renderer-safe module with the same plugin metadata and let the native half
|
|
86
|
+
* run in the Bun process, where the desktop calls it over RPC anyway.
|
|
87
|
+
*/
|
|
88
|
+
export async function resolvePluginBrowserEntry(pluginDir: string): Promise<string | null> {
|
|
89
|
+
return await readPluginPackageField(pluginDir, "browser")
|
|
90
|
+
?? resolveIndexCandidate(pluginDir, "index.browser")
|
|
91
|
+
?? await resolvePluginEntry(pluginDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
46
94
|
export function pluginSupportsTarget(plugin: GloomPlugin, target: PluginTarget): boolean {
|
|
47
95
|
// No declaration means "everywhere"; the registry fills this in for listed plugins.
|
|
48
96
|
return !plugin.targets || plugin.targets.length === 0 || plugin.targets.includes(target);
|
|
@@ -55,7 +103,7 @@ export async function loadExternalPlugins(target: PluginTarget = "cli"): Promise
|
|
|
55
103
|
const entries = await readdir(PLUGINS_DIR, { withFileTypes: true });
|
|
56
104
|
|
|
57
105
|
for (const entry of entries) {
|
|
58
|
-
if (!entry.isDirectory()) continue;
|
|
106
|
+
if (!entry.isDirectory() || !isPluginDirectory(entry.name)) continue;
|
|
59
107
|
const pluginDir = join(PLUGINS_DIR, entry.name);
|
|
60
108
|
|
|
61
109
|
const entryFile = await resolvePluginEntry(pluginDir);
|