@momentco-ai/moment-sdk 0.2.2-dev.17 → 0.3.0-dev.18
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/CHANGELOG.md +81 -0
- package/README.md +226 -118
- package/dist/moment-sdk.js +2 -2
- package/dist/moment-sdk.mjs +28 -19
- package/dist/types/sdk/moment-sdk.d.ts +2 -0
- package/dist/types/sdk/types.d.ts +7 -3
- package/examples/README.md +36 -0
- package/examples/brand-schedule/index.html +41 -0
- package/examples/brand-schedule/main.js +18 -0
- package/examples/collection/index.html +41 -0
- package/examples/collection/main.js +24 -0
- package/examples/index.html +38 -0
- package/examples/index.js +11 -0
- package/examples/moment/index.html +33 -0
- package/examples/moment/main.js +20 -0
- package/examples/serve.mjs +104 -0
- package/examples/shared/boot.js +166 -0
- package/examples/shared/demo.css +270 -0
- package/examples/shared/fixtures.js +24 -0
- package/package.json +9 -2
package/dist/moment-sdk.mjs
CHANGED
|
@@ -124,10 +124,11 @@ class E {
|
|
|
124
124
|
* Programmatically open the sync modal.
|
|
125
125
|
*/
|
|
126
126
|
open(e) {
|
|
127
|
+
const t = this.scopeOpenPayload(e);
|
|
127
128
|
this.emitAnalytics("embed.open", {
|
|
128
|
-
teamSlug:
|
|
129
|
-
triggerType:
|
|
130
|
-
}), this.createModal(
|
|
129
|
+
teamSlug: t.teamSlug,
|
|
130
|
+
triggerType: t.triggerType
|
|
131
|
+
}), this.createModal(t);
|
|
131
132
|
}
|
|
132
133
|
/**
|
|
133
134
|
* Close the modal and clean up all resources.
|
|
@@ -164,34 +165,42 @@ class E {
|
|
|
164
165
|
handleTriggerClick(e) {
|
|
165
166
|
const t = e.getAttribute("data-moment-team-slug") ?? "", i = e.getAttribute("data-moment-slug") ?? void 0, o = e.getAttribute("data-moment-list-slug") ?? void 0, n = e.getAttribute("data-moment-trigger-type");
|
|
166
167
|
let r = "moment";
|
|
167
|
-
n != null && n !== "" && (n === "moment" || n === "list" || n === "team" ? r = n : console.warn(
|
|
168
|
+
n != null && n !== "" && (n === "moment" || n === "list" || n === "team" || n === "brand-playlist" ? r = n : console.warn(
|
|
168
169
|
"[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",
|
|
169
170
|
n
|
|
170
171
|
));
|
|
171
|
-
const d = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "",
|
|
172
|
+
const d = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "", c = d ? d.split(",").filter(Boolean) : [], a = e.getAttribute("data-moment-calendar");
|
|
172
173
|
let m;
|
|
173
174
|
a != null && a !== "" && (a === "google" || a === "outlook" ? m = a : console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:", a));
|
|
174
|
-
const l = e.getAttribute("data-api-source-id") ?? void 0,
|
|
175
|
+
const l = e.getAttribute("data-api-source-id") ?? void 0, u = e.getAttribute("data-external-event-id") ?? void 0;
|
|
175
176
|
if (!t) {
|
|
176
177
|
console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");
|
|
177
178
|
return;
|
|
178
179
|
}
|
|
179
|
-
const
|
|
180
|
+
const p = r === "team" || r === "brand-playlist";
|
|
180
181
|
this.emitAnalytics("embed.trigger.click", {
|
|
181
182
|
teamSlug: t,
|
|
182
183
|
triggerType: r,
|
|
183
|
-
momentIds: p
|
|
184
|
+
momentIds: p ? [] : c
|
|
184
185
|
}), this.open({
|
|
185
186
|
teamSlug: t,
|
|
186
|
-
momentSlug:
|
|
187
|
-
listSlug:
|
|
187
|
+
momentSlug: i,
|
|
188
|
+
listSlug: o,
|
|
188
189
|
triggerType: r,
|
|
189
|
-
ids:
|
|
190
|
+
ids: c,
|
|
190
191
|
calendar: m ?? void 0,
|
|
191
192
|
apiSourceId: l,
|
|
192
|
-
externalEventId:
|
|
193
|
+
externalEventId: u
|
|
193
194
|
});
|
|
194
195
|
}
|
|
196
|
+
/** Drop moment/list/source scoping for brand-level subscription triggers. */
|
|
197
|
+
scopeOpenPayload(e) {
|
|
198
|
+
return e.triggerType === "team" || e.triggerType === "brand-playlist" ? {
|
|
199
|
+
teamSlug: e.teamSlug,
|
|
200
|
+
triggerType: e.triggerType,
|
|
201
|
+
calendar: e.calendar
|
|
202
|
+
} : e;
|
|
203
|
+
}
|
|
195
204
|
// ── Private: modal creation ───────────────────────────────────────
|
|
196
205
|
createModal(e) {
|
|
197
206
|
this.cleanup(), this.previousFocusedElement = document.activeElement || null, this.activePayload = e;
|
|
@@ -270,8 +279,8 @@ class E {
|
|
|
270
279
|
gap: "6px",
|
|
271
280
|
padding: "12px 0"
|
|
272
281
|
});
|
|
273
|
-
const
|
|
274
|
-
|
|
282
|
+
const c = document.createElement("span");
|
|
283
|
+
c.textContent = "Powered by", Object.assign(c.style, {
|
|
275
284
|
fontFamily: "Inter, system-ui, -apple-system, sans-serif",
|
|
276
285
|
fontSize: "12px",
|
|
277
286
|
lineHeight: "16px",
|
|
@@ -281,18 +290,18 @@ class E {
|
|
|
281
290
|
a.alt = "Moment", a.src = `${this.liveBaseUrl}/brand/moment-text-white.png`, Object.assign(a.style, {
|
|
282
291
|
height: "auto",
|
|
283
292
|
width: "67px"
|
|
284
|
-
}), d.appendChild(
|
|
293
|
+
}), d.appendChild(c), d.appendChild(a), o.appendChild(n), o.appendChild(d), this.overlay.appendChild(o), document.body.appendChild(this.overlay);
|
|
285
294
|
const m = (l) => {
|
|
286
295
|
if (l.key !== "Tab") return;
|
|
287
|
-
const
|
|
296
|
+
const u = n.querySelectorAll(
|
|
288
297
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
289
298
|
);
|
|
290
|
-
if (
|
|
299
|
+
if (u.length === 0) {
|
|
291
300
|
l.preventDefault(), n.focus();
|
|
292
301
|
return;
|
|
293
302
|
}
|
|
294
|
-
const
|
|
295
|
-
l.shiftKey &&
|
|
303
|
+
const p = u[0], h = u[u.length - 1], g = document.activeElement;
|
|
304
|
+
l.shiftKey && g === p ? (l.preventDefault(), h.focus()) : !l.shiftKey && g === h && (l.preventDefault(), p.focus());
|
|
296
305
|
};
|
|
297
306
|
n.addEventListener("keydown", m), r.focus(), this.previousBodyOverflow = document.body.style.overflow, document.body.style.overflow = "hidden", this.didManageBodyOverflow = !0, this.setupMessageListener(), this.overlay._escHandler = i, this.overlay._focusTrapHandler = m;
|
|
298
307
|
}
|
|
@@ -39,6 +39,8 @@ export declare class MomentSdk {
|
|
|
39
39
|
private bindTriggers;
|
|
40
40
|
private unbindTriggers;
|
|
41
41
|
private handleTriggerClick;
|
|
42
|
+
/** Drop moment/list/source scoping for brand-level subscription triggers. */
|
|
43
|
+
private scopeOpenPayload;
|
|
42
44
|
private createModal;
|
|
43
45
|
private buildIframeUrl;
|
|
44
46
|
private setupMessageListener;
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* - `moment` → subscribe to one or more specific moments
|
|
6
6
|
* - `list` → subscribe to a curated list of moments
|
|
7
7
|
* - `team` → subscribe to a team / brand schedule (all published moments)
|
|
8
|
+
* - `brand-playlist` → subscribe to the brand's default playlist
|
|
8
9
|
*/
|
|
9
|
-
export type MomentTriggerType = 'moment' | 'list' | 'team';
|
|
10
|
+
export type MomentTriggerType = 'moment' | 'list' | 'team' | 'brand-playlist';
|
|
10
11
|
export type CalendarProvider = 'google' | 'outlook';
|
|
11
12
|
export interface MomentSdkInitOptions {
|
|
12
13
|
/** CSS selector for trigger elements. Default: ".moment-sync-trigger" */
|
|
@@ -30,7 +31,10 @@ export interface MomentSdkInitOptions {
|
|
|
30
31
|
* - `list` → `teamSlug` plus `listSlug` (`ids` not required — list resolves
|
|
31
32
|
* to a single list-level subscription)
|
|
32
33
|
* - `team` → `teamSlug` only (resolves to the brand's team-level
|
|
33
|
-
* subscription
|
|
34
|
+
* subscription). Omit `listSlug`, `momentSlug`, `ids`, `apiSourceId`, and
|
|
35
|
+
* `externalEventId` for this trigger type.
|
|
36
|
+
* - `brand-playlist` → `teamSlug` only (Live resolves the brand's
|
|
37
|
+
* `defaultPlaylistId`). Omit the same fields as `team`.
|
|
34
38
|
*/
|
|
35
39
|
export interface MomentSdkOpenPayload {
|
|
36
40
|
teamSlug: string;
|
|
@@ -55,7 +59,7 @@ export interface MomentSdkOpenPayload {
|
|
|
55
59
|
externalEventId?: string;
|
|
56
60
|
}
|
|
57
61
|
/** Backend subscription resource type that the embed will create */
|
|
58
|
-
export type MomentSubscriptionType = 'moment' | 'list' | 'team';
|
|
62
|
+
export type MomentSubscriptionType = 'moment' | 'list' | 'team' | 'playlist';
|
|
59
63
|
/** Iframe → Parent: request to start OAuth via popup */
|
|
60
64
|
export interface MomentOAuthStartPayload {
|
|
61
65
|
source: 'moment-live-embed';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Moment SDK examples
|
|
2
|
+
|
|
3
|
+
Interactive demos shipped inside `@momentco-ai/moment-sdk`. They use the **sdk-demo** fixture brand and the SDK build from the same package you installed.
|
|
4
|
+
|
|
5
|
+
## Run from npm
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx moment-sdk-examples
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or after installing the package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @momentco-ai/moment-sdk
|
|
15
|
+
npx moment-sdk-examples
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Opens **http://localhost:5051** with:
|
|
19
|
+
|
|
20
|
+
| Page | Trigger type |
|
|
21
|
+
| ------------------ | ---------------- |
|
|
22
|
+
| `/` | Hub |
|
|
23
|
+
| `/moment/` | `moment` |
|
|
24
|
+
| `/collection/` | `list` |
|
|
25
|
+
| `/brand-schedule/` | `brand-playlist` |
|
|
26
|
+
|
|
27
|
+
Demo slugs are in `shared/fixtures.js`. Edit that file if your account uses different values.
|
|
28
|
+
|
|
29
|
+
## Maintainer workflow (repo clone)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm build
|
|
33
|
+
pnpm examples
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`pnpm examples` builds the SDK, then runs the same server npm users get.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Moment SDK — Brand schedule trigger</title>
|
|
7
|
+
<link rel="stylesheet" href="/shared/demo.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="demo-shell">
|
|
11
|
+
<div id="demo-nav" class="demo-top"></div>
|
|
12
|
+
|
|
13
|
+
<h1>Brand schedule trigger</h1>
|
|
14
|
+
<p class="lead">
|
|
15
|
+
Follow the brand's default schedule. Only <code>data-moment-team-slug</code> and
|
|
16
|
+
<code>data-moment-trigger-type="brand-playlist"</code> are required.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<article class="hero-card">
|
|
20
|
+
<div class="card-info">
|
|
21
|
+
<div class="card-title">Follow all sdk-demo events</div>
|
|
22
|
+
<div class="card-meta">Team slug <code id="team-slug-label"></code></div>
|
|
23
|
+
</div>
|
|
24
|
+
<button type="button" id="schedule-trigger" class="moment-sync-trigger demo-btn">
|
|
25
|
+
Follow All Schedule
|
|
26
|
+
</button>
|
|
27
|
+
</article>
|
|
28
|
+
|
|
29
|
+
<section class="section">
|
|
30
|
+
<h2>Embed snippet</h2>
|
|
31
|
+
<div class="snippet">
|
|
32
|
+
<button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
|
|
33
|
+
<pre id="snippet-code">Loading snippet…</pre>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<div id="demo-log-host"></div>
|
|
38
|
+
</main>
|
|
39
|
+
<script type="module" src="/brand-schedule/main.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DEMO_FIXTURES } from '../shared/fixtures.js';
|
|
2
|
+
import { embedSnippetHtml, initDemoSdk, mountLogPanel, mountNav } from '../shared/boot.js';
|
|
3
|
+
|
|
4
|
+
mountNav({ title: 'Brand schedule' });
|
|
5
|
+
mountLogPanel();
|
|
6
|
+
|
|
7
|
+
document.getElementById('team-slug-label').textContent = DEMO_FIXTURES.teamSlug;
|
|
8
|
+
|
|
9
|
+
const trigger = document.getElementById('schedule-trigger');
|
|
10
|
+
trigger.setAttribute('data-moment-team-slug', DEMO_FIXTURES.teamSlug);
|
|
11
|
+
trigger.setAttribute('data-moment-trigger-type', 'brand-playlist');
|
|
12
|
+
|
|
13
|
+
document.getElementById('snippet-code').textContent = await embedSnippetHtml({
|
|
14
|
+
triggerType: 'brand-playlist',
|
|
15
|
+
label: 'Follow All Schedule',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await initDemoSdk();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Moment SDK — Collection trigger</title>
|
|
7
|
+
<link rel="stylesheet" href="/shared/demo.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="demo-shell">
|
|
11
|
+
<div id="demo-nav" class="demo-top"></div>
|
|
12
|
+
|
|
13
|
+
<h1>Collection trigger</h1>
|
|
14
|
+
<p class="lead">
|
|
15
|
+
Sync a curated list of moments. Requires <code>data-moment-list-slug</code> with
|
|
16
|
+
<code>triggerType: list</code>.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<article class="hero-card">
|
|
20
|
+
<div class="card-info">
|
|
21
|
+
<div class="card-title" id="collection-title"></div>
|
|
22
|
+
<div class="card-meta" id="collection-meta"></div>
|
|
23
|
+
</div>
|
|
24
|
+
<button type="button" id="collection-trigger" class="moment-sync-trigger demo-btn">
|
|
25
|
+
Sync Collection
|
|
26
|
+
</button>
|
|
27
|
+
</article>
|
|
28
|
+
|
|
29
|
+
<section class="section">
|
|
30
|
+
<h2>Embed snippet</h2>
|
|
31
|
+
<div class="snippet">
|
|
32
|
+
<button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
|
|
33
|
+
<pre id="snippet-code">Loading snippet…</pre>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<div id="demo-log-host"></div>
|
|
38
|
+
</main>
|
|
39
|
+
<script type="module" src="/collection/main.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DEMO_FIXTURES } from '../shared/fixtures.js';
|
|
2
|
+
import { embedSnippetHtml, initDemoSdk, mountLogPanel, mountNav } from '../shared/boot.js';
|
|
3
|
+
|
|
4
|
+
const { collection, teamSlug } = DEMO_FIXTURES;
|
|
5
|
+
|
|
6
|
+
mountNav({ title: 'Collection' });
|
|
7
|
+
mountLogPanel();
|
|
8
|
+
|
|
9
|
+
document.getElementById('collection-title').textContent = collection.title;
|
|
10
|
+
document.getElementById('collection-meta').innerHTML =
|
|
11
|
+
`${collection.description} · <code>${collection.slug}</code>`;
|
|
12
|
+
|
|
13
|
+
const trigger = document.getElementById('collection-trigger');
|
|
14
|
+
trigger.setAttribute('data-moment-team-slug', teamSlug);
|
|
15
|
+
trigger.setAttribute('data-moment-list-slug', collection.slug);
|
|
16
|
+
trigger.setAttribute('data-moment-trigger-type', 'list');
|
|
17
|
+
|
|
18
|
+
document.getElementById('snippet-code').textContent = await embedSnippetHtml({
|
|
19
|
+
triggerType: 'list',
|
|
20
|
+
listSlug: collection.slug,
|
|
21
|
+
label: 'Sync Collection',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await initDemoSdk();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Moment SDK — Examples</title>
|
|
7
|
+
<link rel="stylesheet" href="/shared/demo.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="demo-shell">
|
|
11
|
+
<div id="demo-nav" class="demo-top"></div>
|
|
12
|
+
|
|
13
|
+
<h1>Moment SDK examples</h1>
|
|
14
|
+
<p class="lead">
|
|
15
|
+
Interactive demos using the <strong>sdk-demo</strong> fixture brand. Each page loads the SDK
|
|
16
|
+
from this package and opens the same calendar sync flow your site will use.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div class="hub-grid">
|
|
20
|
+
<a class="hub-card" href="/moment/">
|
|
21
|
+
<h2>Moment</h2>
|
|
22
|
+
<p>Single-event sync with <code>triggerType: moment</code>.</p>
|
|
23
|
+
</a>
|
|
24
|
+
<a class="hub-card" href="/collection/">
|
|
25
|
+
<h2>Collection</h2>
|
|
26
|
+
<p>List sync with <code>triggerType: list</code>.</p>
|
|
27
|
+
</a>
|
|
28
|
+
<a class="hub-card" href="/brand-schedule/">
|
|
29
|
+
<h2>Brand schedule</h2>
|
|
30
|
+
<p>Follow the full schedule with <code>triggerType: brand-playlist</code>.</p>
|
|
31
|
+
</a>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div id="demo-log-host"></div>
|
|
35
|
+
</main>
|
|
36
|
+
<script type="module" src="/index.js"></script>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Moment SDK — Moment trigger</title>
|
|
7
|
+
<link rel="stylesheet" href="/shared/demo.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="demo-shell">
|
|
11
|
+
<div id="demo-nav" class="demo-top"></div>
|
|
12
|
+
|
|
13
|
+
<h1>Moment trigger</h1>
|
|
14
|
+
<p class="lead">
|
|
15
|
+
Sync one published event. Requires <code>data-moment-team-slug</code> and
|
|
16
|
+
<code>data-moment-slug</code>.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div id="events" class="card-grid"></div>
|
|
20
|
+
|
|
21
|
+
<section class="section">
|
|
22
|
+
<h2>Embed snippet</h2>
|
|
23
|
+
<div class="snippet">
|
|
24
|
+
<button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
|
|
25
|
+
<pre id="snippet-code">Loading snippet…</pre>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<div id="demo-log-host"></div>
|
|
30
|
+
</main>
|
|
31
|
+
<script type="module" src="/moment/main.js"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DEMO_FIXTURES } from '../shared/fixtures.js';
|
|
2
|
+
import {
|
|
3
|
+
embedSnippetHtml,
|
|
4
|
+
initDemoSdk,
|
|
5
|
+
mountLogPanel,
|
|
6
|
+
mountNav,
|
|
7
|
+
renderMomentCards,
|
|
8
|
+
} from '../shared/boot.js';
|
|
9
|
+
|
|
10
|
+
mountNav({ title: 'Moment' });
|
|
11
|
+
mountLogPanel();
|
|
12
|
+
renderMomentCards('#events', DEMO_FIXTURES.moments, DEMO_FIXTURES.teamSlug);
|
|
13
|
+
|
|
14
|
+
document.getElementById('snippet-code').textContent = await embedSnippetHtml({
|
|
15
|
+
triggerType: 'moment',
|
|
16
|
+
momentSlug: DEMO_FIXTURES.moments[0].slug,
|
|
17
|
+
label: 'Add to Calendar',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await initDemoSdk();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { dirname, extname, join, normalize, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const examplesRoot = __dirname;
|
|
9
|
+
const packageRoot = join(__dirname, '..');
|
|
10
|
+
const sdkPath = join(packageRoot, 'dist', 'moment-sdk.js');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
12
|
+
|
|
13
|
+
const PORT = Number(process.env.PORT || 5051);
|
|
14
|
+
const MIME = {
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.html': 'text/html; charset=utf-8',
|
|
17
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
18
|
+
'.json': 'application/json; charset=utf-8',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ROUTES = {
|
|
22
|
+
'/': 'index.html',
|
|
23
|
+
'/moment': 'moment/index.html',
|
|
24
|
+
'/moment/': 'moment/index.html',
|
|
25
|
+
'/collection': 'collection/index.html',
|
|
26
|
+
'/collection/': 'collection/index.html',
|
|
27
|
+
'/brand-schedule': 'brand-schedule/index.html',
|
|
28
|
+
'/brand-schedule/': 'brand-schedule/index.html',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function send(res, status, body, type = 'text/plain; charset=utf-8') {
|
|
32
|
+
res.writeHead(status, { 'Content-Type': type });
|
|
33
|
+
res.end(body);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safePath(relativePath) {
|
|
37
|
+
const resolved = normalize(join(examplesRoot, relativePath));
|
|
38
|
+
const rootPrefix = examplesRoot.endsWith(sep) ? examplesRoot : `${examplesRoot}${sep}`;
|
|
39
|
+
if (resolved !== examplesRoot && !resolved.startsWith(rootPrefix)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function serveFile(res, relativePath) {
|
|
46
|
+
const filePath = safePath(relativePath);
|
|
47
|
+
if (!filePath || !existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const type = MIME[extname(filePath)] ?? 'application/octet-stream';
|
|
52
|
+
res.writeHead(200, { 'Content-Type': type });
|
|
53
|
+
createReadStream(filePath).pipe(res);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const server = createServer((req, res) => {
|
|
58
|
+
const pathname = req.url?.split('?')[0] ?? '/';
|
|
59
|
+
|
|
60
|
+
if (pathname === '/moment-sdk.js') {
|
|
61
|
+
if (!existsSync(sdkPath)) {
|
|
62
|
+
send(
|
|
63
|
+
res,
|
|
64
|
+
503,
|
|
65
|
+
'Moment SDK build not found in this package. Reinstall @momentco-ai/moment-sdk.',
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
70
|
+
createReadStream(sdkPath).pipe(res);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (pathname === '/package-meta.json') {
|
|
75
|
+
send(
|
|
76
|
+
res,
|
|
77
|
+
200,
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
name: packageJson.name,
|
|
80
|
+
version: packageJson.version,
|
|
81
|
+
cdnUrl: `https://cdn.jsdelivr.net/npm/${packageJson.name}@${packageJson.version}`,
|
|
82
|
+
}),
|
|
83
|
+
'application/json; charset=utf-8',
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ROUTES[pathname]) {
|
|
89
|
+
if (serveFile(res, ROUTES[pathname])) return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const relative = pathname.replace(/^\//, '');
|
|
93
|
+
if (relative && serveFile(res, relative)) return;
|
|
94
|
+
|
|
95
|
+
send(res, 404, 'Not found');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
server.listen(PORT, () => {
|
|
99
|
+
console.log(`Moment SDK examples → http://localhost:${PORT}`);
|
|
100
|
+
console.log(`Package ${packageJson.name}@${packageJson.version}`);
|
|
101
|
+
if (!existsSync(sdkPath)) {
|
|
102
|
+
console.warn('Warning: dist/moment-sdk.js missing — triggers will not load the SDK.');
|
|
103
|
+
}
|
|
104
|
+
});
|