@kaena1/ecomm-remote-utils 1.0.0-f-EC-4239.1
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 +833 -0
- package/dist/client.d.ts +144 -0
- package/dist/client.js +12 -0
- package/dist/components/ImportMapScripts.js +51 -0
- package/dist/components/RemoteComponentsProvider.js +13 -0
- package/dist/components/RemoteLinks.js +24 -0
- package/dist/components/RemoteRenderer.js +72 -0
- package/dist/hooks/useRemoteComponent.js +59 -0
- package/dist/lib/api/server/getRemoteManifest/getRemoteManifest.js +25 -0
- package/dist/lib/mappers/mapManifestUrl.js +27 -0
- package/dist/lib/utils/getModulePreloadKey.js +9 -0
- package/dist/lib/utils/getRemoteComponentsData.js +17 -0
- package/dist/lib/utils/withRemoteComponentsGSSP.js +20 -0
- package/dist/node_modules/es-module-shims/dist/es-module-shims.js +1487 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.js +10 -0
- package/dist/services/ImportMapService.js +49 -0
- package/dist/services/fetchManifest.js +26 -0
- package/dist/services/moduleLoader.js +139 -0
- package/dist/types/environment.js +7 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
# ecomm-remote-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-remote-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
|
+
│ ├── lib/ # Business logic
|
|
29
|
+
│ │ ├── api/server/ # Server-side API functions
|
|
30
|
+
│ │ │ └── getMintLayoutProps/ # Fetches MintLayout props from ecomm API
|
|
31
|
+
│ │ └── mappers/ # Data mappers
|
|
32
|
+
│ │ └── mapMintLayoutResponseToProps.ts
|
|
33
|
+
│ ├── services/ # Core services
|
|
34
|
+
│ │ ├── fetchManifest.ts # Fetches component manifests
|
|
35
|
+
│ │ ├── ImportMapService.ts # Generates import map JSON
|
|
36
|
+
│ │ └── moduleLoader.ts # ES module loader service
|
|
37
|
+
│ ├── types/ # TypeScript definitions
|
|
38
|
+
│ ├── server.ts # Server-safe exports for App Router
|
|
39
|
+
│ ├── client.ts # Client-only exports for App Router
|
|
40
|
+
│ └── types.ts # Type-only exports entry point
|
|
41
|
+
├── dist/ # Built files (generated)
|
|
42
|
+
├── vite.config.ts # Build configuration
|
|
43
|
+
├── package.json
|
|
44
|
+
├── release.config.cjs # Semantic Release configuration
|
|
45
|
+
└── README.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Entry Points
|
|
49
|
+
|
|
50
|
+
The package provides three entry points for different use cases:
|
|
51
|
+
|
|
52
|
+
| Entry Point | Import Path | Use Case |
|
|
53
|
+
| ----------- | ----------------------------------- | ---------------------------------------------------- |
|
|
54
|
+
| Server | `@kaena1/ecomm-remote-utils/server` | Server Components (App Router RSC, Pages Router SSR) |
|
|
55
|
+
| Client | `@kaena1/ecomm-remote-utils/client` | Client Components (App Router, Pages Router CSR) |
|
|
56
|
+
| Types | `@kaena1/ecomm-remote-utils/types` | Type-only imports |
|
|
57
|
+
|
|
58
|
+
**Server exports:** `fetchManifest`, `getRemoteManifest`, `ImportMapScripts`, `RemoteLinks`, `generateImportMap`, `generateImportMapJson`, `getMintLayoutProps`, `mapMintLayoutResponseToProps`
|
|
59
|
+
|
|
60
|
+
**Client exports:** `RemoteRenderer`, `useRemoteComponent`, `moduleLoader`, `ModuleLoader`, `MintLayout`
|
|
61
|
+
|
|
62
|
+
**Types exports:** `ComponentManifest`, `RemoteManifest`, `MintLayoutProps`, `MintLayoutPropsAPIResponse`, WP REST API types
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
### In Consumer Applications (e.g., mm-mf-features)
|
|
67
|
+
|
|
68
|
+
#### Install Latest Stable
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @kaena1/ecomm-remote-utils
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Install Latest from a POC Branch
|
|
75
|
+
|
|
76
|
+
Each `f-poc-*` branch publishes a prerelease automatically. Use the branch name as the tag:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install @kaena1/ecomm-remote-utils@f-poc-ec-3644
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
You don't need to track specific version numbers — the tag always points to the latest prerelease from that branch.
|
|
83
|
+
|
|
84
|
+
#### Install Latest Beta (post-POC, from develop)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install @kaena1/ecomm-remote-utils@beta
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Install Specific Version
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install @kaena1/ecomm-remote-utils@1.0.5
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Updating to Latest
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm update @kaena1/ecomm-remote-utils
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### For Local Development
|
|
103
|
+
|
|
104
|
+
**⚠️ Important: Use npm pack instead of npm link**
|
|
105
|
+
|
|
106
|
+
Due to React's requirement of a single instance, `npm link` causes "Invalid hook call" errors. Use `npm pack` instead:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# In the ecomm-remote-utils package
|
|
110
|
+
cd packages/ecomm-remote-utils
|
|
111
|
+
npm run build
|
|
112
|
+
npm pack
|
|
113
|
+
# This generates: kaena1-ecomm-remote-utils-X.X.X.tgz
|
|
114
|
+
|
|
115
|
+
# In the consumer application (mm-mf-features)
|
|
116
|
+
npm install /absolute/path/to/kaena1-ecomm-remote-utils-X.X.X.tgz
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Usage
|
|
120
|
+
|
|
121
|
+
### 1. Setup in Next.js Application
|
|
122
|
+
|
|
123
|
+
The setup differs slightly between Next.js Pages Router and App Router.
|
|
124
|
+
|
|
125
|
+
#### Pages Router Setup
|
|
126
|
+
|
|
127
|
+
> **Recommended approach:** Use [`withRemoteComponentsGSSP`](#withremotecomponentsgsspenvvars-pagegssP-getserversideprops) in each page's `getServerSideProps`. The `getInitialProps` pattern below is kept for reference but `withRemoteComponentsGSSP` is the mandatory pattern for new pages.
|
|
128
|
+
|
|
129
|
+
##### Add Import Map Scripts to `_document.tsx`
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Html, Head, Main, NextScript } from 'next/document';
|
|
133
|
+
import { ImportMapScripts } from '@kaena1/ecomm-remote-utils/server';
|
|
134
|
+
|
|
135
|
+
export default function Document() {
|
|
136
|
+
return (
|
|
137
|
+
<Html lang="en-US">
|
|
138
|
+
<Head>
|
|
139
|
+
{/* Required: Enables import map for dependency resolution */}
|
|
140
|
+
<ImportMapScripts />
|
|
141
|
+
</Head>
|
|
142
|
+
<body>
|
|
143
|
+
<Main />
|
|
144
|
+
<NextScript />
|
|
145
|
+
</body>
|
|
146
|
+
</Html>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
##### Fetch Remote Manifest in `_app.tsx`
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import type { AppProps, AppContext } from 'next/app';
|
|
155
|
+
import { RemoteLinks, getRemoteManifest } from '@kaena1/ecomm-remote-utils/server';
|
|
156
|
+
import { RemoteRenderer } from '@kaena1/ecomm-remote-utils/client';
|
|
157
|
+
|
|
158
|
+
App.getInitialProps = async (ctx: AppContext) => {
|
|
159
|
+
let remoteManifest;
|
|
160
|
+
try {
|
|
161
|
+
remoteManifest = await getRemoteManifest(process.env.MANIFEST_URL);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error('Failed to fetch manifest:', e);
|
|
164
|
+
remoteManifest = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const app = await (await import('next/app')).default.getInitialProps(ctx);
|
|
168
|
+
return {
|
|
169
|
+
...app,
|
|
170
|
+
pageProps: {
|
|
171
|
+
...app.pageProps,
|
|
172
|
+
remoteManifest,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
178
|
+
const remoteManifest = pageProps?.remoteManifest;
|
|
179
|
+
const layoutManifest = remoteManifest?.components?.MintLayout;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<Head>
|
|
184
|
+
{/* Preload component resources */}
|
|
185
|
+
{layoutManifest && <RemoteLinks manifest={layoutManifest} />}
|
|
186
|
+
</Head>
|
|
187
|
+
|
|
188
|
+
{/* Render remote component as wrapper */}
|
|
189
|
+
{layoutManifest ? (
|
|
190
|
+
<RemoteRenderer
|
|
191
|
+
manifest={layoutManifest}
|
|
192
|
+
componentName="MintLayout"
|
|
193
|
+
autoLoad={true}
|
|
194
|
+
componentProps={{
|
|
195
|
+
config: { NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL },
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<Component {...pageProps} />
|
|
199
|
+
</RemoteRenderer>
|
|
200
|
+
) : (
|
|
201
|
+
<Component {...pageProps} />
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### App Router Setup
|
|
209
|
+
|
|
210
|
+
> **Note:** App Router is fully supported. The package provides separate entry points for server and client code:
|
|
211
|
+
>
|
|
212
|
+
> - `@kaena1/ecomm-remote-utils/server` - Server-safe exports (fetchManifest, getRemoteManifest, ImportMapScripts, RemoteLinks, getMintLayoutProps, mapMintLayoutResponseToProps)
|
|
213
|
+
> - `@kaena1/ecomm-remote-utils/client` - Client-only exports (RemoteRenderer, useRemoteComponent, moduleLoader, MintLayout)
|
|
214
|
+
> - `@kaena1/ecomm-remote-utils/types` - Type-only exports
|
|
215
|
+
|
|
216
|
+
> **MintLayout styles:** Importing `@kaena1/ecomm-remote-utils/client` now auto-injects package-owned MintLayout CSS at runtime in the browser. Consumers should not manually import `dist/style.css`.
|
|
217
|
+
|
|
218
|
+
##### Add Import Map Scripts to `app/layout.tsx`
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Use /server for server components
|
|
222
|
+
import {
|
|
223
|
+
ImportMapScripts,
|
|
224
|
+
RemoteLinks,
|
|
225
|
+
getRemoteManifest,
|
|
226
|
+
} from '@kaena1/ecomm-remote-utils/server';
|
|
227
|
+
|
|
228
|
+
// Use /client for client components (in separate 'use client' files)
|
|
229
|
+
// import { RemoteRenderer } from '@kaena1/ecomm-remote-utils/client';
|
|
230
|
+
|
|
231
|
+
export default async function RootLayout({
|
|
232
|
+
children,
|
|
233
|
+
}: {
|
|
234
|
+
children: React.ReactNode;
|
|
235
|
+
}) {
|
|
236
|
+
const remoteManifest = await getRemoteManifest(process.env.MANIFEST_URL);
|
|
237
|
+
const layoutManifest = remoteManifest?.components?.MintLayout;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<html lang="en-US">
|
|
241
|
+
<head>
|
|
242
|
+
{/* Required: Enables import map for dependency resolution */}
|
|
243
|
+
<ImportMapScripts />
|
|
244
|
+
|
|
245
|
+
{/* Preload component resources */}
|
|
246
|
+
{layoutManifest && <RemoteLinks manifest={layoutManifest} />}
|
|
247
|
+
</head>
|
|
248
|
+
<body>
|
|
249
|
+
{layoutManifest ? (
|
|
250
|
+
<RemoteRenderer
|
|
251
|
+
manifest={layoutManifest}
|
|
252
|
+
componentName="MintLayout"
|
|
253
|
+
autoLoad={true}
|
|
254
|
+
componentProps={{
|
|
255
|
+
config: { NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL },
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
{children}
|
|
259
|
+
</RemoteRenderer>
|
|
260
|
+
) : (
|
|
261
|
+
children
|
|
262
|
+
)}
|
|
263
|
+
</body>
|
|
264
|
+
</html>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 2. Using Remote Components in Pages
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { RemoteRenderer } from '@kaena1/ecomm-remote-utils/client';
|
|
273
|
+
|
|
274
|
+
export default function MyPage({ remoteManifest }) {
|
|
275
|
+
const pocComponentManifest = remoteManifest?.components?.PocComponent;
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div>
|
|
279
|
+
<h1>My Page</h1>
|
|
280
|
+
|
|
281
|
+
{pocComponentManifest && (
|
|
282
|
+
<RemoteRenderer
|
|
283
|
+
manifest={pocComponentManifest}
|
|
284
|
+
componentName="PocComponent"
|
|
285
|
+
autoLoad={true}
|
|
286
|
+
componentProps={{
|
|
287
|
+
list: [
|
|
288
|
+
{ id: 1, name: 'Item 1' },
|
|
289
|
+
{ id: 2, name: 'Item 2' },
|
|
290
|
+
],
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## API Reference
|
|
300
|
+
|
|
301
|
+
### Components
|
|
302
|
+
|
|
303
|
+
#### `<ImportMapScripts />`
|
|
304
|
+
|
|
305
|
+
Injects the import map script tags required for ES module dependency resolution.
|
|
306
|
+
|
|
307
|
+
**Props:** None
|
|
308
|
+
|
|
309
|
+
**Usage:**
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
<Head>
|
|
313
|
+
<ImportMapScripts />
|
|
314
|
+
</Head>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
#### `<RemoteLinks manifest={ComponentManifest} />`
|
|
320
|
+
|
|
321
|
+
Preloads CSS and JS resources for a remote component.
|
|
322
|
+
|
|
323
|
+
**Props:**
|
|
324
|
+
|
|
325
|
+
- `manifest`: `ComponentManifest` - Component manifest with `bundleUrl` and optional `styleUrl`
|
|
326
|
+
|
|
327
|
+
**Usage:**
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
<RemoteLinks manifest={pocComponentManifest} />
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
#### `<RemoteRenderer />`
|
|
336
|
+
|
|
337
|
+
Renders a remote React component.
|
|
338
|
+
|
|
339
|
+
**Props:**
|
|
340
|
+
|
|
341
|
+
- `manifest`: `ComponentManifest` - Component manifest
|
|
342
|
+
- `componentName`: `string` - Name of the component to extract from the module
|
|
343
|
+
- `componentProps?`: `Record<string, unknown>` - Props to pass to the remote component
|
|
344
|
+
- `autoLoad?`: `boolean` - Whether to auto-load on mount (default: `true`)
|
|
345
|
+
- `placeholder?`: `ReactNode` - Content to show while loading
|
|
346
|
+
- `errorFallback?`: `(error: string) => ReactNode` - Custom error UI
|
|
347
|
+
- `children?`: `ReactNode` - Children to pass to the remote component
|
|
348
|
+
|
|
349
|
+
**Usage:**
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
<RemoteRenderer
|
|
353
|
+
manifest={manifest}
|
|
354
|
+
componentName="MyComponent"
|
|
355
|
+
autoLoad={true}
|
|
356
|
+
componentProps={{ title: 'Hello' }}
|
|
357
|
+
placeholder={<div>Loading...</div>}
|
|
358
|
+
errorFallback={(error) => <div>Error: {error}</div>}
|
|
359
|
+
>
|
|
360
|
+
<ChildComponent />
|
|
361
|
+
</RemoteRenderer>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### Hooks
|
|
367
|
+
|
|
368
|
+
#### `useRemoteComponent(options)`
|
|
369
|
+
|
|
370
|
+
Hook for loading remote components with manual control.
|
|
371
|
+
|
|
372
|
+
**Parameters:**
|
|
373
|
+
|
|
374
|
+
- `options.manifest`: `ComponentManifest` - Component manifest
|
|
375
|
+
- `options.componentName`: `string` - Component name
|
|
376
|
+
- `options.autoLoad?`: `boolean` - Auto-load on mount (default: `true`)
|
|
377
|
+
|
|
378
|
+
**Returns:**
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
{
|
|
382
|
+
component: React.ComponentType | null,
|
|
383
|
+
manifest: ComponentManifest | null,
|
|
384
|
+
loading: boolean,
|
|
385
|
+
error: string | null,
|
|
386
|
+
loaded: boolean,
|
|
387
|
+
loadComponent: () => Promise<void>,
|
|
388
|
+
reload: () => Promise<void>,
|
|
389
|
+
clearError: () => void
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Usage:**
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
const {
|
|
397
|
+
component: Component,
|
|
398
|
+
loading,
|
|
399
|
+
error,
|
|
400
|
+
} = useRemoteComponent({
|
|
401
|
+
manifest: pocManifest,
|
|
402
|
+
componentName: 'PocComponent',
|
|
403
|
+
autoLoad: true,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (loading) return <div>Loading...</div>;
|
|
407
|
+
if (error) return <div>Error: {error}</div>;
|
|
408
|
+
if (!Component) return null;
|
|
409
|
+
|
|
410
|
+
return <Component {...props} />;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### Data fetching (server-side)
|
|
416
|
+
|
|
417
|
+
#### `getRemoteManifest(environment, cookieStore): Promise<RemoteManifest>`
|
|
418
|
+
|
|
419
|
+
Server-side function that fetches the remote component manifest. Encapsulates URL construction (cache-busting timestamp) and the fetch call.
|
|
420
|
+
|
|
421
|
+
**Parameters:**
|
|
422
|
+
|
|
423
|
+
- `environment`: `Environment` - Environment variable value set in the consumer app's repository. Can be `production` or `develop`. Defaults to `production`.
|
|
424
|
+
- `cookieBranch`: `string | undefined` - f-branch can be set as a cookie with the name `cont-comp`. If it's not set or none is provided, it will default to `f-poc-live-update` for develop, and `master` for production.
|
|
425
|
+
|
|
426
|
+
**Returns:** `Promise<RemoteManifest>`
|
|
427
|
+
|
|
428
|
+
**Usage (NextJS):**
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { getRemoteManifest } from '@kaena1/ecomm-remote-utils/server';
|
|
432
|
+
import { cookies } from 'next/headers';
|
|
433
|
+
|
|
434
|
+
const cookieStore = await cookies();
|
|
435
|
+
const branchCookie = cookieStore.get('cont-comp')?.value;
|
|
436
|
+
const remoteManifest = await getRemoteManifest(
|
|
437
|
+
process.env.NEXT_PUBLIC_ENVIRONMENT,
|
|
438
|
+
branchCookie,
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
#### `getMintLayoutProps(NEXT_PUBLIC_SITE_URL, ECOMM_API_URL, ECOMM_API_STANDALONE_DEVICE_CREDS): Promise<MintLayoutProps>`
|
|
445
|
+
|
|
446
|
+
Server-side function that fetches MintLayout props from the ecomm API and maps them to the expected format.
|
|
447
|
+
|
|
448
|
+
**Parameters:**
|
|
449
|
+
|
|
450
|
+
- `NEXT_PUBLIC_SITE_URL`: `string` - The public site URL
|
|
451
|
+
- `ECOMM_API_URL`: `string` - The ecomm API base URL
|
|
452
|
+
- `ECOMM_API_STANDALONE_DEVICE_CREDS`: `string` - Credentials in `username|password` format
|
|
453
|
+
|
|
454
|
+
**Returns:** `Promise<MintLayoutProps>`
|
|
455
|
+
|
|
456
|
+
**Usage:**
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { getMintLayoutProps } from '@kaena1/ecomm-remote-utils/server';
|
|
460
|
+
|
|
461
|
+
const mintLayoutProps = await getMintLayoutProps(
|
|
462
|
+
process.env.NEXT_PUBLIC_SITE_URL,
|
|
463
|
+
process.env.ECOMM_API_URL,
|
|
464
|
+
process.env.ECOMM_API_STANDALONE_DEVICE_CREDS,
|
|
465
|
+
);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
### Utils
|
|
471
|
+
|
|
472
|
+
#### `getRemoteComponentsData(envVars): Promise<{remoteManifest: RemoteManifest, mintLayoutProps: MintLayoutProps}>`
|
|
473
|
+
|
|
474
|
+
Server-side util function that handles all logic for app router NextJS apps.
|
|
475
|
+
**IMPORTANT**: it is **mandatory** to use this in the app router. This **WON'T WORK** in the pages router.
|
|
476
|
+
|
|
477
|
+
**Parameters:**
|
|
478
|
+
|
|
479
|
+
- `envVars`: `RemoteComponentsEnvVars` - Environment variables object containing:
|
|
480
|
+
- `NEXT_PUBLIC_ENVIRONMENT`: `string` - The environment (e.g., 'production', 'develop')
|
|
481
|
+
- `NEXT_PUBLIC_SITE_URL`: `string` - The public site URL
|
|
482
|
+
- `ECOMM_API_URL`: `string` - The ecomm API base URL
|
|
483
|
+
- `ECOMM_API_STANDALONE_DEVICE_CREDS`: `string` - API credentials
|
|
484
|
+
|
|
485
|
+
**Returns:** `Promise<{ remoteManifest: RemoteManifest, mintLayoutProps: MintLayoutProps }>`
|
|
486
|
+
|
|
487
|
+
**Usage:**
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import { getRemoteComponentsData } from '@kaena1/ecomm-remote-utils/server';
|
|
491
|
+
|
|
492
|
+
const { remoteManifest, mintLayoutProps } = await getRemoteComponentsData({
|
|
493
|
+
NEXT_PUBLIC_ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT,
|
|
494
|
+
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
|
495
|
+
ECOMM_API_URL: process.env.ECOMM_API_URL,
|
|
496
|
+
ECOMM_API_STANDALONE_DEVICE_CREDS:
|
|
497
|
+
process.env.ECOMM_API_STANDALONE_DEVICE_CREDS,
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
#### `withRemoteComponentsGSSP(envVars, pageGSSP?): GetServerSideProps`
|
|
504
|
+
|
|
505
|
+
A Higher-Order Function (HOF) for Next.js Pages Router that composes a `getServerSideProps` function to automatically fetch remote component data (manifest and layout props).
|
|
506
|
+
**IMPORTANT**: It is **mandatory** to use this in the pages router. It handles fetching the manifest and layout props, merging them with your page's props.
|
|
507
|
+
|
|
508
|
+
**Parameters:**
|
|
509
|
+
|
|
510
|
+
- `envVars`: `RemoteComponentsEnvVars` - Environment variables object (same as `getRemoteComponentsData`).
|
|
511
|
+
- `pageGSSP`: `GetServerSideProps` (optional) - Your page's existing `getServerSideProps` function. The results will be merged.
|
|
512
|
+
|
|
513
|
+
**Returns:** A new `GetServerSideProps` function.
|
|
514
|
+
|
|
515
|
+
**Usage:**
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// pages/index.tsx
|
|
519
|
+
import { withRemoteComponentsGSSP } from '@kaena1/ecomm-remote-utils/server';
|
|
520
|
+
|
|
521
|
+
async function pageGSSP() {
|
|
522
|
+
// Your existing logic (optional)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export const getServerSideProps = withRemoteComponentsGSSP(
|
|
526
|
+
{
|
|
527
|
+
NEXT_PUBLIC_ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT,
|
|
528
|
+
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
|
529
|
+
ECOMM_API_URL: process.env.ECOMM_API_URL,
|
|
530
|
+
ECOMM_API_STANDALONE_DEVICE_CREDS:
|
|
531
|
+
process.env.ECOMM_API_STANDALONE_DEVICE_CREDS,
|
|
532
|
+
},
|
|
533
|
+
pageGSSP,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
export default function Page({ remoteManifest, mintLayoutProps, customProp }) {
|
|
537
|
+
// remoteManifest and mintLayoutProps are automatically injected
|
|
538
|
+
// ...
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Build & Development
|
|
545
|
+
|
|
546
|
+
### Build the Package
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
# Production build (minified)
|
|
550
|
+
npm run build
|
|
551
|
+
|
|
552
|
+
# Development build (unminified, preserves modules)
|
|
553
|
+
npm run build:debug
|
|
554
|
+
|
|
555
|
+
# Watch mode (rebuilds on changes)
|
|
556
|
+
npm run dev
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Type Checking & Linting
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
# Type check
|
|
563
|
+
npm run typecheck
|
|
564
|
+
|
|
565
|
+
# Lint
|
|
566
|
+
npm run lint
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Publishing
|
|
570
|
+
|
|
571
|
+
### Automatic Publishing (Active)
|
|
572
|
+
|
|
573
|
+
Publishing is fully automated via semantic-release and Bitbucket Pipelines. No manual steps needed.
|
|
574
|
+
|
|
575
|
+
| Branch | npm tag | Example version |
|
|
576
|
+
| --------- | ------------------------------------- | ----------------------- |
|
|
577
|
+
| `master` | `latest` | `1.0.5` |
|
|
578
|
+
| `develop` | `beta` _(post-POC)_ | `1.0.5-beta.1` |
|
|
579
|
+
| `f-poc-*` | Branch name _(temporary, during POC)_ | `1.0.5-f-poc-ec-3644.1` |
|
|
580
|
+
|
|
581
|
+
A release is triggered automatically when a push includes a `feat` or `fix` commit. The pipeline handles building, versioning, publishing to npm, and committing the updated `package.json` back to the branch.
|
|
582
|
+
|
|
583
|
+
#### Installing a prerelease (POC)
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
# Latest from a specific POC branch
|
|
587
|
+
npm install @kaena1/ecomm-remote-utils@f-poc-ec-3644
|
|
588
|
+
|
|
589
|
+
# Specific version
|
|
590
|
+
npm install @kaena1/ecomm-remote-utils@1.0.5-f-poc-ec-3644.1
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
> **⚠️ Pin prereleases exactly — no `^` or `~`**
|
|
594
|
+
>
|
|
595
|
+
> Semver range operators behave unexpectedly with prerelease versions. `^1.1.0-f-poc-live-update.8` resolves to any `1.1.0-<tag>` where `<tag>` sorts higher lexicographically, which can install a completely different branch.
|
|
596
|
+
>
|
|
597
|
+
> Always use an exact version in `package.json`:
|
|
598
|
+
>
|
|
599
|
+
> ```json
|
|
600
|
+
> "@kaena1/ecomm-remote-utils": "1.1.0-f-poc-live-update.8"
|
|
601
|
+
> ```
|
|
602
|
+
|
|
603
|
+
## Versioning & Branching
|
|
604
|
+
|
|
605
|
+
### Semantic Release Configuration
|
|
606
|
+
|
|
607
|
+
The package uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated versioning based on commit messages.
|
|
608
|
+
|
|
609
|
+
#### Supported Branches
|
|
610
|
+
|
|
611
|
+
| Branch Pattern | Prerelease Tag | Use Case |
|
|
612
|
+
| -------------- | -------------- | -------------------- |
|
|
613
|
+
| `master` | None | Production releases |
|
|
614
|
+
| `develop` | `true` | QA/Staging releases |
|
|
615
|
+
| `f-poc-*` | Branch name | POC/Feature releases |
|
|
616
|
+
|
|
617
|
+
#### Commit Message Format
|
|
618
|
+
|
|
619
|
+
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
|
620
|
+
|
|
621
|
+
```
|
|
622
|
+
<type>(<scope>): <description>
|
|
623
|
+
|
|
624
|
+
[optional body]
|
|
625
|
+
|
|
626
|
+
[optional footer]
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Examples:**
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
feat(ecomm-remote-utils): add new remote component loader
|
|
633
|
+
fix(ecomm-remote-utils): resolve React dependency conflicts
|
|
634
|
+
docs: update README with usage examples
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
#### Release Rules
|
|
638
|
+
|
|
639
|
+
Uses the default [Conventional Commits](https://www.conventionalcommits.org/) release rules:
|
|
640
|
+
|
|
641
|
+
| Commit Type | Release Type |
|
|
642
|
+
| ----------- | ------------- |
|
|
643
|
+
| `feat` | Minor (1.x.0) |
|
|
644
|
+
| `fix` | Patch (1.0.x) |
|
|
645
|
+
| `chore` | None |
|
|
646
|
+
|
|
647
|
+
### POC Branch Strategy
|
|
648
|
+
|
|
649
|
+
For the POC, we use the branch pattern `f-poc-*` with a specific prerelease identifier.
|
|
650
|
+
|
|
651
|
+
**Current setup:**
|
|
652
|
+
|
|
653
|
+
```javascript
|
|
654
|
+
{
|
|
655
|
+
name: 'f-poc-*',
|
|
656
|
+
prerelease: '${name}', // Uses full branch name as tag
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
This means:
|
|
661
|
+
|
|
662
|
+
- Branch `f-poc-EC-3570` → publishes as `1.0.3-f-poc-EC-3570.1`
|
|
663
|
+
- Branch `f-poc-live-update` → publishes as `1.0.3-f-poc-live-update.1`
|
|
664
|
+
|
|
665
|
+
## How It Works
|
|
666
|
+
|
|
667
|
+
### Architecture Overview
|
|
668
|
+
|
|
669
|
+
```
|
|
670
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
671
|
+
│ Consumer App (Next.js) │
|
|
672
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
673
|
+
│ │ _document.tsx: <ImportMapScripts /> │ │
|
|
674
|
+
│ │ └─> Injects import map for React dependency resolution│ │
|
|
675
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
676
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
677
|
+
│ │ _app.tsx: │ │
|
|
678
|
+
│ │ 1. fetchManifest() - Gets component URLs │ │
|
|
679
|
+
│ │ 2. <RemoteLinks /> - Preloads resources │ │
|
|
680
|
+
│ │ 3. <RemoteRenderer /> - Loads & renders component │ │
|
|
681
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
682
|
+
└─────────────────────────────────────────────────────────────┘
|
|
683
|
+
↓
|
|
684
|
+
┌─────────────────────────────────┐
|
|
685
|
+
│ ModuleLoader (ecomm-remote-utils) │
|
|
686
|
+
│ 1. Checks cache │
|
|
687
|
+
│ 2. Loads ES module via script │
|
|
688
|
+
│ 3. Extracts component │
|
|
689
|
+
│ 4. Returns React component │
|
|
690
|
+
└─────────────────────────────────┘
|
|
691
|
+
↓
|
|
692
|
+
┌─────────────────────────────────┐
|
|
693
|
+
│ Remote Component (S3/CDN) │
|
|
694
|
+
│ • MintLayout │
|
|
695
|
+
│ • PocComponent │
|
|
696
|
+
│ • TestComponent │
|
|
697
|
+
│ etc. │
|
|
698
|
+
└─────────────────────────────────┘
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Component Manifest Format
|
|
702
|
+
|
|
703
|
+
Remote components are described by a manifest JSON file:
|
|
704
|
+
|
|
705
|
+
```json
|
|
706
|
+
{
|
|
707
|
+
"components": {
|
|
708
|
+
"MintLayout": {
|
|
709
|
+
"bundleUrl": "https://.../MintLayout.abc123.es.js",
|
|
710
|
+
"styleUrl": "https://.../MintLayout.def456.css"
|
|
711
|
+
},
|
|
712
|
+
"PocComponent": {
|
|
713
|
+
"bundleUrl": "https://.../PocComponent.xyz789.es.js",
|
|
714
|
+
"styleUrl": "https://.../PocComponent.uvw012.css"
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
"version": "0.1.0",
|
|
718
|
+
"generatedAt": "2026-01-20T12:00:00.000Z"
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Import Map Resolution
|
|
723
|
+
|
|
724
|
+
The `ImportMapScripts` component injects an import map that resolves bare specifiers to use the **host app's React instance**:
|
|
725
|
+
|
|
726
|
+
```html
|
|
727
|
+
<script type="importmap-shim">
|
|
728
|
+
{
|
|
729
|
+
"imports": {
|
|
730
|
+
"react": "data:text/javascript,...(uses window.React)",
|
|
731
|
+
"react/jsx-runtime": "data:text/javascript,...(uses window.React.createElement)",
|
|
732
|
+
"react-dom": "https://esm.sh/react-dom@18.3.1",
|
|
733
|
+
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client"
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
</script>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**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.
|
|
740
|
+
|
|
741
|
+
### Module Loading Flow
|
|
742
|
+
|
|
743
|
+
1. **Component Manifest** is fetched via `fetchManifest()`
|
|
744
|
+
2. **ModuleLoader** dynamically imports the ES module from `bundleUrl`
|
|
745
|
+
3. **Component extraction** finds the named export or default export
|
|
746
|
+
4. **React component** is returned and rendered via `<RemoteRenderer>`
|
|
747
|
+
|
|
748
|
+
## CI/CD Pipeline
|
|
749
|
+
|
|
750
|
+
### Bitbucket Pipelines Configuration
|
|
751
|
+
|
|
752
|
+
The package is built and deployed via Bitbucket Pipelines (`bitbucket-pipelines.yml` in repo root).
|
|
753
|
+
|
|
754
|
+
#### Pipeline Stages
|
|
755
|
+
|
|
756
|
+
**For `f-poc-*` branches (temporary, during POC):**
|
|
757
|
+
|
|
758
|
+
```yaml
|
|
759
|
+
- stage: <EcommContainedUtils> - POC
|
|
760
|
+
condition:
|
|
761
|
+
changesets:
|
|
762
|
+
includePaths:
|
|
763
|
+
- packages/ecomm-remote-utils/**
|
|
764
|
+
steps:
|
|
765
|
+
- Lint & Build
|
|
766
|
+
- Deploy (prerelease published to npm with branch name as tag)
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**For `develop` branch (post-POC):**
|
|
770
|
+
|
|
771
|
+
```yaml
|
|
772
|
+
- stage: <EcommContainedUtils> - QA
|
|
773
|
+
condition:
|
|
774
|
+
changesets:
|
|
775
|
+
includePaths:
|
|
776
|
+
- packages/ecomm-remote-utils/**
|
|
777
|
+
steps:
|
|
778
|
+
- Lint & Build
|
|
779
|
+
- Deploy (beta prerelease published to npm)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**For `master` branch:**
|
|
783
|
+
|
|
784
|
+
```yaml
|
|
785
|
+
- stage: <EcommContainedUtils> - Production
|
|
786
|
+
condition:
|
|
787
|
+
changesets:
|
|
788
|
+
includePaths:
|
|
789
|
+
- packages/ecomm-remote-utils/**
|
|
790
|
+
steps:
|
|
791
|
+
- Lint & Build
|
|
792
|
+
- Deploy (stable release published to npm)
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
**Triggers:**
|
|
796
|
+
|
|
797
|
+
- Runs when files in `packages/ecomm-remote-utils/**` are modified
|
|
798
|
+
|
|
799
|
+
## Environment Variables
|
|
800
|
+
|
|
801
|
+
All env vars are read in the consumer app, not in this package. These are passed into the utility functions as arguments.
|
|
802
|
+
|
|
803
|
+
| Variable | Used by | Description |
|
|
804
|
+
| ----------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
805
|
+
| `NEXT_PUBLIC_ENVIRONMENT` | `getRemoteManifest`, `getRemoteComponentsData`, `withRemoteComponentsGSSP` | Controls which S3 bucket the manifest is fetched from. `production` → `assets.mintmobile.com`, anything else → `assets-qa.mintmobile.com` |
|
|
806
|
+
| `NEXT_PUBLIC_SITE_URL` | `getMintLayoutProps`, `getRemoteComponentsData` | Public-facing site URL, forwarded to MintLayout's `config` prop |
|
|
807
|
+
| `ECOMM_API_URL` | `getMintLayoutProps`, `getRemoteComponentsData` | Base URL of the ecomm API |
|
|
808
|
+
| `ECOMM_API_STANDALONE_DEVICE_CREDS` | `getMintLayoutProps`, `getRemoteComponentsData` | API credentials in `username\|password` format |
|
|
809
|
+
|
|
810
|
+
The `cont-comp` **cookie** (not an env var) can be set in the browser to override which branch's manifest is loaded. `getRemoteManifest` reads it when passed via `cookieBranch`. If absent, defaults to `f-poc-live-update` (develop) or `master` (production).
|
|
811
|
+
|
|
812
|
+
## Troubleshooting
|
|
813
|
+
|
|
814
|
+
**`Invalid hook call` / "You may have more than one copy of React"**
|
|
815
|
+
Caused by `npm link` creating two separate React instances. Use `npm pack` instead — see [For Local Development](#for-local-development).
|
|
816
|
+
|
|
817
|
+
**Component renders nothing / stays on placeholder indefinitely**
|
|
818
|
+
Check the browser console for a module load error. Common causes:
|
|
819
|
+
|
|
820
|
+
- `bundleUrl` in the manifest is unreachable (wrong environment, pipeline hasn't run)
|
|
821
|
+
- Import map is missing — confirm `<ImportMapScripts />` is rendered before the component loads
|
|
822
|
+
|
|
823
|
+
**`getRemoteManifest` returns `null` or throws in production**
|
|
824
|
+
The manifest fetch failed server-side. The consumer app should handle `null` gracefully and render a fallback (e.g. render `children` without the layout wrapper). `withRemoteComponentsGSSP` and `getRemoteComponentsData` do this automatically.
|
|
825
|
+
|
|
826
|
+
**`RemoteRenderer` throws "Cannot use import statement" or similar ESM error**
|
|
827
|
+
`es-module-shims` is not loaded. Confirm `<ImportMapScripts />` is in `<head>` before any contained component scripts are executed.
|
|
828
|
+
|
|
829
|
+
**TypeScript: "Module has no exported member" for a remote component**
|
|
830
|
+
The `urlImports.d.ts` type declarations in the consumer app are out of date. Re-run `getMintLayoutTypes` (or update the file manually) to pull the latest type definitions.
|
|
831
|
+
|
|
832
|
+
**Wrong branch loaded in QA**
|
|
833
|
+
Check the `cont-comp` cookie value in the browser. It is case-sensitive and must match the branch name exactly (e.g. `f-poc-EC-3808`, not `f-poc-ec-3808`). Clear or update the cookie to switch branches.
|