@kaena1/ecomm-contained-utils 1.0.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/README.md ADDED
@@ -0,0 +1,681 @@
1
+ # ecomm-contained-utils
2
+
3
+ Shared utilities for loading and managing remote React components in Mint Mobile microfrontends.
4
+
5
+ ## Package Overview
6
+
7
+ This package provides the essential infrastructure for dynamically loading remote React components (contained components) from external URLs into Next.js applications. It handles module loading, dependency resolution via import maps, and component rendering.
8
+
9
+ ### Key Features
10
+
11
+ - **Dynamic Component Loading**: Load React components from remote URLs at runtime
12
+ - **Import Map Management**: Automatic dependency resolution for React and React DOM
13
+ - **Type-Safe**: Full TypeScript support with generated type definitions
14
+ - **Optimized for CDN**: Preserves CDN cache and TTL through native ES modules
15
+ - **Caching**: Built-in module caching to avoid redundant network requests
16
+
17
+ ## Package Structure
18
+
19
+ ```
20
+ packages/ecomm-contained-utils/
21
+ ├── src/
22
+ │ ├── components/ # React components
23
+ │ │ ├── ImportMapScripts.tsx # Injects import map for dependency resolution
24
+ │ │ ├── RemoteLinks.tsx # Preloads remote component resources
25
+ │ │ └── RemoteRenderer.tsx # Renders remote components ('use client')
26
+ │ ├── hooks/ # React hooks
27
+ │ │ └── useRemoteComponent.ts # Hook for loading remote components ('use client')
28
+ │ ├── services/ # Core services
29
+ │ │ ├── fetchManifest.ts # Fetches component manifests
30
+ │ │ ├── ImportMapService.ts # Generates import map JSON
31
+ │ │ └── moduleLoader.ts # ES module loader service
32
+ │ ├── types/ # TypeScript definitions
33
+ │ ├── index.ts # Main entry point (exports everything)
34
+ │ ├── server.ts # Server-safe exports for App Router
35
+ │ └── client.ts # Client-only exports for App Router
36
+ ├── dist/ # Built files (generated)
37
+ ├── vite.config.ts # Build configuration
38
+ ├── package.json
39
+ ├── release.config.cjs # Semantic Release configuration
40
+ └── README.md
41
+ ```
42
+
43
+ ### Entry Points
44
+
45
+ The package provides three entry points for different use cases:
46
+
47
+ | Entry Point | Import Path | Use Case |
48
+ |-------------|-------------|----------|
49
+ | Main | `ecomm-contained-utils` | Everything (Pages Router, or when you need all exports) |
50
+ | Server | `ecomm-contained-utils/server` | Server Components (App Router RSC) |
51
+ | Client | `ecomm-contained-utils/client` | Client Components (App Router) |
52
+
53
+ **Server exports:** `fetchManifest`, `ImportMapScripts`, `RemoteLinks`, `generateImportMap`, `generateImportMapJson`, all types
54
+
55
+ **Client exports:** `RemoteRenderer`, `useRemoteComponent`, `moduleLoader`, `ModuleLoader`
56
+
57
+ ## Installation
58
+
59
+ ### In Consumer Applications (e.g., mm-mf-features)
60
+
61
+ #### Option 1: Install Latest from POC (Recommended)
62
+
63
+ Always get the latest version from the POC branch:
64
+
65
+ ```bash
66
+ npm install ecomm-contained-utils@f-poc-EC-3301
67
+ ```
68
+
69
+ This installs the latest version tagged with `f-poc-EC-3301`. You don't need to track specific version numbers.
70
+
71
+ #### Option 2: Install Specific Version
72
+
73
+ If you need a specific version:
74
+
75
+ ```bash
76
+ npm install ecomm-contained-utils@1.25.0-f-poc-EC-3301.5
77
+ ```
78
+
79
+ #### Updating to Latest
80
+
81
+ To update to the latest POC version:
82
+
83
+ ```bash
84
+ # Update and save to package.json
85
+ npm install ecomm-contained-utils@f-poc-EC-3301 --save
86
+
87
+ # Or use npm update (if version is already in package.json with the tag)
88
+ npm update ecomm-contained-utils
89
+ ```
90
+
91
+ #### For Local Development
92
+
93
+ **⚠️ Important: Use npm pack instead of npm link**
94
+
95
+ Due to React's requirement of a single instance, `npm link` causes "Invalid hook call" errors. Use `npm pack` instead:
96
+
97
+ ```bash
98
+ # In the ecomm-contained-utils package
99
+ cd packages/ecomm-contained-utils
100
+ npm run build
101
+ npm pack
102
+ # This generates: ecomm-contained-utils-X.X.X.tgz
103
+
104
+ # In the consumer application (mm-mf-features)
105
+ npm install /absolute/path/to/ecomm-contained-utils-X.X.X.tgz
106
+ ```
107
+
108
+ ## Usage
109
+
110
+ ### 1. Setup in Next.js Application
111
+
112
+ The setup differs slightly between Next.js Pages Router and App Router.
113
+
114
+ #### Pages Router Setup
115
+
116
+ ##### Add Import Map Scripts to `_document.tsx`
117
+
118
+ ```typescript
119
+ import { Html, Head, Main, NextScript } from 'next/document';
120
+ import { ImportMapScripts } from 'ecomm-contained-utils';
121
+
122
+ export default function Document() {
123
+ return (
124
+ <Html lang="en-US">
125
+ <Head>
126
+ {/* Required: Enables import map for dependency resolution */}
127
+ <ImportMapScripts />
128
+ </Head>
129
+ <body>
130
+ <Main />
131
+ <NextScript />
132
+ </body>
133
+ </Html>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ##### Fetch Remote Manifest in `_app.tsx`
139
+
140
+ ```typescript
141
+ import type { AppProps, AppContext } from 'next/app';
142
+ import {
143
+ RemoteRenderer,
144
+ RemoteLinks,
145
+ fetchManifest,
146
+ } from 'ecomm-contained-utils';
147
+
148
+ App.getInitialProps = async (ctx: AppContext) => {
149
+ const manifestUrl = `https://assets-qa.mintmobile.com/contained-components/manifest-poc/f-poc-live-update/manifest.json?_ts=${Date.now()}`;
150
+
151
+ let remoteManifest;
152
+ try {
153
+ remoteManifest = await fetchManifest(manifestUrl);
154
+ } catch (e) {
155
+ console.error('Failed to fetch manifest:', e);
156
+ remoteManifest = null;
157
+ }
158
+
159
+ const app = await (await import('next/app')).default.getInitialProps(ctx);
160
+ return {
161
+ ...app,
162
+ pageProps: {
163
+ ...app.pageProps,
164
+ remoteManifest,
165
+ },
166
+ };
167
+ };
168
+
169
+ export default function App({ Component, pageProps }: AppProps) {
170
+ const remoteManifest = pageProps?.remoteManifest;
171
+ const layoutManifest = remoteManifest?.components?.MintLayout;
172
+
173
+ return (
174
+ <>
175
+ <Head>
176
+ {/* Preload component resources */}
177
+ {layoutManifest && <RemoteLinks manifest={layoutManifest} />}
178
+ </Head>
179
+
180
+ {/* Render remote component as wrapper */}
181
+ {layoutManifest ? (
182
+ <RemoteRenderer
183
+ manifest={layoutManifest}
184
+ componentName="MintLayout"
185
+ autoLoad={true}
186
+ componentProps={{
187
+ config: { NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL },
188
+ }}
189
+ >
190
+ <Component {...pageProps} />
191
+ </RemoteRenderer>
192
+ ) : (
193
+ <Component {...pageProps} />
194
+ )}
195
+ </>
196
+ );
197
+ }
198
+ ```
199
+
200
+ #### App Router Setup
201
+
202
+ > **Note:** App Router is fully supported. The package provides separate entry points for server and client code:
203
+ > - `ecomm-contained-utils/server` - Server-safe exports (fetchManifest, ImportMapScripts, RemoteLinks, types)
204
+ > - `ecomm-contained-utils/client` - Client-only exports (RemoteRenderer, useRemoteComponent, moduleLoader)
205
+
206
+ ##### Add Import Map Scripts to `app/layout.tsx`
207
+
208
+ ```typescript
209
+ // Use /server for server components
210
+ import {
211
+ ImportMapScripts,
212
+ RemoteLinks,
213
+ fetchManifest
214
+ } from 'ecomm-contained-utils/server';
215
+
216
+ // Use /client for client components (in separate 'use client' files)
217
+ // import { RemoteRenderer } from 'ecomm-contained-utils/client';
218
+
219
+ // Fetch manifest at build time
220
+ async function getRemoteManifest() {
221
+ try {
222
+ const manifestUrl = `https://assets-qa.mintmobile.com/contained-components/manifest-poc/f-poc-live-update/manifest.json?_ts=${Date.now()}`;
223
+ return await fetchManifest(manifestUrl);
224
+ } catch (e) {
225
+ console.error('Failed to fetch manifest:', e);
226
+ return null;
227
+ }
228
+ }
229
+
230
+ export default async function RootLayout({
231
+ children,
232
+ }: {
233
+ children: React.ReactNode;
234
+ }) {
235
+ const remoteManifest = await getRemoteManifest();
236
+ const layoutManifest = remoteManifest?.components?.MintLayout;
237
+
238
+ return (
239
+ <html lang="en-US">
240
+ <head>
241
+ {/* Required: Enables import map for dependency resolution */}
242
+ <ImportMapScripts />
243
+
244
+ {/* Preload component resources */}
245
+ {layoutManifest && <RemoteLinks manifest={layoutManifest} />}
246
+ </head>
247
+ <body>
248
+ {layoutManifest ? (
249
+ <RemoteRenderer
250
+ manifest={layoutManifest}
251
+ componentName="MintLayout"
252
+ autoLoad={true}
253
+ componentProps={{
254
+ config: { NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL },
255
+ }}
256
+ >
257
+ {children}
258
+ </RemoteRenderer>
259
+ ) : (
260
+ children
261
+ )}
262
+ </body>
263
+ </html>
264
+ );
265
+ }
266
+ ```
267
+
268
+ ### 2. Using Remote Components in Pages
269
+
270
+ ```typescript
271
+ import { RemoteRenderer } from 'ecomm-contained-utils';
272
+
273
+ export default function MyPage({ remoteManifest }) {
274
+ const pocComponentManifest = remoteManifest?.components?.PocComponent;
275
+
276
+ return (
277
+ <div>
278
+ <h1>My Page</h1>
279
+
280
+ {pocComponentManifest && (
281
+ <RemoteRenderer
282
+ manifest={pocComponentManifest}
283
+ componentName="PocComponent"
284
+ autoLoad={true}
285
+ componentProps={{
286
+ list: [
287
+ { id: 1, name: 'Item 1' },
288
+ { id: 2, name: 'Item 2' },
289
+ ],
290
+ }}
291
+ />
292
+ )}
293
+ </div>
294
+ );
295
+ }
296
+ ```
297
+
298
+ ## API Reference
299
+
300
+ ### Components
301
+
302
+ #### `<ImportMapScripts />`
303
+
304
+ Injects the import map script tags required for ES module dependency resolution.
305
+
306
+ **Props:** None
307
+
308
+ **Usage:**
309
+
310
+ ```tsx
311
+ <Head>
312
+ <ImportMapScripts />
313
+ </Head>
314
+ ```
315
+
316
+ ---
317
+
318
+ #### `<RemoteLinks manifest={ComponentManifest} />`
319
+
320
+ Preloads CSS and JS resources for a remote component.
321
+
322
+ **Props:**
323
+
324
+ - `manifest`: `ComponentManifest` - Component manifest with `bundleUrl` and optional `styleUrl`
325
+
326
+ **Usage:**
327
+
328
+ ```tsx
329
+ <RemoteLinks manifest={pocComponentManifest} />
330
+ ```
331
+
332
+ ---
333
+
334
+ #### `<RemoteRenderer />`
335
+
336
+ Renders a remote React component.
337
+
338
+ **Props:**
339
+
340
+ - `manifest`: `ComponentManifest` - Component manifest
341
+ - `componentName`: `string` - Name of the component to extract from the module
342
+ - `componentProps?`: `Record<string, unknown>` - Props to pass to the remote component
343
+ - `autoLoad?`: `boolean` - Whether to auto-load on mount (default: `true`)
344
+ - `placeholder?`: `ReactNode` - Content to show while loading
345
+ - `errorFallback?`: `(error: string) => ReactNode` - Custom error UI
346
+ - `children?`: `ReactNode` - Children to pass to the remote component
347
+
348
+ **Usage:**
349
+
350
+ ```tsx
351
+ <RemoteRenderer
352
+ manifest={manifest}
353
+ componentName="MyComponent"
354
+ autoLoad={true}
355
+ componentProps={{ title: 'Hello' }}
356
+ placeholder={<div>Loading...</div>}
357
+ errorFallback={(error) => <div>Error: {error}</div>}
358
+ >
359
+ <ChildComponent />
360
+ </RemoteRenderer>
361
+ ```
362
+
363
+ ---
364
+
365
+ ### Hooks
366
+
367
+ #### `useRemoteComponent(options)`
368
+
369
+ Hook for loading remote components with manual control.
370
+
371
+ **Parameters:**
372
+
373
+ - `options.manifest`: `ComponentManifest` - Component manifest
374
+ - `options.componentName`: `string` - Component name
375
+ - `options.autoLoad?`: `boolean` - Auto-load on mount (default: `true`)
376
+
377
+ **Returns:**
378
+
379
+ ```typescript
380
+ {
381
+ component: React.ComponentType | null,
382
+ manifest: ComponentManifest | null,
383
+ loading: boolean,
384
+ error: string | null,
385
+ loaded: boolean,
386
+ loadComponent: () => Promise<void>,
387
+ reload: () => Promise<void>,
388
+ clearError: () => void
389
+ }
390
+ ```
391
+
392
+ **Usage:**
393
+
394
+ ```tsx
395
+ const {
396
+ component: Component,
397
+ loading,
398
+ error,
399
+ } = useRemoteComponent({
400
+ manifest: pocManifest,
401
+ componentName: 'PocComponent',
402
+ autoLoad: true,
403
+ });
404
+
405
+ if (loading) return <div>Loading...</div>;
406
+ if (error) return <div>Error: {error}</div>;
407
+ if (!Component) return null;
408
+
409
+ return <Component {...props} />;
410
+ ```
411
+
412
+ ---
413
+
414
+ ### Services
415
+
416
+ #### `fetchManifest(manifestUrl: string): Promise<RemoteManifest>`
417
+
418
+ Fetches and validates a remote component manifest.
419
+
420
+ **Parameters:**
421
+
422
+ - `manifestUrl`: `string` - URL to the manifest JSON file
423
+
424
+ **Returns:** `Promise<RemoteManifest>`
425
+
426
+ **Usage:**
427
+
428
+ ```typescript
429
+ const manifest = await fetchManifest(
430
+ 'https://assets-qa.mintmobile.com/.../manifest.json',
431
+ );
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Build & Development
437
+
438
+ ### Build the Package
439
+
440
+ ```bash
441
+ # Production build (minified)
442
+ npm run build
443
+
444
+ # Development build (unminified, preserves modules)
445
+ npm run build:debug
446
+
447
+ # Watch mode (rebuilds on changes)
448
+ npm run dev
449
+ ```
450
+
451
+ ### Type Checking & Linting
452
+
453
+ ```bash
454
+ # Type check
455
+ npm run typecheck
456
+
457
+ # Lint
458
+ npm run lint
459
+ ```
460
+
461
+ ## Publishing
462
+
463
+ ### Current State (Manual Publishing)
464
+
465
+ **⚠️ Automatic npm publishing via semantic-release is currently disabled** because the NPM_TOKEN is not configured in Bitbucket Pipelines.
466
+
467
+ #### Manual Publishing Process
468
+
469
+ 1. **Update version in `package.json`:**
470
+
471
+ ```json
472
+ {
473
+ "version": "1.25.0-f-poc-EC-3301.5"
474
+ }
475
+ ```
476
+
477
+ 2. **Build the package:**
478
+
479
+ ```bash
480
+ npm run build
481
+ ```
482
+
483
+ 3. **Publish to npm:**
484
+
485
+ ```bash
486
+ npm publish --tag f-poc-live-update
487
+ ```
488
+
489
+ 4. **Install in consumer apps:**
490
+
491
+ ```bash
492
+ # Specific version
493
+ npm install ecomm-contained-utils@1.25.0-f-poc-EC-3301.5
494
+
495
+ # Or use the tag for latest POC version
496
+ npm install ecomm-contained-utils@f-poc-live-update
497
+ ```
498
+
499
+ ### Enabling Automatic Publishing (Future)
500
+
501
+ To enable automatic npm publishing via semantic-release:
502
+
503
+ 1. **Create an Automation token**
504
+ 2. **Add token to Bitbucket:**
505
+ 3. **Uncomment `@semantic-release/npm` in `release.config.cjs`:**
506
+
507
+ ```javascript
508
+ '@semantic-release/npm', // ← Uncomment this line
509
+ ```
510
+
511
+ 4. **Commit and push changes** - semantic-release will now automatically publish on merge.
512
+
513
+ ## Versioning & Branching
514
+
515
+ ### Semantic Release Configuration
516
+
517
+ The package uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated versioning based on commit messages.
518
+
519
+ #### Supported Branches
520
+
521
+ | Branch Pattern | Prerelease Tag | Use Case |
522
+ | -------------- | -------------- | -------------------- |
523
+ | `master` | None | Production releases |
524
+ | `develop` | `true` | QA/Staging releases |
525
+ | `f-poc-*` | Branch name | POC/Feature releases |
526
+
527
+ #### Commit Message Format
528
+
529
+ Follow [Conventional Commits](https://www.conventionalcommits.org/):
530
+
531
+ ```
532
+ <type>(<scope>): <description>
533
+
534
+ [optional body]
535
+
536
+ [optional footer]
537
+ ```
538
+
539
+ **Examples:**
540
+
541
+ ```bash
542
+ feat(ecomm-contained-utils): add new remote component loader
543
+ fix(ecomm-contained-utils): resolve React dependency conflicts
544
+ docs: update README with usage examples
545
+ ```
546
+
547
+ #### Release Rules
548
+
549
+ | Commit Type | Scope | Release Type |
550
+ | ----------- | -------------- | ------------- |
551
+ | `feat` | `ecomm-contained-utils` | Minor (1.x.0) |
552
+ | `fix` | `ecomm-contained-utils` | Patch (1.0.x) |
553
+ | `feat` | Any | Minor |
554
+ | `fix` | Any | Patch |
555
+
556
+ ### POC Branch Strategy
557
+
558
+ For the POC, we use the branch pattern `f-poc-*` with a specific prerelease identifier.
559
+
560
+ **Current setup:**
561
+
562
+ ```javascript
563
+ {
564
+ name: 'f-poc-*',
565
+ prerelease: '${name}', // Uses full branch name as tag
566
+ }
567
+ ```
568
+
569
+ This means:
570
+
571
+ - Branch `f-poc-EC-3301` → publishes as `1.25.0-f-poc-EC-3301.1`
572
+ - Branch `f-poc-live-update` → publishes as `1.25.0-f-poc-live-update.1`
573
+
574
+ ## How It Works
575
+
576
+ ### Architecture Overview
577
+
578
+ ```
579
+ ┌─────────────────────────────────────────────────────────────┐
580
+ │ Consumer App (Next.js) │
581
+ │ ┌────────────────────────────────────────────────────────┐ │
582
+ │ │ _document.tsx: <ImportMapScripts /> │ │
583
+ │ │ └─> Injects import map for React dependency resolution│ │
584
+ │ └────────────────────────────────────────────────────────┘ │
585
+ │ ┌────────────────────────────────────────────────────────┐ │
586
+ │ │ _app.tsx: │ │
587
+ │ │ 1. fetchManifest() - Gets component URLs │ │
588
+ │ │ 2. <RemoteLinks /> - Preloads resources │ │
589
+ │ │ 3. <RemoteRenderer /> - Loads & renders component │ │
590
+ │ └────────────────────────────────────────────────────────┘ │
591
+ └─────────────────────────────────────────────────────────────┘
592
+
593
+ ┌─────────────────────────────────┐
594
+ │ ModuleLoader (ecomm-contained-utils) │
595
+ │ 1. Checks cache │
596
+ │ 2. Loads ES module via script │
597
+ │ 3. Extracts component │
598
+ │ 4. Returns React component │
599
+ └─────────────────────────────────┘
600
+
601
+ ┌─────────────────────────────────┐
602
+ │ Remote Component (S3/CDN) │
603
+ │ • MintLayout │
604
+ │ • PocComponent │
605
+ │ • TestComponent │
606
+ │ etc. │
607
+ └─────────────────────────────────┘
608
+ ```
609
+
610
+ ### Component Manifest Format
611
+
612
+ Remote components are described by a manifest JSON file:
613
+
614
+ ```json
615
+ {
616
+ "components": {
617
+ "MintLayout": {
618
+ "bundleUrl": "https://.../MintLayout.abc123.es.js",
619
+ "styleUrl": "https://.../MintLayout.def456.css"
620
+ },
621
+ "PocComponent": {
622
+ "bundleUrl": "https://.../PocComponent.xyz789.es.js",
623
+ "styleUrl": "https://.../PocComponent.uvw012.css"
624
+ }
625
+ },
626
+ "version": "0.1.0",
627
+ "generatedAt": "2026-01-20T12:00:00.000Z"
628
+ }
629
+ ```
630
+
631
+ ### Import Map Resolution
632
+
633
+ The `ImportMapScripts` component injects an import map that resolves bare specifiers to use the **host app's React instance**:
634
+
635
+ ```html
636
+ <script type="importmap-shim">
637
+ {
638
+ "imports": {
639
+ "react": "data:text/javascript,...(uses window.React)",
640
+ "react/jsx-runtime": "data:text/javascript,...(uses window.React.createElement)",
641
+ "react-dom": "https://esm.sh/react-dom@18.3.1",
642
+ "react-dom/client": "https://esm.sh/react-dom@18.3.1/client"
643
+ }
644
+ }
645
+ </script>
646
+ ```
647
+
648
+ **Key design:** Both `react` and `react/jsx-runtime` are mapped to use `window.React`, ensuring remote components use the exact same React instance as the host app. This prevents "multiple copies of React" errors.
649
+
650
+ ### Module Loading Flow
651
+
652
+ 1. **Component Manifest** is fetched via `fetchManifest()`
653
+ 2. **ModuleLoader** dynamically imports the ES module from `bundleUrl`
654
+ 3. **Component extraction** finds the named export or default export
655
+ 4. **React component** is returned and rendered via `<RemoteRenderer>`
656
+
657
+ ## CI/CD Pipeline
658
+
659
+ ### Bitbucket Pipelines Configuration
660
+
661
+ The package is built and deployed via Bitbucket Pipelines (`bitbucket-pipelines.yml` in repo root).
662
+
663
+ #### Pipeline Stages
664
+
665
+ **For `f-poc-*` branches:**
666
+
667
+ ```yaml
668
+ - stage: <EcommContainedUtils> - POC
669
+ condition:
670
+ changesets:
671
+ includePaths:
672
+ - packages/ecomm-contained-utils/**
673
+ steps:
674
+ - Lint & Build
675
+ - Deploy (currently disabled, manual publishing)
676
+ ```
677
+
678
+ **Triggers:**
679
+
680
+ - Runs when files in `packages/ecomm-contained-utils/**` are modified
681
+ - Pushes to any branch matching `f-poc-*` pattern