@bstage-sdk/react 0.1.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/dist/index.js ADDED
@@ -0,0 +1,402 @@
1
+ // src/createTemplate.tsx
2
+ import { createElement } from "react";
3
+ var __bstage_registry__ = /* @__PURE__ */ new Set();
4
+ function createTemplate(Component, options) {
5
+ const { name } = options;
6
+ validateElementName(name);
7
+ const meta = {
8
+ name,
9
+ elementName: name,
10
+ framework: "react",
11
+ ...options.slot && { slot: options.slot },
12
+ ...options.type && { type: options.type }
13
+ };
14
+ function TemplateWrapper(props) {
15
+ return createElement(Component, props);
16
+ }
17
+ TemplateWrapper.displayName = `BstageTemplate(${name})`;
18
+ TemplateWrapper.__bstage_meta__ = meta;
19
+ TemplateWrapper.__bstage_options__ = options;
20
+ TemplateWrapper.__bstage_inner__ = Component;
21
+ const wrapper = TemplateWrapper;
22
+ __bstage_registry__.add(wrapper);
23
+ return wrapper;
24
+ }
25
+ var ELEMENT_NAME_RE = /^[a-z][a-z0-9-]*-[a-z0-9-]*$/;
26
+ function validateElementName(name) {
27
+ if (!ELEMENT_NAME_RE.test(name)) {
28
+ throw new Error(
29
+ `createTemplate: \uC798\uBABB\uB41C name "${name}". Custom Element \uC2A4\uD399\uC0C1 \uC18C\uBB38\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uD558\uC774\uD508\uC744 1\uAC1C \uC774\uC0C1 \uD3EC\uD568\uD574\uC57C \uD558\uBA70, \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC608: "bmf-hello"`
30
+ );
31
+ }
32
+ }
33
+
34
+ // src/registerTemplate.ts
35
+ import { createElement as createElement2, useEffect } from "react";
36
+ import { createRoot } from "react-dom/client";
37
+ import {
38
+ announceElementName,
39
+ createWebComponent
40
+ } from "@bstage-sdk/core";
41
+
42
+ // src/BstageContext.tsx
43
+ import { createContext, useContext } from "react";
44
+ var BstageContext = createContext(null);
45
+ function useBstageContext() {
46
+ const ctx = useContext(BstageContext);
47
+ if (!ctx) {
48
+ throw new Error("[bstage] useBstageContext must be used inside a bstage template");
49
+ }
50
+ return ctx;
51
+ }
52
+
53
+ // src/registerTemplate.ts
54
+ function ReadySignal({ host }) {
55
+ useEffect(() => {
56
+ host.dispatchEvent(new CustomEvent("bstage:__ready__", { bubbles: false, composed: false }));
57
+ }, []);
58
+ return null;
59
+ }
60
+ var ReactAdapter = class {
61
+ constructor(options = {}) {
62
+ this.options = options;
63
+ this.root = null;
64
+ this.component = null;
65
+ this.contextValue = null;
66
+ this.host = null;
67
+ }
68
+ mount(component, container, props, bridge) {
69
+ this.component = component;
70
+ this.contextValue = { bridge };
71
+ this.host = bridge.getHost();
72
+ if (this.options.styles && container instanceof ShadowRoot) {
73
+ const style = document.createElement("style");
74
+ style.textContent = this.options.styles;
75
+ container.appendChild(style);
76
+ }
77
+ this.root = createRoot(container);
78
+ this.render(props);
79
+ }
80
+ update(props) {
81
+ this.render(props);
82
+ }
83
+ unmount() {
84
+ const root = this.root;
85
+ queueMicrotask(() => root?.unmount());
86
+ this.root = null;
87
+ this.component = null;
88
+ this.contextValue = null;
89
+ this.host = null;
90
+ }
91
+ render(props) {
92
+ if (!this.root || !this.component || !this.contextValue || !this.host) return;
93
+ this.root.render(
94
+ createElement2(
95
+ BstageContext.Provider,
96
+ { value: this.contextValue },
97
+ createElement2(this.component, props),
98
+ createElement2(ReadySignal, { host: this.host })
99
+ )
100
+ );
101
+ }
102
+ };
103
+ function registerTemplate(template) {
104
+ const meta = template.__bstage_meta__;
105
+ const options = template.__bstage_options__;
106
+ const Component = template.__bstage_inner__;
107
+ const tagName = meta.elementName;
108
+ const WebComponent = createWebComponent(
109
+ () => new ReactAdapter({ styles: options.styles }),
110
+ Component,
111
+ { ...options, framework: "react" }
112
+ );
113
+ if (!customElements.get(tagName)) {
114
+ customElements.define(tagName, WebComponent);
115
+ }
116
+ announceElementName(tagName);
117
+ return WebComponent;
118
+ }
119
+
120
+ // src/hooks/useNavigation.ts
121
+ import { useCallback, useContext as useContext2 } from "react";
122
+ function useNavigation() {
123
+ const ctx = useContext2(BstageContext);
124
+ const bridge = ctx?.bridge;
125
+ const navigate = useCallback(
126
+ (path, params) => {
127
+ if (bridge) {
128
+ bridge.emit("navigate", { path, params });
129
+ } else {
130
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
131
+ window.location.href = path + query;
132
+ }
133
+ },
134
+ [bridge]
135
+ );
136
+ const goBack = useCallback(() => {
137
+ if (bridge) {
138
+ bridge.emit("go-back", {});
139
+ } else {
140
+ window.history.back();
141
+ }
142
+ }, [bridge]);
143
+ const openExternal = useCallback(
144
+ (url) => {
145
+ if (bridge) {
146
+ bridge.emit("open-external", { url });
147
+ } else {
148
+ window.open(url, "_blank");
149
+ }
150
+ },
151
+ [bridge]
152
+ );
153
+ return { navigate, goBack, openExternal };
154
+ }
155
+
156
+ // src/hooks/usePlatformEvent.ts
157
+ import { useContext as useContext3, useEffect as useEffect2, useRef } from "react";
158
+ function usePlatformEvent(type, handler) {
159
+ const ctx = useContext3(BstageContext);
160
+ const bridge = ctx?.bridge;
161
+ const handlerRef = useRef(handler);
162
+ handlerRef.current = handler;
163
+ useEffect2(() => {
164
+ if (!bridge) return;
165
+ return bridge.on(type, ((payload) => {
166
+ handlerRef.current(payload);
167
+ }));
168
+ }, [bridge, type]);
169
+ }
170
+
171
+ // src/hooks/useSlotContext.ts
172
+ import { useState } from "react";
173
+ function useSlotContext() {
174
+ const [context, setContext] = useState();
175
+ usePlatformEvent("slot.context", (payload) => {
176
+ setContext(payload);
177
+ });
178
+ return context;
179
+ }
180
+
181
+ // src/hooks/useShadowStyle.ts
182
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
183
+ function useShadowStyle(css) {
184
+ const ref = useRef2(null);
185
+ const injected = useRef2(false);
186
+ useEffect3(() => {
187
+ if (injected.current || !ref.current) return;
188
+ const root = ref.current.getRootNode();
189
+ if (!(root instanceof ShadowRoot)) return;
190
+ const hash = hashString(css);
191
+ if (root.querySelector(`style[data-shadow-style="${hash}"]`)) return;
192
+ const style = document.createElement("style");
193
+ style.setAttribute("data-shadow-style", hash);
194
+ style.textContent = css;
195
+ root.appendChild(style);
196
+ injected.current = true;
197
+ });
198
+ return ref;
199
+ }
200
+ function hashString(str) {
201
+ let hash = 0;
202
+ for (let i = 0; i < str.length; i++) {
203
+ hash = Math.imul(31, hash) + str.charCodeAt(i) | 0;
204
+ }
205
+ return (hash >>> 0).toString(36);
206
+ }
207
+
208
+ // src/hooks/useLocale.ts
209
+ import { useCallback as useCallback2, useSyncExternalStore } from "react";
210
+ import { readLocale, observeLocale, DEFAULT_LANGUAGE } from "@bstage-sdk/core";
211
+
212
+ // src/LocaleProvider.tsx
213
+ import { createContext as createContext2, createElement as createElement3, useContext as useContext4 } from "react";
214
+ var LocaleTargetContext = createContext2(null);
215
+ function useLocaleTarget() {
216
+ const target = useContext4(LocaleTargetContext);
217
+ if (target === null) {
218
+ throw new Error(
219
+ "[bstage] \uB85C\uCF00\uC77C \uD6C5(useLocale/useMessages/useBstageTranslations)\uC740 <BstageLocaleProvider> \uC548\uC5D0\uC11C \uD638\uCD9C\uD574\uC57C \uD569\uB2C8\uB2E4."
220
+ );
221
+ }
222
+ return target;
223
+ }
224
+ function BstageLocaleProvider({
225
+ target = "user",
226
+ children
227
+ }) {
228
+ return createElement3(LocaleTargetContext.Provider, { value: target }, children);
229
+ }
230
+
231
+ // src/hooks/useLocale.ts
232
+ function useLocale() {
233
+ const target = useLocaleTarget();
234
+ const subscribe = useCallback2((onChange) => observeLocale(onChange, target), [target]);
235
+ const getSnapshot = useCallback2(() => readLocale(target), [target]);
236
+ const current = useSyncExternalStore(subscribe, getSnapshot, () => DEFAULT_LANGUAGE);
237
+ return { current };
238
+ }
239
+
240
+ // src/hooks/useMessages.ts
241
+ import { DEFAULT_LANGUAGE as DEFAULT_LANGUAGE2 } from "@bstage-sdk/core";
242
+
243
+ // src/devWarn.ts
244
+ var warned = /* @__PURE__ */ new Set();
245
+ function devWarnOnce(dedupeKey, message) {
246
+ if (warned.has(dedupeKey)) return;
247
+ warned.add(dedupeKey);
248
+ console.warn(`[bstage] ${message}`);
249
+ }
250
+
251
+ // src/hooks/useMessages.ts
252
+ function interpolate(template, params) {
253
+ if (!params) return template;
254
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
255
+ const value = params[key];
256
+ return value === void 0 ? `{{${key}}}` : String(value);
257
+ });
258
+ }
259
+ function useMessages(messages) {
260
+ const { current } = useLocale();
261
+ return (key, params) => {
262
+ if (process.env.NODE_ENV !== "production" && typeof params === "string") {
263
+ devWarnOnce(
264
+ `msg-string-param:${String(key)}`,
265
+ `useMessages\uC758 t('${String(key)}', ...) 2\uBC88\uC9F8 \uC778\uC790\uB294 \uBCF4\uAC04 \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uC785\uB2C8\uB2E4. fallback \uBB38\uC790\uC5F4\uC740 \uBB34\uC2DC\uB429\uB2C8\uB2E4.`
266
+ );
267
+ }
268
+ const dict = messages[current] ?? messages[DEFAULT_LANGUAGE2] ?? Object.values(messages)[0] ?? {};
269
+ const template = dict[key] ?? key;
270
+ return interpolate(template, params);
271
+ };
272
+ }
273
+
274
+ // src/hooks/useBstageTranslations.ts
275
+ import { cloneElement, useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
276
+ import {
277
+ fetchTranslations,
278
+ interpolateTranslation,
279
+ parseRichText,
280
+ pickTranslation,
281
+ resolveTranslationChain
282
+ } from "@bstage-sdk/core";
283
+ var requiredDepth = /* @__PURE__ */ new Map();
284
+ function useBstageTranslations() {
285
+ const { current } = useLocale();
286
+ const target = useLocaleTarget();
287
+ const depthKey = `${target}|${current}`;
288
+ const [dicts, setDicts] = useState2();
289
+ const [depth, setDepth] = useState2(() => requiredDepth.get(depthKey) ?? 1);
290
+ const pendingRef = useRef3(false);
291
+ useEffect4(() => {
292
+ setDicts(void 0);
293
+ setDepth(requiredDepth.get(depthKey) ?? 1);
294
+ pendingRef.current = false;
295
+ }, [depthKey]);
296
+ useEffect4(() => {
297
+ let active = true;
298
+ const chain = resolveTranslationChain(current).slice(0, depth);
299
+ Promise.all(chain.map((locale) => fetchTranslations(locale, target).catch(() => ({})))).then((loaded) => {
300
+ if (!active) return;
301
+ setDicts(loaded);
302
+ pendingRef.current = false;
303
+ }).catch(() => {
304
+ if (active) setDicts([]);
305
+ });
306
+ return () => {
307
+ active = false;
308
+ };
309
+ }, [current, target, depth]);
310
+ const requestFallback = useCallback3(
311
+ (chainLength) => {
312
+ if (pendingRef.current) return;
313
+ pendingRef.current = true;
314
+ queueMicrotask(
315
+ () => setDepth((d) => {
316
+ if (d >= chainLength) return d;
317
+ const next = d + 1;
318
+ requiredDepth.set(depthKey, Math.max(requiredDepth.get(depthKey) ?? 1, next));
319
+ return next;
320
+ })
321
+ );
322
+ },
323
+ [depthKey]
324
+ );
325
+ const resolveRaw = useCallback3(
326
+ (key) => {
327
+ const resolved = dicts ? pickTranslation(dicts, key) : void 0;
328
+ if (resolved !== void 0) return resolved;
329
+ if (dicts) {
330
+ const chain = resolveTranslationChain(current);
331
+ if (dicts.length < chain.length) {
332
+ requestFallback(chain.length);
333
+ } else if (process.env.NODE_ENV !== "production") {
334
+ devWarnOnce(
335
+ `t-missing-key:${String(key)}`,
336
+ `'${String(key)}'\uB294 \uD50C\uB7AB\uD3FC \uBC88\uC5ED \uC0AC\uC804\uC5D0 \uC5C6\uB294 \uD0A4\uC785\uB2C8\uB2E4(\uC624\uD0C8\uC790\uC774\uAC70\uB098 useMessages \uB300\uC0C1\uC77C \uC218 \uC788\uC74C). \uD0A4 \uBB38\uC790\uC5F4\uC774 \uADF8\uB300\uB85C \uB80C\uB354\uB429\uB2C8\uB2E4.`
337
+ );
338
+ }
339
+ }
340
+ return void 0;
341
+ },
342
+ [dicts, current, requestFallback]
343
+ );
344
+ const t = useCallback3(
345
+ (key, params) => {
346
+ if (process.env.NODE_ENV !== "production") {
347
+ if (typeof params === "string") {
348
+ devWarnOnce(
349
+ `t-string-param:${String(key)}`,
350
+ `t('${String(key)}', ...)\uC758 2\uBC88\uC9F8 \uC778\uC790\uB294 \uBCF4\uAC04 \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uC785\uB2C8\uB2E4. fallback \uBB38\uC790\uC5F4\uC740 \uBB34\uC2DC\uB418\uACE0 \uD50C\uB7AB\uD3FC \uC0AC\uC804 \uAC12\uC774 \uB80C\uB354\uB429\uB2C8\uB2E4 \u2014 \uD15C\uD50C\uB9BF \uC790\uCCB4 \uBB38\uAD6C\uB294 useMessages\uB97C \uC4F0\uC138\uC694.`
351
+ );
352
+ }
353
+ }
354
+ const raw = resolveRaw(key);
355
+ if (process.env.NODE_ENV !== "production" && raw !== void 0 && /<\d+>/.test(raw)) {
356
+ devWarnOnce(
357
+ `t-rich-text:${String(key)}`,
358
+ `'${String(key)}'\uB294 \uB9AC\uCE58\uD14D\uC2A4\uD2B8(<0>\u2026</0>) \uBB38\uAD6C\uC785\uB2C8\uB2E4. t()\uB294 \uBB38\uC790\uC5F4\uC744 \uBC18\uD658\uD558\uBBC0\uB85C \uD0DC\uADF8\uAC00 \uAE00\uC790\uB85C \uBCF4\uC785\uB2C8\uB2E4 \u2014 tNode('${String(key)}', { tags: { 0: <a href="\u2026" /> } })\uB97C \uC4F0\uC138\uC694.`
359
+ );
360
+ }
361
+ return interpolateTranslation(raw ?? key, params);
362
+ },
363
+ [resolveRaw]
364
+ );
365
+ const tNode = useCallback3(
366
+ (key, options) => {
367
+ const raw = resolveRaw(key) ?? key;
368
+ const tokens = parseRichText(raw, options?.params);
369
+ return tokens.map((token, i) => {
370
+ if (token.type === "text") return token.value;
371
+ const element = options?.tags?.[token.index];
372
+ if (!element) {
373
+ if (process.env.NODE_ENV !== "production") {
374
+ devWarnOnce(
375
+ `tnode-missing-tag:${String(key)}:${token.index}`,
376
+ `'${String(key)}'\uC758 <${token.index}> \uD0DC\uADF8\uC5D0 \uB300\uC751\uD558\uB294 \uC5D8\uB9AC\uBA3C\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 tags: { ${token.index}: <a href="\u2026" /> }\uB97C \uB118\uAE30\uC138\uC694. \uC9C0\uAE08\uC740 \uB0B4\uC6A9\uB9CC \uB80C\uB354\uB429\uB2C8\uB2E4.`
377
+ );
378
+ }
379
+ return token.value;
380
+ }
381
+ return cloneElement(element, { key: i }, token.value);
382
+ });
383
+ },
384
+ [resolveRaw]
385
+ );
386
+ return { current, t, tNode, ready: dicts !== void 0 };
387
+ }
388
+ export {
389
+ BstageContext,
390
+ BstageLocaleProvider,
391
+ __bstage_registry__,
392
+ createTemplate,
393
+ registerTemplate,
394
+ useBstageContext,
395
+ useBstageTranslations,
396
+ useLocale,
397
+ useMessages,
398
+ useNavigation,
399
+ usePlatformEvent,
400
+ useShadowStyle,
401
+ useSlotContext
402
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@bstage-sdk/react",
3
+ "version": "0.1.0",
4
+ "description": "bstage SDK React bindings — create b.stage templates with React",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/bstage-sdk/web-sdk.git",
9
+ "directory": "packages/react"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ }
22
+ },
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@bstage-sdk/core": "0.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=18",
34
+ "react-dom": ">=18"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^18.0.0",
38
+ "@types/react-dom": "^18.0.0",
39
+ "react": "^18.0.0",
40
+ "react-dom": "^18.0.0",
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^5.5.0"
43
+ },
44
+ "license": "MIT",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }