@checkstack/ui 1.13.1 → 1.14.0
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/.storybook/main.ts +10 -33
- package/CHANGELOG.md +33 -0
- package/package.json +13 -5
- package/src/components/DynamicForm/JsonField.tsx +3 -1
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +3 -1
- package/src/components/PerformanceProvider.tsx +13 -7
- package/src/components/ScriptTestPanel.tsx +3 -1
- package/src/components/TemplateInput.tsx +6 -1
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/ToastProvider.tsx +7 -2
- package/src/index.ts +7 -1
- package/src/utils/registered-context.test.ts +27 -0
- package/src/utils/registered-context.ts +68 -0
- package/src/vite-monaco.test.ts +26 -0
- package/src/vite-monaco.ts +60 -0
package/.storybook/main.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { mergeConfig } from "vite";
|
|
5
|
+
import { monacoViteConfig } from "../src/vite-monaco";
|
|
6
6
|
|
|
7
7
|
const storybookDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const uiRoot = path.resolve(storybookDir, "..");
|
|
9
|
-
const localRequire = createRequire(import.meta.url);
|
|
10
9
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
// "npm:@codingame/monaco-vscode-extension-api"` alias only exists inside the
|
|
19
|
-
// typefox / monaco-languageclient scopes, so the Storybook build can't resolve
|
|
20
|
-
// a bare `vscode` import and fails with "Rolldown failed to resolve import
|
|
21
|
-
// 'vscode'". We resolve the alias *through* @typefox so the path follows bun's
|
|
22
|
-
// store layout on any machine/CI rather than being hardcoded.
|
|
23
|
-
const typefoxDir = path.dirname(
|
|
24
|
-
localRequire.resolve("@typefox/monaco-editor-react", { paths: [uiRoot] }),
|
|
25
|
-
);
|
|
26
|
-
const vscodeApiDir = path.dirname(
|
|
27
|
-
localRequire.resolve("vscode", { paths: [typefoxDir] }),
|
|
28
|
-
);
|
|
10
|
+
// Shared Monaco editor Vite settings (ES-module workers + the `vscode` alias),
|
|
11
|
+
// from @checkstack/ui's monacoViteConfig helper — the SAME settings the app's
|
|
12
|
+
// vite.config.ts and @checkstack/dev-server use, so the three configs never
|
|
13
|
+
// drift. `CodeEditor` (via @typefox/monaco-editor-react + monaco-languageclient)
|
|
14
|
+
// pulls in the @codingame/monaco-vscode-* stack whose runtime does
|
|
15
|
+
// `require("vscode")`; the alias resolves it under bun's isolated store.
|
|
16
|
+
const monaco = monacoViteConfig({ resolveFrom: [uiRoot] });
|
|
29
17
|
|
|
30
18
|
const config: StorybookConfig = {
|
|
31
19
|
stories: [
|
|
@@ -46,19 +34,8 @@ const config: StorybookConfig = {
|
|
|
46
34
|
},
|
|
47
35
|
viteFinal: (viteConfig) =>
|
|
48
36
|
mergeConfig(viteConfig, {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
worker: {
|
|
52
|
-
format: "es",
|
|
53
|
-
},
|
|
54
|
-
resolve: {
|
|
55
|
-
alias: {
|
|
56
|
-
// Alias the `vscode` npm-alias to its real package dir so @codingame's
|
|
57
|
-
// CJS `require("vscode")` resolves under bun's isolated store instead
|
|
58
|
-
// of failing the build.
|
|
59
|
-
vscode: vscodeApiDir,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
37
|
+
worker: monaco.worker,
|
|
38
|
+
resolve: { alias: { ...monaco.resolve.alias } },
|
|
62
39
|
}),
|
|
63
40
|
};
|
|
64
41
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ed251b6: Make `@checkstack/ui`'s Monaco `CodeEditor` render in standalone `bun run dev`, and consolidate the Monaco editor Vite settings into one shared helper so they can't drift.
|
|
8
|
+
|
|
9
|
+
- **Shared config (now in `@checkstack/ui`).** `@checkstack/ui` exports a `monacoViteConfig` helper (`@checkstack/ui/src/vite-monaco`) with the editor's Vite settings - `worker.format: "es"` and the `vscode` resolve alias (so `require("vscode")` doesn't leak into the browser). `@checkstack/ui` owns `CodeEditor` and the editor dependencies, so all three consumers now share one source: the app's `vite.config.ts`, `@checkstack/dev-server`, and `@checkstack/ui`'s own Storybook config (each previously hand-rolled its own copy).
|
|
10
|
+
- **Pre-built workers (dev server).** In a standalone plugin, `@checkstack/ui` is a _pre-bundled npm dependency_, and Vite's dependency optimizer can't process the Monaco language workers it imports via `?worker&url` - the dev server used to crash (and serving `@checkstack/ui` as source instead broke the CJS/ESM interop of its other deps). The dev server now pre-builds the three Monaco workers (editor / TypeScript / JSON) into static ES-module bundles, serves them, and redirects the `?worker&url` imports to them via `resolve.alias` (which applies during pre-bundling). `@checkstack/ui` stays pre-bundled and the workers resolve, so the editor renders. Builds are content-addressed and cached under `node_modules/.cache/checkstack-dev-monaco` (concurrency-safe atomic promotion), so only the first run after a dependency change pays the build cost. React is deduped so the editor's hooks share the dev shell's React instance.
|
|
11
|
+
|
|
12
|
+
- 968c12f: Make installed (runtime) frontend plugins actually load, via Module Federation 2.0. Previously a packed external plugin's frontend could not run: the host only shared React/router with runtime plugins, and there was no working way to share the framework/UI singletons (hand-rolled import-map externalisation hit an unsolvable rolldown CJS-interop wall).
|
|
13
|
+
|
|
14
|
+
- **Host (`@checkstack/frontend`)** now uses `@module-federation/vite` as an MF host and loads runtime plugins through the MF runtime (`registerRemotes` + `loadRemote`) instead of a raw `import()`. The shared set (react, react-dom, react-router-dom, @tanstack/react-query, @checkstack/frontend-api) is owned by the host; plugins reuse those exact instances via the share scope. The old hand-rolled vendor build + import map are removed.
|
|
15
|
+
- **`@checkstack/ui`** is bundled per consumer (tree-shaken); its Theme / Toast / Performance React contexts are unified across the host and bundled-in-plugin copies via a registered (globalThis-keyed) context, so a plugin's `useTheme`/`useToast`/`usePerformance` resolve to the host's providers. The ONE exception is the Monaco / VS Code **CodeEditor**, now exposed as the `@checkstack/ui/code-editor` subpath and shared as an MF singleton: the host owns the single editor instance (and builds its `?worker&url` workers), and plugins reuse it. A plugin can now render `<CodeEditor>` (directly or via `ScriptTestPanel` / template/JSON fields) without bundling Monaco.
|
|
16
|
+
- **Scaffold + pack (`@checkstack/scripts`)** build frontend plugins as MF remotes (`vite build` with the federation plugin, exposing `./plugin`, manifest enabled, DTS disabled). The CodeEditor is shared with `import: false` so the plugin is a consume-only participant - it never bundles a local fallback of the editor, keeping the heavy `@codingame/*` / `monaco-languageclient` / `vscode` subtree out of the plugin entirely (so no `vscode` alias or ES-worker config is needed in the plugin build). `plugin-pack` builds frontend packages with `NODE_ENV=production` (the MF plugin skips the remote under `NODE_ENV=test`) and ships only `dist/`. The scaffolded route now declares a `nav` entry so it appears in the sidebar.
|
|
17
|
+
- **Backend (`@checkstack/backend`)** serves a plugin's MF assets under its (possibly scoped) package name (`/assets/plugins/@scope/name/*`), with correct content types, and the SPA catch-all defers those paths so the federation manifest/remoteEntry are not shadowed by `index.html`.
|
|
18
|
+
|
|
19
|
+
Verified end-to-end by the external-plugin install E2E (scaffold → pack → install via the Plugin Manager UI → frontend + backend + co-loaded core plugins all work).
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- @checkstack/common@0.14.1
|
|
24
|
+
- @checkstack/frontend-api@0.7.2
|
|
25
|
+
- @checkstack/template-engine@0.4.2
|
|
26
|
+
|
|
27
|
+
## 1.13.2
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [1fee9da]
|
|
32
|
+
- @checkstack/common@0.14.1
|
|
33
|
+
- @checkstack/frontend-api@0.7.2
|
|
34
|
+
- @checkstack/template-engine@0.4.2
|
|
35
|
+
|
|
3
36
|
## 1.13.1
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./code-editor": "./src/components/CodeEditor/index.ts",
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./src/vite-monaco": "./src/vite-monaco.ts",
|
|
12
|
+
"./src/components/CodeEditor/generateTypeDefinitions": "./src/components/CodeEditor/generateTypeDefinitions.ts",
|
|
13
|
+
"./src/*": "./src/*"
|
|
14
|
+
},
|
|
7
15
|
"dependencies": {
|
|
8
|
-
"@checkstack/common": "0.
|
|
9
|
-
"@checkstack/frontend-api": "0.7.
|
|
10
|
-
"@checkstack/template-engine": "0.4.
|
|
16
|
+
"@checkstack/common": "0.14.1",
|
|
17
|
+
"@checkstack/frontend-api": "0.7.2",
|
|
18
|
+
"@checkstack/template-engine": "0.4.2",
|
|
11
19
|
"@codingame/monaco-vscode-editor-api": "25.1.2",
|
|
12
20
|
"@codingame/monaco-vscode-languages-service-override": "25.1.2",
|
|
13
21
|
"@codingame/monaco-vscode-standalone-json-language-features": "25.1.2",
|
|
@@ -43,7 +51,7 @@
|
|
|
43
51
|
"zod": "^4.2.1"
|
|
44
52
|
},
|
|
45
53
|
"devDependencies": {
|
|
46
|
-
"@checkstack/scripts": "0.
|
|
54
|
+
"@checkstack/scripts": "0.5.0",
|
|
47
55
|
"@checkstack/test-utils-frontend": "0.1.0",
|
|
48
56
|
"@checkstack/tsconfig": "0.0.7",
|
|
49
57
|
"@storybook/addon-a11y": "^10.4.1",
|
|
@@ -2,7 +2,9 @@ import React from "react";
|
|
|
2
2
|
import Ajv from "ajv";
|
|
3
3
|
import addFormats from "ajv-formats";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Deep package specifier (not "../CodeEditor"): routes through the Module
|
|
6
|
+
// Federation shared singleton so this never bundles its own Monaco copy.
|
|
7
|
+
import { CodeEditor } from "@checkstack/ui/code-editor";
|
|
6
8
|
import type { JsonFieldProps } from "./types";
|
|
7
9
|
|
|
8
10
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Label } from "../Label";
|
|
3
3
|
import { Textarea } from "../Textarea";
|
|
4
|
+
// Deep package specifier (not "../CodeEditor"): routes the CodeEditor value
|
|
5
|
+
// through the Module Federation shared singleton (see core/ui/src/index.ts).
|
|
4
6
|
import {
|
|
5
7
|
CodeEditor,
|
|
6
8
|
type TemplateProperty,
|
|
7
9
|
type AcquireTypes,
|
|
8
10
|
type AcquiredTypeFile,
|
|
9
|
-
} from "
|
|
11
|
+
} from "@checkstack/ui/code-editor";
|
|
10
12
|
import {
|
|
11
13
|
Select,
|
|
12
14
|
SelectContent,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
2
2
|
import { useToast } from "./ToastProvider";
|
|
3
|
+
import { createRegisteredContext } from "../utils/registered-context";
|
|
3
4
|
|
|
4
5
|
const STORAGE_KEY = "checkstack-low-power";
|
|
5
6
|
|
|
@@ -17,12 +18,17 @@ interface PerformanceContextValue {
|
|
|
17
18
|
toggleManualLowPower: () => void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// Registered (singleton) context: @checkstack/ui is bundled per consumer, but
|
|
22
|
+
// the host's provider and a plugin's usePerformance() must share one context.
|
|
23
|
+
const PerformanceContext = createRegisteredContext<PerformanceContextValue>(
|
|
24
|
+
"checkstack.ui.performance",
|
|
25
|
+
{
|
|
26
|
+
isLowPower: false,
|
|
27
|
+
isLoaded: false,
|
|
28
|
+
manualLowPower: false,
|
|
29
|
+
toggleManualLowPower: () => {},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* usePerformance - Hook to access the global hardware performance state.
|
|
@@ -11,7 +11,9 @@ import { Button } from "./Button";
|
|
|
11
11
|
import { Badge } from "./Badge";
|
|
12
12
|
import { Input } from "./Input";
|
|
13
13
|
import { Label } from "./Label";
|
|
14
|
-
|
|
14
|
+
// Deep package specifier (not "./CodeEditor"): routes through the Module
|
|
15
|
+
// Federation shared singleton so this never bundles its own Monaco copy.
|
|
16
|
+
import { CodeEditor } from "@checkstack/ui/code-editor";
|
|
15
17
|
import { cn } from "../utils";
|
|
16
18
|
import { usePerformance } from "./PerformanceProvider";
|
|
17
19
|
import {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
2
|
+
// Deep package specifier (not "./CodeEditor"): routes the CodeEditor value
|
|
3
|
+
// through the Module Federation shared singleton (see core/ui/src/index.ts).
|
|
4
|
+
import {
|
|
5
|
+
CodeEditor,
|
|
6
|
+
type TemplateProperty,
|
|
7
|
+
} from "@checkstack/ui/code-editor";
|
|
3
8
|
import { TemplateValueInput } from "./TemplateValueInput";
|
|
4
9
|
|
|
5
10
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
2
|
+
import { createRegisteredContext } from "../utils/registered-context";
|
|
2
3
|
|
|
3
4
|
type Theme = "light" | "dark" | "system";
|
|
4
5
|
|
|
@@ -29,7 +30,12 @@ const initialState: ThemeProviderState = {
|
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Registered (singleton) context: shared between the host's provider and a
|
|
34
|
+
// plugin's useTheme() even though @checkstack/ui is bundled per consumer.
|
|
35
|
+
const ThemeProviderContext = createRegisteredContext<ThemeProviderState>(
|
|
36
|
+
"checkstack.ui.theme",
|
|
37
|
+
initialState,
|
|
38
|
+
);
|
|
33
39
|
|
|
34
40
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
35
41
|
children,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useContext, useState, useCallback } from "react";
|
|
2
2
|
import { Toast } from "./Toast";
|
|
3
|
+
import { createRegisteredContext } from "../utils/registered-context";
|
|
3
4
|
|
|
4
5
|
type ToastVariant = "default" | "success" | "error" | "warning" | "info";
|
|
5
6
|
|
|
@@ -26,7 +27,11 @@ interface ToastContextValue {
|
|
|
26
27
|
info: (message: string, duration?: number) => void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// Registered (singleton) context: shared between the host's provider and a
|
|
31
|
+
// plugin's useToast() even though @checkstack/ui is bundled per consumer.
|
|
32
|
+
const ToastContext = createRegisteredContext<ToastContextValue | undefined>(
|
|
33
|
+
"checkstack.ui.toast",
|
|
34
|
+
);
|
|
30
35
|
|
|
31
36
|
export const useToast = (): ToastContextValue => {
|
|
32
37
|
const context = useContext(ToastContext);
|
package/src/index.ts
CHANGED
|
@@ -60,7 +60,13 @@ export * from "./components/CommandPalette";
|
|
|
60
60
|
export * from "./components/TerminalFeed";
|
|
61
61
|
export * from "./components/PerformanceProvider";
|
|
62
62
|
export * from "./components/AmbientBackground";
|
|
63
|
-
|
|
63
|
+
// The CodeEditor (Monaco / VS Code) stack is re-exported via the package's own
|
|
64
|
+
// deep specifier - NOT the relative "./components/CodeEditor" - so Module
|
|
65
|
+
// Federation can treat it as a shared singleton. The host owns the one editor
|
|
66
|
+
// instance (and builds its workers); runtime plugins reuse it via the share
|
|
67
|
+
// scope instead of bundling Monaco. The rest of @checkstack/ui stays bundled
|
|
68
|
+
// per consumer. See core/frontend/vite.config.ts `shared`.
|
|
69
|
+
export * from "@checkstack/ui/code-editor";
|
|
64
70
|
export * from "./components/TemplateValueInput";
|
|
65
71
|
export * from "./components/VariablePicker";
|
|
66
72
|
export * from "./components/TemplateInput";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { createRegisteredContext } from "./registered-context";
|
|
3
|
+
|
|
4
|
+
describe("createRegisteredContext", () => {
|
|
5
|
+
it("returns the SAME context instance for a repeated key (singleton)", () => {
|
|
6
|
+
// Simulates two bundled copies of @checkstack/ui registering the same
|
|
7
|
+
// context: the second call must reuse the first's instance.
|
|
8
|
+
const first = createRegisteredContext<number>("test.same", 1);
|
|
9
|
+
const second = createRegisteredContext<number>("test.same", 999);
|
|
10
|
+
expect(second).toBe(first);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns distinct contexts for distinct keys", () => {
|
|
14
|
+
const a = createRegisteredContext<number>("test.a", 0);
|
|
15
|
+
const b = createRegisteredContext<number>("test.b", 0);
|
|
16
|
+
expect(a).not.toBe(b);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("supports an omitted default for an undefined-capable T", () => {
|
|
20
|
+
const ctx = createRegisteredContext<string | undefined>("test.optional");
|
|
21
|
+
expect(ctx).toBeDefined();
|
|
22
|
+
// Re-registration under the same key still returns the same instance.
|
|
23
|
+
expect(createRegisteredContext<string | undefined>("test.optional")).toBe(
|
|
24
|
+
ctx,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createContext, type Context } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `React.createContext` replacement whose context object is registered in a
|
|
5
|
+
* process-global registry keyed by a stable string.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists
|
|
8
|
+
*
|
|
9
|
+
* `@checkstack/ui` is NOT a shared (vendored / import-mapped) singleton — it is
|
|
10
|
+
* bundled into every consumer (the host app AND each runtime plugin) so it can
|
|
11
|
+
* be tree-shaken (a plugin using a couple of components ships a couple of KB,
|
|
12
|
+
* not the whole 2 MB kit incl. Monaco/recharts). See the runtime
|
|
13
|
+
* frontend-plugin sharing design.
|
|
14
|
+
*
|
|
15
|
+
* But a few of its values MUST behave as singletons: the Theme / Toast /
|
|
16
|
+
* Performance React contexts. The host mounts the providers; a plugin's
|
|
17
|
+
* components call the matching hooks. With `@checkstack/ui` duplicated per
|
|
18
|
+
* bundle, a plain `createContext()` would create a DIFFERENT context object in
|
|
19
|
+
* each copy, so a plugin's `usePerformance()` would read its own empty context
|
|
20
|
+
* instead of the host's provider value.
|
|
21
|
+
*
|
|
22
|
+
* Registering the context on `globalThis` by key fixes that: whichever copy
|
|
23
|
+
* loads first (always the host, which boots before any plugin) creates and
|
|
24
|
+
* registers the context; every later copy returns that same instance. React
|
|
25
|
+
* itself is the shared singleton (resolved via the import map), so the context
|
|
26
|
+
* object is usable across the bundles. This is the well-established
|
|
27
|
+
* "singleton context" pattern for shared React context from a bundled-per-MFE
|
|
28
|
+
* library (cf. `@indeedeng/react-singleton-context`).
|
|
29
|
+
*
|
|
30
|
+
* Only contexts that are genuinely shared between host and plugins need this;
|
|
31
|
+
* purely internal component state should keep using plain `createContext`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const REGISTRY_KEY = "__checkstackUiContextRegistry__";
|
|
35
|
+
|
|
36
|
+
// The registry stores contexts for heterogeneous value types keyed by string,
|
|
37
|
+
// so it cannot be typed per key; values are stored as `Context<unknown>` and
|
|
38
|
+
// narrowed back by the caller's `T` on read. This single, contained cast is
|
|
39
|
+
// the reason the registry is wrapped in this helper instead of inlined.
|
|
40
|
+
type ContextRegistry = Map<string, Context<unknown>>;
|
|
41
|
+
|
|
42
|
+
function getRegistry(): ContextRegistry {
|
|
43
|
+
const holder = globalThis as typeof globalThis & {
|
|
44
|
+
[REGISTRY_KEY]?: ContextRegistry;
|
|
45
|
+
};
|
|
46
|
+
holder[REGISTRY_KEY] ??= new Map<string, Context<unknown>>();
|
|
47
|
+
return holder[REGISTRY_KEY];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createRegisteredContext<T>(
|
|
51
|
+
key: string,
|
|
52
|
+
// Optional so callers whose `T` includes `undefined` (a context that is
|
|
53
|
+
// absent until a provider mounts) can omit it rather than pass a literal
|
|
54
|
+
// `undefined` argument.
|
|
55
|
+
...defaultValue: [T] | []
|
|
56
|
+
): Context<T> {
|
|
57
|
+
const registry = getRegistry();
|
|
58
|
+
const existing = registry.get(key);
|
|
59
|
+
if (existing) {
|
|
60
|
+
// Safe by construction: a given `key` is always created with the same `T`.
|
|
61
|
+
return existing as Context<T>;
|
|
62
|
+
}
|
|
63
|
+
// `defaultValue[0]` is `undefined` only when the caller omitted it, in which
|
|
64
|
+
// case `T` includes `undefined`; otherwise it is the provided `T`.
|
|
65
|
+
const context = createContext<T>(defaultValue[0] as T);
|
|
66
|
+
registry.set(key, context as Context<unknown>);
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { monacoViteConfig } from "./vite-monaco";
|
|
5
|
+
|
|
6
|
+
// `@typefox/monaco-editor-react` + the `vscode` npm-alias live in
|
|
7
|
+
// `@checkstack/ui`'s dependencies (this package's root).
|
|
8
|
+
const UI_DIR = path.resolve(import.meta.dir, "..");
|
|
9
|
+
|
|
10
|
+
describe("monacoViteConfig", () => {
|
|
11
|
+
it("returns the ES-module worker format the @codingame workers require", () => {
|
|
12
|
+
const config = monacoViteConfig({ resolveFrom: [UI_DIR] });
|
|
13
|
+
expect(config.worker.format).toBe("es");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("resolves the `vscode` alias to the real @codingame package dir", () => {
|
|
17
|
+
const config = monacoViteConfig({ resolveFrom: [UI_DIR] });
|
|
18
|
+
const vscodeDir = config.resolve.alias.vscode;
|
|
19
|
+
// The alias must point at @codingame/monaco-vscode-extension-api on disk
|
|
20
|
+
// (the package `require("vscode")` actually resolves to), not the bare
|
|
21
|
+
// `vscode` specifier that leaks a runtime require into the browser.
|
|
22
|
+
expect(vscodeDir).toContain("monaco-vscode");
|
|
23
|
+
expect(path.isAbsolute(vscodeDir)).toBe(true);
|
|
24
|
+
expect(existsSync(vscodeDir)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared Vite settings required to bundle the Monaco / VS Code editor stack
|
|
6
|
+
* that `@checkstack/ui`'s `CodeEditor` pulls in (`@typefox/monaco-editor-react`
|
|
7
|
+
* + `monaco-languageclient` + `@codingame/monaco-vscode-*`).
|
|
8
|
+
*
|
|
9
|
+
* Lives here in `@checkstack/ui` (which owns `CodeEditor` and the editor
|
|
10
|
+
* dependencies) so every consumer can share one source instead of drifting:
|
|
11
|
+
* `@checkstack/frontend`'s `vite.config.ts`, `@checkstack/dev-server`'s
|
|
12
|
+
* standalone-plugin dev config, and `@checkstack/ui`'s own Storybook config.
|
|
13
|
+
*
|
|
14
|
+
* Two settings are required:
|
|
15
|
+
*
|
|
16
|
+
* - `worker.format: "es"` - the `@codingame` language-feature workers are ES
|
|
17
|
+
* modules; the default (iife) worker format cannot bundle them.
|
|
18
|
+
* - `resolve.alias.vscode` - `@typefox/monaco-editor-react` and
|
|
19
|
+
* `monaco-languageclient` declare `"vscode": "npm:@codingame/monaco-vscode-extension-api"`,
|
|
20
|
+
* but the package that actually does `require("vscode")` at runtime
|
|
21
|
+
* (`@codingame/monaco-vscode-api`) has no `vscode` in its own scope under
|
|
22
|
+
* bun's isolated `node_modules`. Aliasing every `vscode` specifier to the
|
|
23
|
+
* real package dir resolves it (otherwise a runtime `require` leaks into the
|
|
24
|
+
* browser).
|
|
25
|
+
*/
|
|
26
|
+
export interface MonacoViteConfig {
|
|
27
|
+
worker: { format: "es" };
|
|
28
|
+
resolve: { alias: { vscode: string } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the Monaco-related Vite settings, resolving the `vscode` npm-alias to
|
|
33
|
+
* an absolute path so Vite can alias it.
|
|
34
|
+
*
|
|
35
|
+
* `resolveFrom` are candidate base dirs from which
|
|
36
|
+
* `@typefox/monaco-editor-react` (and, through it, the `vscode` alias) can be
|
|
37
|
+
* resolved: `core/ui` in the monorepo, or the plugin's installed
|
|
38
|
+
* `@checkstack/ui` location in a standalone scaffold. The path follows bun's
|
|
39
|
+
* store layout on any machine rather than being hardcoded.
|
|
40
|
+
*
|
|
41
|
+
* Throws if the editor stack cannot be resolved (e.g. `@checkstack/ui` is not
|
|
42
|
+
* installed). Callers that must degrade gracefully should wrap the call.
|
|
43
|
+
*/
|
|
44
|
+
export function monacoViteConfig({
|
|
45
|
+
resolveFrom,
|
|
46
|
+
}: {
|
|
47
|
+
resolveFrom: string[];
|
|
48
|
+
}): MonacoViteConfig {
|
|
49
|
+
const req = createRequire(import.meta.url);
|
|
50
|
+
const typefoxDir = path.dirname(
|
|
51
|
+
req.resolve("@typefox/monaco-editor-react", { paths: resolveFrom }),
|
|
52
|
+
);
|
|
53
|
+
const vscodeApiDir = path.dirname(
|
|
54
|
+
req.resolve("vscode", { paths: [typefoxDir] }),
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
worker: { format: "es" },
|
|
58
|
+
resolve: { alias: { vscode: vscodeApiDir } },
|
|
59
|
+
};
|
|
60
|
+
}
|