@bstage-sdk/host 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bemyfriends Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SLOT_CATALOG: () => import_core2.SLOT_CATALOG,
24
+ TemplateHandle: () => TemplateHandle,
25
+ getSlotsByTarget: () => import_core2.getSlotsByTarget,
26
+ loadTemplate: () => loadTemplate,
27
+ loadTemplatesBySlots: () => loadTemplatesBySlots
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/loadTemplate.ts
32
+ var import_core = require("@bstage-sdk/core");
33
+
34
+ // src/scriptLoader.ts
35
+ var cache = /* @__PURE__ */ new Map();
36
+ function loadScript(url, timeoutMs = 1e4) {
37
+ const existing = cache.get(url);
38
+ if (existing) return existing;
39
+ const promise = new Promise((resolve, reject) => {
40
+ const script = document.createElement("script");
41
+ script.src = url;
42
+ script.async = true;
43
+ const timer = setTimeout(() => {
44
+ cleanup();
45
+ cache.delete(url);
46
+ reject(new Error(`Script load timed out after ${timeoutMs}ms: ${url}`));
47
+ }, timeoutMs);
48
+ const cleanup = () => {
49
+ clearTimeout(timer);
50
+ script.removeEventListener("load", onLoad);
51
+ script.removeEventListener("error", onError);
52
+ };
53
+ const onLoad = () => {
54
+ cleanup();
55
+ resolve();
56
+ };
57
+ const onError = () => {
58
+ cleanup();
59
+ cache.delete(url);
60
+ reject(new Error(`Failed to load script: ${url}`));
61
+ };
62
+ script.addEventListener("load", onLoad);
63
+ script.addEventListener("error", onError);
64
+ document.head.appendChild(script);
65
+ });
66
+ cache.set(url, promise);
67
+ return promise;
68
+ }
69
+
70
+ // src/TemplateHandle.ts
71
+ var READY_EVENT = "bstage:__ready__";
72
+ var TemplateHandle = class _TemplateHandle {
73
+ constructor(info) {
74
+ this.element = null;
75
+ this.container = null;
76
+ this.bufferedListeners = [];
77
+ this.activeListeners = [];
78
+ this._templateReady = false;
79
+ this._dispatchBuffer = [];
80
+ this.info = info;
81
+ }
82
+ /**
83
+ * 컨테이너 엘리먼트에 템플릿을 마운트합니다.
84
+ * 커스텀 엘리먼트를 생성하고, 버퍼링된 리스너를 연결한 뒤, DOM에 추가합니다.
85
+ */
86
+ mount(container) {
87
+ if (this.element) {
88
+ throw new Error(`Template "${this.info.elementName}" is already mounted`);
89
+ }
90
+ this.container = container;
91
+ this.element = document.createElement(this.info.elementName);
92
+ this.element.addEventListener(
93
+ READY_EVENT,
94
+ () => {
95
+ this._templateReady = true;
96
+ for (const { type, detail } of this._dispatchBuffer) {
97
+ this.element.dispatchEvent(
98
+ new CustomEvent(type, { bubbles: false, composed: false, detail })
99
+ );
100
+ }
101
+ this._dispatchBuffer = [];
102
+ },
103
+ { once: true }
104
+ );
105
+ for (const { type, handler } of this.bufferedListeners) {
106
+ this.element.addEventListener(type, handler);
107
+ this.activeListeners.push({ type, handler });
108
+ }
109
+ this.bufferedListeners = [];
110
+ container.appendChild(this.element);
111
+ }
112
+ /**
113
+ * 동일한 매니페스트로 독립적인 새 핸들을 생성합니다.
114
+ * 같은 슬롯을 페이지 내 여러 위치에 마운트해야 할 때 사용합니다.
115
+ * 번들 스크립트는 이미 로드된 상태이므로 추가 네트워크 요청이 없습니다.
116
+ */
117
+ fork() {
118
+ return new _TemplateHandle(this.info);
119
+ }
120
+ /**
121
+ * 템플릿을 언마운트합니다 — 리스너와 엘리먼트를 DOM에서 제거합니다.
122
+ * 멱등성: 여러 번 호출해도 안전합니다.
123
+ */
124
+ unmount() {
125
+ if (!this.element) return;
126
+ for (const { type, handler } of this.activeListeners) {
127
+ this.element.removeEventListener(type, handler);
128
+ }
129
+ this.activeListeners = [];
130
+ this.element.remove();
131
+ this.element = null;
132
+ this.container = null;
133
+ this._templateReady = false;
134
+ this._dispatchBuffer = [];
135
+ }
136
+ /**
137
+ * 템플릿 이벤트를 수신합니다 (템플릿 → 플랫폼).
138
+ * mount() 전에 호출할 수 있습니다 — 리스너가 버퍼링되어 마운트 시 연결됩니다.
139
+ * 구독 해제 함수를 반환합니다.
140
+ */
141
+ on(type, handler) {
142
+ const eventType = `bstage:${type}`;
143
+ const listener = (e) => {
144
+ handler(e.detail);
145
+ };
146
+ if (this.element) {
147
+ this.element.addEventListener(eventType, listener);
148
+ this.activeListeners.push({ type: eventType, handler: listener });
149
+ } else {
150
+ this.bufferedListeners.push({ type: eventType, handler: listener });
151
+ }
152
+ return () => {
153
+ if (this.element) {
154
+ this.element.removeEventListener(eventType, listener);
155
+ this.activeListeners = this.activeListeners.filter((l) => l.handler !== listener);
156
+ } else {
157
+ this.bufferedListeners = this.bufferedListeners.filter((l) => l.handler !== listener);
158
+ }
159
+ };
160
+ }
161
+ /**
162
+ * 템플릿으로 플랫폼 이벤트를 디스패치합니다 (플랫폼 → 템플릿).
163
+ * 템플릿이 아직 ready가 아니면 버퍼링 후 ready 시 리플레이합니다.
164
+ */
165
+ dispatch(type, payload) {
166
+ if (!this.element) {
167
+ throw new Error(`Template "${this.info.elementName}" is not mounted`);
168
+ }
169
+ const eventType = `bstage:${type}`;
170
+ if (!this._templateReady) {
171
+ this._dispatchBuffer.push({ type: eventType, detail: payload });
172
+ return;
173
+ }
174
+ this.element.dispatchEvent(
175
+ new CustomEvent(eventType, {
176
+ bubbles: false,
177
+ composed: false,
178
+ detail: payload
179
+ })
180
+ );
181
+ }
182
+ };
183
+
184
+ // src/loadTemplate.ts
185
+ async function loadTemplate(templateUrl, options) {
186
+ const timeout = options?.timeout ?? 1e4;
187
+ await loadScript(templateUrl, timeout);
188
+ const elementName = (0, import_core.lookupElementName)(templateUrl) ?? recoverLegacyElementName(templateUrl);
189
+ if (!elementName) {
190
+ throw new Error(
191
+ `Template bundle registered no custom element: ${templateUrl}. Rebuild the template with a current @bstage-sdk/cli.`
192
+ );
193
+ }
194
+ return new TemplateHandle({ elementName });
195
+ }
196
+ function recoverLegacyElementName(templateUrl) {
197
+ const path = templateUrl.split("?")[0].split("#")[0];
198
+ const segments = path.split("/").filter(Boolean);
199
+ const candidate = segments[segments.length - 2];
200
+ if (!candidate) return void 0;
201
+ return customElements.get(candidate) ? candidate : void 0;
202
+ }
203
+
204
+ // src/loadTemplatesBySlots.ts
205
+ async function loadTemplatesBySlots(customSlots, options) {
206
+ const result = /* @__PURE__ */ new Map();
207
+ const tasks = [];
208
+ for (const [slot, entries] of Object.entries(customSlots)) {
209
+ for (const entry of entries) {
210
+ tasks.push({ slot, templateUrl: entry.templateUrl });
211
+ }
212
+ }
213
+ const settled = await Promise.allSettled(
214
+ tasks.map(({ templateUrl }) => loadTemplate(templateUrl, options))
215
+ );
216
+ for (let i = 0; i < tasks.length; i++) {
217
+ const { slot } = tasks[i];
218
+ const outcome = settled[i];
219
+ if (outcome.status === "rejected") {
220
+ console.warn(`[bstage-host] Failed to load template for slot "${slot}":`, outcome.reason);
221
+ continue;
222
+ }
223
+ const handles = result.get(slot) ?? [];
224
+ handles.push(outcome.value);
225
+ result.set(slot, handles);
226
+ }
227
+ return result;
228
+ }
229
+
230
+ // src/index.ts
231
+ var import_core2 = require("@bstage-sdk/core");
232
+ // Annotate the CommonJS export names for ESM import in node:
233
+ 0 && (module.exports = {
234
+ SLOT_CATALOG,
235
+ TemplateHandle,
236
+ getSlotsByTarget,
237
+ loadTemplate,
238
+ loadTemplatesBySlots
239
+ });
@@ -0,0 +1,93 @@
1
+ import { TemplateEventType, TemplateEventMap, PlatformEventType, PlatformEventMap, CustomSlots, CustomSlotsV2 } from '@bstage-sdk/core';
2
+ export { CustomSlots, SLOT_CATALOG, TemplateSlot, getSlotsByTarget } from '@bstage-sdk/core';
3
+
4
+ /**
5
+ * 로드한 템플릿에 대해 호스트가 아는 것.
6
+ *
7
+ * `bstage build`가 manifest.json을 만들던 시절에는 sdkVersion·framework·entry도 함께 실렸지만,
8
+ * 호스트가 실제로 쓰는 값은 태그명 하나뿐이었다. 지금은 번들이 로드되면서 스스로 알린다.
9
+ */
10
+ interface TemplateInfo {
11
+ /**
12
+ * Custom Element 태그명. 템플릿이 `createTemplate({ name })`에 넣은 값 그대로.
13
+ * `customElements.define`과 `document.createElement`가 사용하는 load-bearing 식별자.
14
+ */
15
+ elementName: string;
16
+ }
17
+ interface LoadTemplateOptions {
18
+ /** 스크립트 로드 타임아웃 (밀리초 단위, 기본값: 10,000) */
19
+ timeout?: number;
20
+ }
21
+
22
+ declare class TemplateHandle {
23
+ readonly info: TemplateInfo;
24
+ private element;
25
+ private container;
26
+ private bufferedListeners;
27
+ private activeListeners;
28
+ private _templateReady;
29
+ private _dispatchBuffer;
30
+ constructor(info: TemplateInfo);
31
+ /**
32
+ * 컨테이너 엘리먼트에 템플릿을 마운트합니다.
33
+ * 커스텀 엘리먼트를 생성하고, 버퍼링된 리스너를 연결한 뒤, DOM에 추가합니다.
34
+ */
35
+ mount(container: HTMLElement): void;
36
+ /**
37
+ * 동일한 매니페스트로 독립적인 새 핸들을 생성합니다.
38
+ * 같은 슬롯을 페이지 내 여러 위치에 마운트해야 할 때 사용합니다.
39
+ * 번들 스크립트는 이미 로드된 상태이므로 추가 네트워크 요청이 없습니다.
40
+ */
41
+ fork(): TemplateHandle;
42
+ /**
43
+ * 템플릿을 언마운트합니다 — 리스너와 엘리먼트를 DOM에서 제거합니다.
44
+ * 멱등성: 여러 번 호출해도 안전합니다.
45
+ */
46
+ unmount(): void;
47
+ /**
48
+ * 템플릿 이벤트를 수신합니다 (템플릿 → 플랫폼).
49
+ * mount() 전에 호출할 수 있습니다 — 리스너가 버퍼링되어 마운트 시 연결됩니다.
50
+ * 구독 해제 함수를 반환합니다.
51
+ */
52
+ on<T extends TemplateEventType>(type: T, handler: (payload: TemplateEventMap[T]) => void): () => void;
53
+ /**
54
+ * 템플릿으로 플랫폼 이벤트를 디스패치합니다 (플랫폼 → 템플릿).
55
+ * 템플릿이 아직 ready가 아니면 버퍼링 후 ready 시 리플레이합니다.
56
+ */
57
+ dispatch<T extends PlatformEventType>(type: T, payload: PlatformEventMap[T]): void;
58
+ }
59
+
60
+ /**
61
+ * 템플릿 번들 URL로부터 템플릿을 로드합니다.
62
+ *
63
+ * 1. `<script>`로 IIFE 번들을 로드합니다 (멱등성)
64
+ * 2. 번들이 등록한 Custom Element 태그명을 확인합니다
65
+ * 3. 생명주기 관리를 위한 TemplateHandle을 반환합니다
66
+ *
67
+ * 예전에는 manifest.json을 먼저 받아 태그명과 번들 경로를 읽었습니다. manifest가 실어 나르던
68
+ * 정보는 태그명 하나뿐이었고 entry는 늘 `template.js`로 고정이라, 번들이 로드되면서 스스로
69
+ * 태그명을 알리는 방식으로 바꿨습니다. 관리도구도 `templateUrl` 하나만 내려줍니다.
70
+ *
71
+ * @example
72
+ * const tpl = await loadTemplate('/cdn/bmf/landing/template.js')
73
+ * tpl.on('navigate', ({ path }) => router.push(path))
74
+ * tpl.mount(container)
75
+ */
76
+ declare function loadTemplate(templateUrl: string, options?: LoadTemplateOptions): Promise<TemplateHandle>;
77
+
78
+ /**
79
+ * 관리도구가 CDN에 업로드한 `config-latest.json`(또는 동일 형태의 객체)을 받아
80
+ * 슬롯별 TemplateHandle 배열을 반환한다.
81
+ *
82
+ * - 런타임에서는 목록에 없는 슬롯 키도 허용(관리도구가 신버전을 선반영한 경우)
83
+ * - 개별 템플릿 로드 실패 시 해당 항목만 건너뛰고 나머지를 반환
84
+ * - 옛 모양(`CustomSlots`, 4-part 키)·새 모양(`CustomSlotsV2`, 3-part 키) 둘 다 받음.
85
+ * 한 객체에 둘이 섞여 있어도 동작한다(키 형식만 다른 같은 모양).
86
+ *
87
+ * @example
88
+ * const slotMap = await loadTemplatesBySlots(await fetchCustomSlots(spaceId))
89
+ * const beforeHandles = slotMap.get('contents.home.contents.before') ?? []
90
+ */
91
+ declare function loadTemplatesBySlots(customSlots: CustomSlots | CustomSlotsV2, options?: LoadTemplateOptions): Promise<Map<string, TemplateHandle[]>>;
92
+
93
+ export { type LoadTemplateOptions, TemplateHandle, type TemplateInfo, loadTemplate, loadTemplatesBySlots };
@@ -0,0 +1,93 @@
1
+ import { TemplateEventType, TemplateEventMap, PlatformEventType, PlatformEventMap, CustomSlots, CustomSlotsV2 } from '@bstage-sdk/core';
2
+ export { CustomSlots, SLOT_CATALOG, TemplateSlot, getSlotsByTarget } from '@bstage-sdk/core';
3
+
4
+ /**
5
+ * 로드한 템플릿에 대해 호스트가 아는 것.
6
+ *
7
+ * `bstage build`가 manifest.json을 만들던 시절에는 sdkVersion·framework·entry도 함께 실렸지만,
8
+ * 호스트가 실제로 쓰는 값은 태그명 하나뿐이었다. 지금은 번들이 로드되면서 스스로 알린다.
9
+ */
10
+ interface TemplateInfo {
11
+ /**
12
+ * Custom Element 태그명. 템플릿이 `createTemplate({ name })`에 넣은 값 그대로.
13
+ * `customElements.define`과 `document.createElement`가 사용하는 load-bearing 식별자.
14
+ */
15
+ elementName: string;
16
+ }
17
+ interface LoadTemplateOptions {
18
+ /** 스크립트 로드 타임아웃 (밀리초 단위, 기본값: 10,000) */
19
+ timeout?: number;
20
+ }
21
+
22
+ declare class TemplateHandle {
23
+ readonly info: TemplateInfo;
24
+ private element;
25
+ private container;
26
+ private bufferedListeners;
27
+ private activeListeners;
28
+ private _templateReady;
29
+ private _dispatchBuffer;
30
+ constructor(info: TemplateInfo);
31
+ /**
32
+ * 컨테이너 엘리먼트에 템플릿을 마운트합니다.
33
+ * 커스텀 엘리먼트를 생성하고, 버퍼링된 리스너를 연결한 뒤, DOM에 추가합니다.
34
+ */
35
+ mount(container: HTMLElement): void;
36
+ /**
37
+ * 동일한 매니페스트로 독립적인 새 핸들을 생성합니다.
38
+ * 같은 슬롯을 페이지 내 여러 위치에 마운트해야 할 때 사용합니다.
39
+ * 번들 스크립트는 이미 로드된 상태이므로 추가 네트워크 요청이 없습니다.
40
+ */
41
+ fork(): TemplateHandle;
42
+ /**
43
+ * 템플릿을 언마운트합니다 — 리스너와 엘리먼트를 DOM에서 제거합니다.
44
+ * 멱등성: 여러 번 호출해도 안전합니다.
45
+ */
46
+ unmount(): void;
47
+ /**
48
+ * 템플릿 이벤트를 수신합니다 (템플릿 → 플랫폼).
49
+ * mount() 전에 호출할 수 있습니다 — 리스너가 버퍼링되어 마운트 시 연결됩니다.
50
+ * 구독 해제 함수를 반환합니다.
51
+ */
52
+ on<T extends TemplateEventType>(type: T, handler: (payload: TemplateEventMap[T]) => void): () => void;
53
+ /**
54
+ * 템플릿으로 플랫폼 이벤트를 디스패치합니다 (플랫폼 → 템플릿).
55
+ * 템플릿이 아직 ready가 아니면 버퍼링 후 ready 시 리플레이합니다.
56
+ */
57
+ dispatch<T extends PlatformEventType>(type: T, payload: PlatformEventMap[T]): void;
58
+ }
59
+
60
+ /**
61
+ * 템플릿 번들 URL로부터 템플릿을 로드합니다.
62
+ *
63
+ * 1. `<script>`로 IIFE 번들을 로드합니다 (멱등성)
64
+ * 2. 번들이 등록한 Custom Element 태그명을 확인합니다
65
+ * 3. 생명주기 관리를 위한 TemplateHandle을 반환합니다
66
+ *
67
+ * 예전에는 manifest.json을 먼저 받아 태그명과 번들 경로를 읽었습니다. manifest가 실어 나르던
68
+ * 정보는 태그명 하나뿐이었고 entry는 늘 `template.js`로 고정이라, 번들이 로드되면서 스스로
69
+ * 태그명을 알리는 방식으로 바꿨습니다. 관리도구도 `templateUrl` 하나만 내려줍니다.
70
+ *
71
+ * @example
72
+ * const tpl = await loadTemplate('/cdn/bmf/landing/template.js')
73
+ * tpl.on('navigate', ({ path }) => router.push(path))
74
+ * tpl.mount(container)
75
+ */
76
+ declare function loadTemplate(templateUrl: string, options?: LoadTemplateOptions): Promise<TemplateHandle>;
77
+
78
+ /**
79
+ * 관리도구가 CDN에 업로드한 `config-latest.json`(또는 동일 형태의 객체)을 받아
80
+ * 슬롯별 TemplateHandle 배열을 반환한다.
81
+ *
82
+ * - 런타임에서는 목록에 없는 슬롯 키도 허용(관리도구가 신버전을 선반영한 경우)
83
+ * - 개별 템플릿 로드 실패 시 해당 항목만 건너뛰고 나머지를 반환
84
+ * - 옛 모양(`CustomSlots`, 4-part 키)·새 모양(`CustomSlotsV2`, 3-part 키) 둘 다 받음.
85
+ * 한 객체에 둘이 섞여 있어도 동작한다(키 형식만 다른 같은 모양).
86
+ *
87
+ * @example
88
+ * const slotMap = await loadTemplatesBySlots(await fetchCustomSlots(spaceId))
89
+ * const beforeHandles = slotMap.get('contents.home.contents.before') ?? []
90
+ */
91
+ declare function loadTemplatesBySlots(customSlots: CustomSlots | CustomSlotsV2, options?: LoadTemplateOptions): Promise<Map<string, TemplateHandle[]>>;
92
+
93
+ export { type LoadTemplateOptions, TemplateHandle, type TemplateInfo, loadTemplate, loadTemplatesBySlots };
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ // src/loadTemplate.ts
2
+ import { lookupElementName } from "@bstage-sdk/core";
3
+
4
+ // src/scriptLoader.ts
5
+ var cache = /* @__PURE__ */ new Map();
6
+ function loadScript(url, timeoutMs = 1e4) {
7
+ const existing = cache.get(url);
8
+ if (existing) return existing;
9
+ const promise = new Promise((resolve, reject) => {
10
+ const script = document.createElement("script");
11
+ script.src = url;
12
+ script.async = true;
13
+ const timer = setTimeout(() => {
14
+ cleanup();
15
+ cache.delete(url);
16
+ reject(new Error(`Script load timed out after ${timeoutMs}ms: ${url}`));
17
+ }, timeoutMs);
18
+ const cleanup = () => {
19
+ clearTimeout(timer);
20
+ script.removeEventListener("load", onLoad);
21
+ script.removeEventListener("error", onError);
22
+ };
23
+ const onLoad = () => {
24
+ cleanup();
25
+ resolve();
26
+ };
27
+ const onError = () => {
28
+ cleanup();
29
+ cache.delete(url);
30
+ reject(new Error(`Failed to load script: ${url}`));
31
+ };
32
+ script.addEventListener("load", onLoad);
33
+ script.addEventListener("error", onError);
34
+ document.head.appendChild(script);
35
+ });
36
+ cache.set(url, promise);
37
+ return promise;
38
+ }
39
+
40
+ // src/TemplateHandle.ts
41
+ var READY_EVENT = "bstage:__ready__";
42
+ var TemplateHandle = class _TemplateHandle {
43
+ constructor(info) {
44
+ this.element = null;
45
+ this.container = null;
46
+ this.bufferedListeners = [];
47
+ this.activeListeners = [];
48
+ this._templateReady = false;
49
+ this._dispatchBuffer = [];
50
+ this.info = info;
51
+ }
52
+ /**
53
+ * 컨테이너 엘리먼트에 템플릿을 마운트합니다.
54
+ * 커스텀 엘리먼트를 생성하고, 버퍼링된 리스너를 연결한 뒤, DOM에 추가합니다.
55
+ */
56
+ mount(container) {
57
+ if (this.element) {
58
+ throw new Error(`Template "${this.info.elementName}" is already mounted`);
59
+ }
60
+ this.container = container;
61
+ this.element = document.createElement(this.info.elementName);
62
+ this.element.addEventListener(
63
+ READY_EVENT,
64
+ () => {
65
+ this._templateReady = true;
66
+ for (const { type, detail } of this._dispatchBuffer) {
67
+ this.element.dispatchEvent(
68
+ new CustomEvent(type, { bubbles: false, composed: false, detail })
69
+ );
70
+ }
71
+ this._dispatchBuffer = [];
72
+ },
73
+ { once: true }
74
+ );
75
+ for (const { type, handler } of this.bufferedListeners) {
76
+ this.element.addEventListener(type, handler);
77
+ this.activeListeners.push({ type, handler });
78
+ }
79
+ this.bufferedListeners = [];
80
+ container.appendChild(this.element);
81
+ }
82
+ /**
83
+ * 동일한 매니페스트로 독립적인 새 핸들을 생성합니다.
84
+ * 같은 슬롯을 페이지 내 여러 위치에 마운트해야 할 때 사용합니다.
85
+ * 번들 스크립트는 이미 로드된 상태이므로 추가 네트워크 요청이 없습니다.
86
+ */
87
+ fork() {
88
+ return new _TemplateHandle(this.info);
89
+ }
90
+ /**
91
+ * 템플릿을 언마운트합니다 — 리스너와 엘리먼트를 DOM에서 제거합니다.
92
+ * 멱등성: 여러 번 호출해도 안전합니다.
93
+ */
94
+ unmount() {
95
+ if (!this.element) return;
96
+ for (const { type, handler } of this.activeListeners) {
97
+ this.element.removeEventListener(type, handler);
98
+ }
99
+ this.activeListeners = [];
100
+ this.element.remove();
101
+ this.element = null;
102
+ this.container = null;
103
+ this._templateReady = false;
104
+ this._dispatchBuffer = [];
105
+ }
106
+ /**
107
+ * 템플릿 이벤트를 수신합니다 (템플릿 → 플랫폼).
108
+ * mount() 전에 호출할 수 있습니다 — 리스너가 버퍼링되어 마운트 시 연결됩니다.
109
+ * 구독 해제 함수를 반환합니다.
110
+ */
111
+ on(type, handler) {
112
+ const eventType = `bstage:${type}`;
113
+ const listener = (e) => {
114
+ handler(e.detail);
115
+ };
116
+ if (this.element) {
117
+ this.element.addEventListener(eventType, listener);
118
+ this.activeListeners.push({ type: eventType, handler: listener });
119
+ } else {
120
+ this.bufferedListeners.push({ type: eventType, handler: listener });
121
+ }
122
+ return () => {
123
+ if (this.element) {
124
+ this.element.removeEventListener(eventType, listener);
125
+ this.activeListeners = this.activeListeners.filter((l) => l.handler !== listener);
126
+ } else {
127
+ this.bufferedListeners = this.bufferedListeners.filter((l) => l.handler !== listener);
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * 템플릿으로 플랫폼 이벤트를 디스패치합니다 (플랫폼 → 템플릿).
133
+ * 템플릿이 아직 ready가 아니면 버퍼링 후 ready 시 리플레이합니다.
134
+ */
135
+ dispatch(type, payload) {
136
+ if (!this.element) {
137
+ throw new Error(`Template "${this.info.elementName}" is not mounted`);
138
+ }
139
+ const eventType = `bstage:${type}`;
140
+ if (!this._templateReady) {
141
+ this._dispatchBuffer.push({ type: eventType, detail: payload });
142
+ return;
143
+ }
144
+ this.element.dispatchEvent(
145
+ new CustomEvent(eventType, {
146
+ bubbles: false,
147
+ composed: false,
148
+ detail: payload
149
+ })
150
+ );
151
+ }
152
+ };
153
+
154
+ // src/loadTemplate.ts
155
+ async function loadTemplate(templateUrl, options) {
156
+ const timeout = options?.timeout ?? 1e4;
157
+ await loadScript(templateUrl, timeout);
158
+ const elementName = lookupElementName(templateUrl) ?? recoverLegacyElementName(templateUrl);
159
+ if (!elementName) {
160
+ throw new Error(
161
+ `Template bundle registered no custom element: ${templateUrl}. Rebuild the template with a current @bstage-sdk/cli.`
162
+ );
163
+ }
164
+ return new TemplateHandle({ elementName });
165
+ }
166
+ function recoverLegacyElementName(templateUrl) {
167
+ const path = templateUrl.split("?")[0].split("#")[0];
168
+ const segments = path.split("/").filter(Boolean);
169
+ const candidate = segments[segments.length - 2];
170
+ if (!candidate) return void 0;
171
+ return customElements.get(candidate) ? candidate : void 0;
172
+ }
173
+
174
+ // src/loadTemplatesBySlots.ts
175
+ async function loadTemplatesBySlots(customSlots, options) {
176
+ const result = /* @__PURE__ */ new Map();
177
+ const tasks = [];
178
+ for (const [slot, entries] of Object.entries(customSlots)) {
179
+ for (const entry of entries) {
180
+ tasks.push({ slot, templateUrl: entry.templateUrl });
181
+ }
182
+ }
183
+ const settled = await Promise.allSettled(
184
+ tasks.map(({ templateUrl }) => loadTemplate(templateUrl, options))
185
+ );
186
+ for (let i = 0; i < tasks.length; i++) {
187
+ const { slot } = tasks[i];
188
+ const outcome = settled[i];
189
+ if (outcome.status === "rejected") {
190
+ console.warn(`[bstage-host] Failed to load template for slot "${slot}":`, outcome.reason);
191
+ continue;
192
+ }
193
+ const handles = result.get(slot) ?? [];
194
+ handles.push(outcome.value);
195
+ result.set(slot, handles);
196
+ }
197
+ return result;
198
+ }
199
+
200
+ // src/index.ts
201
+ import {
202
+ SLOT_CATALOG,
203
+ getSlotsByTarget
204
+ } from "@bstage-sdk/core";
205
+ export {
206
+ SLOT_CATALOG,
207
+ TemplateHandle,
208
+ getSlotsByTarget,
209
+ loadTemplate,
210
+ loadTemplatesBySlots
211
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@bstage-sdk/host",
3
+ "version": "0.1.0",
4
+ "description": "bstage SDK host — load and manage template bundles on the platform side",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/bstage-sdk/web-sdk.git",
9
+ "directory": "packages/host"
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
+ "devDependencies": {
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.5.0"
35
+ },
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "typecheck": "tsc --noEmit"
43
+ }
44
+ }