@horizonmedia/blu-next-adapter 0.0.0-stage → 1.2.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 +81 -2
- package/chunks/chunk-P6QUCTDO.js +228 -0
- package/client.js +26 -0
- package/package.json +41 -4
- package/server.js +67 -0
- package/shims/image.js +13 -0
- package/shims/link.js +27 -0
- package/shims/navigation.js +50 -0
- package/types/client.d.ts +47 -0
- package/types/server.d.ts +183 -0
- package/types/vite.d.ts +21 -0
- package/vite.js +224 -0
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blu/next-adapter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Publish an existing **Next.js (App Router)** app as a Blu module, from the same codebase that still runs standalone with `next build`. It's published on npm as **`@horizonmedia/blu-next-adapter`**; your code imports it as **`@blu/next-adapter`**.
|
|
4
|
+
|
|
5
|
+
It works with **[`@blu/sdk`](../sdk/README.md)**, which you need alongside it.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
It's public on npm, so it needs no `.npmrc` or token (see the [`@blu/sdk` install steps](../sdk/README.md#install)). Add:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@blu/sdk": "npm:@horizonmedia/blu-sdk@^1.8.0",
|
|
14
|
+
"@blu/next-adapter": "npm:@horizonmedia/blu-next-adapter@^1.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"vite": "^7.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build:blu": "vite build -c vite.blu.config.ts"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You already have `react`, `tailwindcss` and `autoprefixer` from your Next.js app.
|
|
25
|
+
|
|
26
|
+
## What you get
|
|
27
|
+
|
|
28
|
+
| Import | Use |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `@blu/next-adapter/vite` | `bluModule({ name, tailwind })`: the whole module build. `build:blu` builds your app's own pages and components for Module Federation into `public/blu/`, which your Next.js server serves. |
|
|
31
|
+
| `@blu/next-adapter/client` | `<BluNextModule>`: what your root `app/layout.tsx` does, inside Blu. It runs your `app/**/page.tsx` and `layout.tsx` files under Blu's routing. Also `moduleFetch`, `useModuleAssetUrl`, `preloadModuleAsset` and `moduleUrl` for calls to your own server. |
|
|
32
|
+
| `@blu/next-adapter/server` | `createBluNextServer(...)`: turns a Blu token into the caller for your `auth()`, serves the route your server actions arrive at inside Blu, and the CORS helpers for `proxy.ts`. |
|
|
33
|
+
|
|
34
|
+
**Inside Blu, without changing your pages:**
|
|
35
|
+
- **Routing:** `next/link`, `next/navigation` (`usePathname`, `useParams`, `useSearchParams`, `useRouter`, `redirect()`, `notFound()`) and `next/image` work against Blu's routing. Query strings show in Blu's address bar, and the back button and deep links work.
|
|
36
|
+
- **Server actions** are sent over HTTP with the user's Blu token and run your same action functions.
|
|
37
|
+
- **Styles:** your `globals.css` and Tailwind are scoped to the module, so they can't restyle Blu:
|
|
38
|
+
- Tailwind's global reset (`@tailwind base`) is dropped, and a small reset scoped to the module is added.
|
|
39
|
+
- Design tokens on `:root` and `.dark` move onto the module's wrapper.
|
|
40
|
+
- Rules in `@layer base { … }` are unwrapped and scoped (1.2+):
|
|
41
|
+
- `:root`, `html` and `body` become the wrapper;
|
|
42
|
+
- page-root states like `.dark` or `[data-skin="…"]` become the wrapper inside them;
|
|
43
|
+
- anything else, such as `*` or `h1`, applies inside the wrapper only.
|
|
44
|
+
- **Navigation keeps state:** like Next.js, navigating keeps your providers, top bar and section shells mounted.
|
|
45
|
+
|
|
46
|
+
## Minimal setup
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// vite.blu.config.ts
|
|
50
|
+
import { bluModule } from "@blu/next-adapter/vite";
|
|
51
|
+
import tailwind from "./tailwind.config.js";
|
|
52
|
+
export default bluModule({ name: "unified", tailwind });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// blu/App.tsx: your root layout, inside Blu
|
|
57
|
+
import { BluNextModule } from "@blu/next-adapter/client";
|
|
58
|
+
import css from "../app/globals.css?inline";
|
|
59
|
+
|
|
60
|
+
export default function InBlu() {
|
|
61
|
+
return (
|
|
62
|
+
<BluNextModule css={css} pages={import.meta.glob("../app/**/page.tsx", { eager: true })}>
|
|
63
|
+
{(page) => <YourProviders>{page}</YourProviders>}
|
|
64
|
+
</BluNextModule>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// app/api/blu/actions/route.ts: your server actions, called from inside Blu
|
|
71
|
+
import * as campaigns from "@/app/actions/campaigns";
|
|
72
|
+
import { createBluNextServer } from "@blu/next-adapter/server";
|
|
73
|
+
|
|
74
|
+
const blu = createBluNextServer({ moduleId, issuer, jwksUrl, apiUrl, roleMap, allowedOrigins });
|
|
75
|
+
export const POST = (request: Request) => blu.actionsRoute({ campaigns }).POST(request);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a complete worked example, see the Unified stand-in in this repository: `teams/team-d/README.md` and its `BLU_INTEGRATION.patch`.
|
|
79
|
+
|
|
80
|
+
## Versions
|
|
81
|
+
|
|
82
|
+
Requires `@blu/sdk` `^1.5.0`. It's versioned separately; upgrade either within a major version whenever you like.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// src/client.tsx
|
|
2
|
+
import { useApiClient, useHost, useRoute } from "@blu/sdk";
|
|
3
|
+
import { Component, createContext, useContext, useEffect, useInsertionEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var DEFAULT_SCOPE = "blu-module";
|
|
6
|
+
var api = null;
|
|
7
|
+
async function callServerAction(module, name, args) {
|
|
8
|
+
if (!api) throw new Error("[@blu/next-adapter] server action called outside <BluNextModule>");
|
|
9
|
+
return api.post("/api/blu/actions", { module, name, args });
|
|
10
|
+
}
|
|
11
|
+
function bluApi() {
|
|
12
|
+
if (!api) throw new Error("[@blu/next-adapter] bluApi() called outside <BluNextModule>");
|
|
13
|
+
return api;
|
|
14
|
+
}
|
|
15
|
+
var mod = null;
|
|
16
|
+
var ownPath = (path) => {
|
|
17
|
+
if (!path.startsWith("/") || path.startsWith("//")) throw new Error(`[@blu/next-adapter] expected a path on your own server ("/api/\u2026"), got "${path}"`);
|
|
18
|
+
return path;
|
|
19
|
+
};
|
|
20
|
+
function moduleUrl(path) {
|
|
21
|
+
return mod ? `${mod.baseUrl}${ownPath(path)}` : path;
|
|
22
|
+
}
|
|
23
|
+
async function moduleFetch(path, init = {}) {
|
|
24
|
+
if (!mod) return fetch(path, init);
|
|
25
|
+
const headers = new Headers(init.headers);
|
|
26
|
+
headers.set("Authorization", `Bearer ${await mod.getToken()}`);
|
|
27
|
+
return fetch(moduleUrl(path), { ...init, headers, credentials: "omit", redirect: "error" });
|
|
28
|
+
}
|
|
29
|
+
var MAX_ASSETS = 200;
|
|
30
|
+
var assets = /* @__PURE__ */ new Map();
|
|
31
|
+
function loadAsset(path) {
|
|
32
|
+
let p = assets.get(path);
|
|
33
|
+
if (!p) {
|
|
34
|
+
p = moduleFetch(path).then(async (r) => {
|
|
35
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
36
|
+
return URL.createObjectURL(await r.blob());
|
|
37
|
+
});
|
|
38
|
+
p.catch(() => assets.delete(path));
|
|
39
|
+
assets.set(path, p);
|
|
40
|
+
if (assets.size > MAX_ASSETS) {
|
|
41
|
+
const [oldest, old] = assets.entries().next().value;
|
|
42
|
+
assets.delete(oldest);
|
|
43
|
+
old.then(URL.revokeObjectURL, () => {
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
function useModuleAssetUrl(path) {
|
|
50
|
+
const [loaded2, setLoaded] = useState(null);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!mod) return;
|
|
53
|
+
let live = true;
|
|
54
|
+
loadAsset(path).then(
|
|
55
|
+
(src) => live && setLoaded({ path, src }),
|
|
56
|
+
() => live && setLoaded({ path, src: null })
|
|
57
|
+
);
|
|
58
|
+
return () => {
|
|
59
|
+
live = false;
|
|
60
|
+
};
|
|
61
|
+
}, [path]);
|
|
62
|
+
if (!mod) return path;
|
|
63
|
+
return loaded2?.path === path ? loaded2.src : null;
|
|
64
|
+
}
|
|
65
|
+
function preloadModuleAsset(path) {
|
|
66
|
+
if (!mod) {
|
|
67
|
+
new window.Image().src = path;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
loadAsset(path).catch(() => {
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
var toSegments = (key, file) => key.replace(/^.*?\/app\//, "").replace(new RegExp(`(^|/)${file}\\.(t|j)sx?$`), "").split("/").filter((s) => s && !/^\(.*\)$/.test(s));
|
|
74
|
+
function match(segments, path) {
|
|
75
|
+
const params = {};
|
|
76
|
+
for (let i = 0; i < segments.length; i++) {
|
|
77
|
+
const seg = segments[i];
|
|
78
|
+
const catchAll = /^\[\.\.\.(\w+)\]$/.exec(seg);
|
|
79
|
+
if (catchAll) {
|
|
80
|
+
if (i >= path.length) return null;
|
|
81
|
+
params[catchAll[1]] = path.slice(i).join("/");
|
|
82
|
+
return params;
|
|
83
|
+
}
|
|
84
|
+
if (i >= path.length) return null;
|
|
85
|
+
const param = /^\[(\w+)\]$/.exec(seg);
|
|
86
|
+
if (param) params[param[1]] = path[i];
|
|
87
|
+
else if (seg !== path[i]) return null;
|
|
88
|
+
}
|
|
89
|
+
return segments.length === path.length ? params : null;
|
|
90
|
+
}
|
|
91
|
+
var ParamsContext = createContext({});
|
|
92
|
+
var useBluParams = () => useContext(ParamsContext);
|
|
93
|
+
var BluRedirect = class extends Error {
|
|
94
|
+
constructor(href) {
|
|
95
|
+
super(`redirect(${href})`);
|
|
96
|
+
this.href = href;
|
|
97
|
+
}
|
|
98
|
+
href;
|
|
99
|
+
};
|
|
100
|
+
var BluNotFound = class extends Error {
|
|
101
|
+
constructor() {
|
|
102
|
+
super("notFound()");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var RouteBoundary = class extends Component {
|
|
106
|
+
state = { routeKey: this.props.routeKey, caught: null, other: null };
|
|
107
|
+
static getDerivedStateFromError(error) {
|
|
108
|
+
return error instanceof BluRedirect || error instanceof BluNotFound ? { caught: error, other: null } : { caught: null, other: error };
|
|
109
|
+
}
|
|
110
|
+
static getDerivedStateFromProps(props, state) {
|
|
111
|
+
return props.routeKey === state.routeKey ? null : { routeKey: props.routeKey, caught: null, other: null };
|
|
112
|
+
}
|
|
113
|
+
componentDidUpdate() {
|
|
114
|
+
if (this.state.caught instanceof BluRedirect) this.props.onRedirect(this.state.caught.href);
|
|
115
|
+
}
|
|
116
|
+
componentDidMount() {
|
|
117
|
+
this.componentDidUpdate();
|
|
118
|
+
}
|
|
119
|
+
render() {
|
|
120
|
+
if (this.state.other) throw this.state.other;
|
|
121
|
+
if (this.state.caught instanceof BluNotFound) return /* @__PURE__ */ jsx("p", { style: { padding: 24 }, children: "Not found." });
|
|
122
|
+
return this.state.caught ? null : this.props.children;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var loaded = /* @__PURE__ */ new Map();
|
|
126
|
+
var componentOf = (e) => typeof e === "function" ? loaded.get(e) : e.default;
|
|
127
|
+
var ensure = async (e) => {
|
|
128
|
+
if (typeof e === "function" && !loaded.has(e)) loaded.set(e, (await e()).default);
|
|
129
|
+
};
|
|
130
|
+
function BluNextModule({
|
|
131
|
+
css,
|
|
132
|
+
scope = DEFAULT_SCOPE,
|
|
133
|
+
pages,
|
|
134
|
+
layouts = {},
|
|
135
|
+
exclude = [],
|
|
136
|
+
children
|
|
137
|
+
}) {
|
|
138
|
+
api = useApiClient();
|
|
139
|
+
const host = useHost();
|
|
140
|
+
mod = { baseUrl: host.environment.apiBaseUrl, getToken: host.getToken };
|
|
141
|
+
const route = useRoute();
|
|
142
|
+
useInsertionEffect(() => {
|
|
143
|
+
if (!css) return;
|
|
144
|
+
const id = `blu-css-${scope}`;
|
|
145
|
+
let el = document.getElementById(id);
|
|
146
|
+
if (!el) {
|
|
147
|
+
el = document.createElement("style");
|
|
148
|
+
el.id = id;
|
|
149
|
+
el.textContent = css;
|
|
150
|
+
document.head.append(el);
|
|
151
|
+
}
|
|
152
|
+
const users = Number(el.dataset.users ?? 0) + 1;
|
|
153
|
+
el.dataset.users = String(users);
|
|
154
|
+
return () => {
|
|
155
|
+
const n = Number(el.dataset.users) - 1;
|
|
156
|
+
if (n <= 0) el.remove();
|
|
157
|
+
else el.dataset.users = String(n);
|
|
158
|
+
};
|
|
159
|
+
}, [css, scope]);
|
|
160
|
+
const routes = useMemo(() => {
|
|
161
|
+
const defs = Object.entries(pages).map(([k, entry]) => ({ segments: toSegments(k, "page"), entry }));
|
|
162
|
+
const weight = (s) => /^\[\.\.\./.test(s) ? 2 : /^\[/.test(s) ? 1 : 0;
|
|
163
|
+
return defs.sort((a, b) => a.segments.map(weight).join("").localeCompare(b.segments.map(weight).join("")));
|
|
164
|
+
}, [pages]);
|
|
165
|
+
const layoutDefs = useMemo(() => Object.entries(layouts).map(([k, entry]) => ({ segments: toSegments(k, "layout"), entry })), [layouts]);
|
|
166
|
+
const path = route.path.split("/").filter(Boolean).map(decodeURIComponent);
|
|
167
|
+
const excluded = exclude.some((e) => route.path === e || route.path.startsWith(`${e}/`));
|
|
168
|
+
let hit = null;
|
|
169
|
+
if (!excluded) {
|
|
170
|
+
for (const def of routes) {
|
|
171
|
+
const params = match(def.segments, path);
|
|
172
|
+
if (params) {
|
|
173
|
+
hit = { def, params };
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const wrappers = hit ? layoutDefs.filter((l) => l.segments.length > 0 && match(l.segments, path.slice(0, l.segments.length))).sort((a, b) => a.segments.length - b.segments.length) : [];
|
|
179
|
+
const needed = hit ? [...wrappers.map((w) => w.entry), hit.def.entry] : [];
|
|
180
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
181
|
+
const last = useRef(null);
|
|
182
|
+
const ready = needed.every((e) => componentOf(e));
|
|
183
|
+
if (ready) {
|
|
184
|
+
let page = /* @__PURE__ */ jsx("p", { style: { padding: 24 }, children: "Not found." });
|
|
185
|
+
if (hit) {
|
|
186
|
+
const Page = componentOf(hit.def.entry);
|
|
187
|
+
page = /* @__PURE__ */ jsx(Page, {});
|
|
188
|
+
for (const w of [...wrappers].reverse()) {
|
|
189
|
+
const Layout = componentOf(w.entry);
|
|
190
|
+
page = /* @__PURE__ */ jsx(Layout, { children: page });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
last.current = { page, params: hit?.params ?? {} };
|
|
194
|
+
}
|
|
195
|
+
const routeKey = route.path;
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (ready) return;
|
|
198
|
+
let live = true;
|
|
199
|
+
Promise.all(needed.map(ensure)).then(() => live && rerender());
|
|
200
|
+
return () => {
|
|
201
|
+
live = false;
|
|
202
|
+
};
|
|
203
|
+
}, [routeKey, ready]);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const all = [...routes.map((r) => r.entry), ...layoutDefs.map((l) => l.entry)].filter((e) => typeof e === "function");
|
|
206
|
+
if (all.length === 0) return;
|
|
207
|
+
const t = setTimeout(() => all.forEach((e) => ensure(e).catch(() => {
|
|
208
|
+
})), 1500);
|
|
209
|
+
return () => clearTimeout(t);
|
|
210
|
+
}, [routes, layoutDefs]);
|
|
211
|
+
return /* @__PURE__ */ jsx("div", { className: scope, "data-blu-module": true, children: /* @__PURE__ */ jsx(ParamsContext.Provider, { value: last.current?.params ?? {}, children: children(
|
|
212
|
+
last.current ? /* @__PURE__ */ jsx(RouteBoundary, { routeKey: `${route.path}${route.search ?? ""}`, onRedirect: (href) => route.navigate(href, { replace: true }), children: last.current.page }) : null
|
|
213
|
+
) }) });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
DEFAULT_SCOPE,
|
|
218
|
+
callServerAction,
|
|
219
|
+
bluApi,
|
|
220
|
+
moduleUrl,
|
|
221
|
+
moduleFetch,
|
|
222
|
+
useModuleAssetUrl,
|
|
223
|
+
preloadModuleAsset,
|
|
224
|
+
useBluParams,
|
|
225
|
+
BluRedirect,
|
|
226
|
+
BluNotFound,
|
|
227
|
+
BluNextModule
|
|
228
|
+
};
|
package/client.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BluNextModule,
|
|
3
|
+
BluNotFound,
|
|
4
|
+
BluRedirect,
|
|
5
|
+
DEFAULT_SCOPE,
|
|
6
|
+
bluApi,
|
|
7
|
+
callServerAction,
|
|
8
|
+
moduleFetch,
|
|
9
|
+
moduleUrl,
|
|
10
|
+
preloadModuleAsset,
|
|
11
|
+
useBluParams,
|
|
12
|
+
useModuleAssetUrl
|
|
13
|
+
} from "./chunks/chunk-P6QUCTDO.js";
|
|
14
|
+
export {
|
|
15
|
+
BluNextModule,
|
|
16
|
+
BluNotFound,
|
|
17
|
+
BluRedirect,
|
|
18
|
+
DEFAULT_SCOPE,
|
|
19
|
+
bluApi,
|
|
20
|
+
callServerAction,
|
|
21
|
+
moduleFetch,
|
|
22
|
+
moduleUrl,
|
|
23
|
+
preloadModuleAsset,
|
|
24
|
+
useBluParams,
|
|
25
|
+
useModuleAssetUrl
|
|
26
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@horizonmedia/blu-next-adapter",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "Publish an existing Next.js (App Router) app as a Blu module from the same codebase: the Vite build (@blu/next-adapter/vite), the client runtime (/client) and server helpers (/server).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/HorizonMedia/blu-composable-experience.git",
|
|
10
|
+
"directory": "packages/next-adapter"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/HorizonMedia/blu-composable-experience/tree/main/packages/next-adapter#readme",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org",
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
"./vite": {
|
|
20
|
+
"types": "./types/vite.d.ts",
|
|
21
|
+
"default": "./vite.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"types": "./types/client.d.ts",
|
|
25
|
+
"default": "./client.js"
|
|
26
|
+
},
|
|
27
|
+
"./server": {
|
|
28
|
+
"types": "./types/server.d.ts",
|
|
29
|
+
"default": "./server.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@module-federation/vite": "^1.23.0",
|
|
34
|
+
"@vitejs/plugin-react": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@blu/sdk": "npm:@horizonmedia/blu-sdk@^1.8.4",
|
|
38
|
+
"autoprefixer": "^10.4.0",
|
|
39
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
40
|
+
"tailwindcss": "^3.4.0",
|
|
41
|
+
"vite": "^7.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/server.js
|
|
2
|
+
import { bluPlatformAuth, createModuleAuth } from "@blu/sdk/server";
|
|
3
|
+
var bearerOf = (headers) => /^Bearer (.+)$/i.exec(headers.get("authorization") ?? "")?.[1] ?? null;
|
|
4
|
+
function bluPreflight(request, allowedOrigins) {
|
|
5
|
+
if (request.method !== "OPTIONS") return null;
|
|
6
|
+
const origin = request.headers.get("origin");
|
|
7
|
+
if (!origin || !allowedOrigins.includes(origin)) return new Response(null, { status: 403 });
|
|
8
|
+
return new Response(null, {
|
|
9
|
+
status: 204,
|
|
10
|
+
headers: {
|
|
11
|
+
"Access-Control-Allow-Origin": origin,
|
|
12
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
13
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
14
|
+
"Access-Control-Max-Age": "600",
|
|
15
|
+
Vary: "Origin"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function withBluCors(request, response, allowedOrigins) {
|
|
20
|
+
const origin = request.headers.get("origin");
|
|
21
|
+
if (origin && allowedOrigins.includes(origin)) response.headers.set("Access-Control-Allow-Origin", origin);
|
|
22
|
+
response.headers.append("Vary", "Origin");
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
function createBluNextServer({ moduleId, issuer, jwksUrl, apiUrl, roleMap, allowedOrigins }) {
|
|
26
|
+
const moduleAuth = createModuleAuth({ apiUrl, strategies: [bluPlatformAuth({ issuer, jwksUrl, moduleId, roleMap })] });
|
|
27
|
+
async function principalFromHeaders(headers) {
|
|
28
|
+
if (!bearerOf(headers)) return null;
|
|
29
|
+
try {
|
|
30
|
+
const p = await moduleAuth.verifyRequest(new Request(apiUrl, { headers }));
|
|
31
|
+
return p.authMode === "blu" ? p : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function actionsRoute(modules) {
|
|
37
|
+
return {
|
|
38
|
+
OPTIONS: (request) => bluPreflight(request, allowedOrigins) ?? new Response(null, { status: 204 }),
|
|
39
|
+
async POST(request) {
|
|
40
|
+
const reply = (body2, status = 200) => withBluCors(request, Response.json(body2, { status }), allowedOrigins);
|
|
41
|
+
if (!await principalFromHeaders(request.headers)) return reply({ error: "blu_token_required" }, 401);
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = await request.json();
|
|
45
|
+
} catch {
|
|
46
|
+
return reply({ error: "bad_request" }, 400);
|
|
47
|
+
}
|
|
48
|
+
const mod = Object.prototype.hasOwnProperty.call(modules, body?.module) ? modules[body.module] : null;
|
|
49
|
+
const fn = mod && Object.prototype.hasOwnProperty.call(mod, body?.name) && typeof mod[body.name] === "function" ? mod[body.name] : null;
|
|
50
|
+
if (!fn || !Array.isArray(body.args)) return reply({ error: "unknown_action" }, 404);
|
|
51
|
+
return reply(await fn(...body.args) ?? null);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
principalFromHeaders,
|
|
57
|
+
/** Strict: the Blu caller, or throws (status 401/403) — e.g. for createMcpWebHandler's `verify`. */
|
|
58
|
+
verifyRequest: (request) => moduleAuth.verifyRequest(request),
|
|
59
|
+
actionsRoute,
|
|
60
|
+
allowedOrigins
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
bluPreflight,
|
|
65
|
+
createBluNextServer,
|
|
66
|
+
withBluCors
|
|
67
|
+
};
|
package/shims/image.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
moduleUrl
|
|
3
|
+
} from "../chunks/chunk-P6QUCTDO.js";
|
|
4
|
+
|
|
5
|
+
// src/shims/image.tsx
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function Image({ src, priority: _p, fill: _f, quality: _q, unoptimized: _u, alt = "", ...rest }) {
|
|
8
|
+
const url = typeof src === "string" ? src : src.src;
|
|
9
|
+
return /* @__PURE__ */ jsx("img", { src: url.startsWith("/") && !url.startsWith("//") ? moduleUrl(url) : url, alt, ...rest });
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
Image as default
|
|
13
|
+
};
|
package/shims/link.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/shims/link.tsx
|
|
2
|
+
import { useRoute } from "@blu/sdk";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var Link = forwardRef(function Link2({ href, onClick, prefetch: _p, replace, scroll: _s, children, ...rest }, ref) {
|
|
6
|
+
const route = useRoute();
|
|
7
|
+
const to = typeof href === "string" ? href : href.pathname;
|
|
8
|
+
return /* @__PURE__ */ jsx(
|
|
9
|
+
"a",
|
|
10
|
+
{
|
|
11
|
+
ref,
|
|
12
|
+
href: to,
|
|
13
|
+
...rest,
|
|
14
|
+
onClick: (e) => {
|
|
15
|
+
onClick?.(e);
|
|
16
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || !to.startsWith("/")) return;
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
route.navigate(to, { replace });
|
|
19
|
+
},
|
|
20
|
+
children
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
var link_default = Link;
|
|
25
|
+
export {
|
|
26
|
+
link_default as default
|
|
27
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BluNotFound,
|
|
3
|
+
BluRedirect,
|
|
4
|
+
useBluParams
|
|
5
|
+
} from "../chunks/chunk-P6QUCTDO.js";
|
|
6
|
+
|
|
7
|
+
// src/shims/navigation.ts
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
import { useRoute } from "@blu/sdk";
|
|
10
|
+
var usePathname = () => useRoute().path;
|
|
11
|
+
var useParams = () => useBluParams();
|
|
12
|
+
var useSelectedLayoutSegment = () => useRoute().path.split("/").filter(Boolean)[0] ?? null;
|
|
13
|
+
function useSearchParams() {
|
|
14
|
+
const search = useRoute().search ?? "";
|
|
15
|
+
return useMemo(() => new URLSearchParams(search), [search]);
|
|
16
|
+
}
|
|
17
|
+
function useRouter() {
|
|
18
|
+
const route = useRoute();
|
|
19
|
+
return useMemo(
|
|
20
|
+
() => ({
|
|
21
|
+
// Paths ("/media-delivery?x=1") or just a query ("?x=1"), as in Next.
|
|
22
|
+
push: (href) => route.navigate(href),
|
|
23
|
+
replace: (href) => route.navigate(href, { replace: true }),
|
|
24
|
+
back: () => window.history.back(),
|
|
25
|
+
forward: () => window.history.forward(),
|
|
26
|
+
refresh: () => {
|
|
27
|
+
},
|
|
28
|
+
prefetch: () => {
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
[route]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function redirect(href) {
|
|
35
|
+
throw new BluRedirect(href);
|
|
36
|
+
}
|
|
37
|
+
var permanentRedirect = redirect;
|
|
38
|
+
function notFound() {
|
|
39
|
+
throw new BluNotFound();
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
notFound,
|
|
43
|
+
permanentRedirect,
|
|
44
|
+
redirect,
|
|
45
|
+
useParams,
|
|
46
|
+
usePathname,
|
|
47
|
+
useRouter,
|
|
48
|
+
useSearchParams,
|
|
49
|
+
useSelectedLayoutSegment
|
|
50
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { ApiClient } from '@blu/sdk';
|
|
4
|
+
|
|
5
|
+
declare const DEFAULT_SCOPE = "blu-module";
|
|
6
|
+
/** What import.meta.glob returns: loaders (per-page chunks) or, with { eager: true }, the modules themselves. */
|
|
7
|
+
type GlobMap = Record<string, (() => Promise<unknown>) | unknown>;
|
|
8
|
+
/** Used by the generated proxies for your server actions: same arguments, same return value. */
|
|
9
|
+
declare function callServerAction(module: string, name: string, args: unknown[]): Promise<unknown>;
|
|
10
|
+
/** Your API, called from inside Blu: the module's own backend, with the Blu token. */
|
|
11
|
+
declare function bluApi(): ApiClient;
|
|
12
|
+
/** A root-relative URL of your own server ("/logo.svg"): unchanged standalone, absolute inside Blu. */
|
|
13
|
+
declare function moduleUrl(path: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* fetch() for your own API: plain fetch standalone; inside Blu, the same request
|
|
16
|
+
* to your server with the Blu token. Same Response (status codes and all). Only
|
|
17
|
+
* paths on your own server, so the token can't be sent anywhere else.
|
|
18
|
+
*/
|
|
19
|
+
declare function moduleFetch(path: string, init?: RequestInit): Promise<Response>;
|
|
20
|
+
/** The URL to put in src: `path` itself standalone; inside Blu, a blob: URL once loaded (null until then, or if it failed). */
|
|
21
|
+
declare function useModuleAssetUrl(path: string): string | null;
|
|
22
|
+
/** Warm the cache for an image you'll show next (new Image().src standalone). */
|
|
23
|
+
declare function preloadModuleAsset(path: string): void;
|
|
24
|
+
/** next/navigation useParams() inside Blu (via the shim). */
|
|
25
|
+
declare const useBluParams: () => Record<string, string>;
|
|
26
|
+
/** Thrown by the next/navigation shim's redirect() / notFound(); handled by <BluNextModule>. */
|
|
27
|
+
declare class BluRedirect extends Error {
|
|
28
|
+
readonly href: string;
|
|
29
|
+
constructor(href: string);
|
|
30
|
+
}
|
|
31
|
+
declare class BluNotFound extends Error {
|
|
32
|
+
constructor();
|
|
33
|
+
}
|
|
34
|
+
declare function BluNextModule({ css, scope, pages, layouts, exclude, children, }: {
|
|
35
|
+
/** Your app/globals.css, imported with ?inline (scoped by the Vite config). */
|
|
36
|
+
css?: string;
|
|
37
|
+
scope?: string;
|
|
38
|
+
pages: GlobMap;
|
|
39
|
+
/** Section layouts (not the root app/layout.tsx — your children(...) plays that part). */
|
|
40
|
+
layouts?: GlobMap;
|
|
41
|
+
/** Paths that don't belong in Blu (e.g. "/login"). */
|
|
42
|
+
exclude?: string[];
|
|
43
|
+
/** What your root layout renders around the page (providers, top bar, guards…). */
|
|
44
|
+
children: (page: ReactNode) => ReactNode;
|
|
45
|
+
}): react.JSX.Element;
|
|
46
|
+
|
|
47
|
+
export { BluNextModule, BluNotFound, BluRedirect, DEFAULT_SCOPE, bluApi, callServerAction, moduleFetch, moduleUrl, preloadModuleAsset, useBluParams, useModuleAssetUrl };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** For proxy.ts: answer a CORS preflight from an allowed Blu shell (or null if this isn't one). */
|
|
2
|
+
declare function bluPreflight(request: any, allowedOrigins: any): Response;
|
|
3
|
+
/** For proxy.ts / route handlers: allow an approved Blu shell to read this response. */
|
|
4
|
+
declare function withBluCors(request: any, response: any, allowedOrigins: any): any;
|
|
5
|
+
/**
|
|
6
|
+
* @param {{ moduleId: string, issuer: string, jwksUrl: string, apiUrl: string,
|
|
7
|
+
* roleMap: Record<string, string[]>, allowedOrigins: string[] }} opts
|
|
8
|
+
* roleMap — Blu role → permissions on your API (your mapping)
|
|
9
|
+
* apiUrl — this deployment's own URL (passwordless dev sign-ins only accepted when it's local)
|
|
10
|
+
*/
|
|
11
|
+
declare function createBluNextServer({ moduleId, issuer, jwksUrl, apiUrl, roleMap, allowedOrigins }: {
|
|
12
|
+
moduleId: string;
|
|
13
|
+
issuer: string;
|
|
14
|
+
jwksUrl: string;
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
roleMap: Record<string, string[]>;
|
|
17
|
+
allowedOrigins: string[];
|
|
18
|
+
}): {
|
|
19
|
+
principalFromHeaders: (headers: any) => Promise<{
|
|
20
|
+
authMode: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
issuer: string;
|
|
23
|
+
subject: any;
|
|
24
|
+
key: string;
|
|
25
|
+
devIdentity: any;
|
|
26
|
+
displayName: any;
|
|
27
|
+
email: any;
|
|
28
|
+
provider: any;
|
|
29
|
+
via: any;
|
|
30
|
+
actor: any;
|
|
31
|
+
clientId: any;
|
|
32
|
+
allowedClients: any;
|
|
33
|
+
roles: any;
|
|
34
|
+
permissions: any[];
|
|
35
|
+
} | {
|
|
36
|
+
authMode: string;
|
|
37
|
+
kind: string;
|
|
38
|
+
issuer: string;
|
|
39
|
+
/**
|
|
40
|
+
* user id, or api-key id
|
|
41
|
+
*/
|
|
42
|
+
subject?: string;
|
|
43
|
+
/**
|
|
44
|
+
* stable identity across requests: `<authMode>|<issuer>|<subject>`.
|
|
45
|
+
* Key your own records (audit, ownership, preferences) on this —
|
|
46
|
+
* a Blu user needs no account in your system first.
|
|
47
|
+
*/
|
|
48
|
+
key: string;
|
|
49
|
+
displayName?: string;
|
|
50
|
+
email?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Blu only: the app that obtained the token ("blu-host",
|
|
53
|
+
* "blu-concierge", or your own standalone app)
|
|
54
|
+
*/
|
|
55
|
+
via?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Blu only: software acting for the user (e.g. "blu-concierge")
|
|
58
|
+
*/
|
|
59
|
+
actor?: string;
|
|
60
|
+
/**
|
|
61
|
+
* active client (from the credential, never the request — except see api keys)
|
|
62
|
+
*/
|
|
63
|
+
clientId?: string | null;
|
|
64
|
+
allowedClients?: string[];
|
|
65
|
+
roles?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* resolved; may contain "*"
|
|
68
|
+
*/
|
|
69
|
+
permissions?: string[];
|
|
70
|
+
devIdentity: any;
|
|
71
|
+
} | {
|
|
72
|
+
subject: any;
|
|
73
|
+
displayName: any;
|
|
74
|
+
email: any;
|
|
75
|
+
clientId: any;
|
|
76
|
+
allowedClients: any;
|
|
77
|
+
roles: any[];
|
|
78
|
+
permissions: any;
|
|
79
|
+
authMode: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
issuer: string;
|
|
82
|
+
key: string;
|
|
83
|
+
devIdentity: any;
|
|
84
|
+
} | {
|
|
85
|
+
authMode: string;
|
|
86
|
+
kind: string;
|
|
87
|
+
issuer: string;
|
|
88
|
+
subject: string;
|
|
89
|
+
key: string;
|
|
90
|
+
displayName: string;
|
|
91
|
+
clientId: any;
|
|
92
|
+
allowedClients: string[];
|
|
93
|
+
roles: any[];
|
|
94
|
+
permissions: string[];
|
|
95
|
+
clientFromRequest: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
/** Strict: the Blu caller, or throws (status 401/403) — e.g. for createMcpWebHandler's `verify`. */
|
|
98
|
+
verifyRequest: (request: any) => Promise<{
|
|
99
|
+
authMode: string;
|
|
100
|
+
kind: string;
|
|
101
|
+
issuer: string;
|
|
102
|
+
subject: any;
|
|
103
|
+
key: string;
|
|
104
|
+
devIdentity: any;
|
|
105
|
+
displayName: any;
|
|
106
|
+
email: any;
|
|
107
|
+
provider: any;
|
|
108
|
+
via: any;
|
|
109
|
+
actor: any;
|
|
110
|
+
clientId: any;
|
|
111
|
+
allowedClients: any;
|
|
112
|
+
roles: any;
|
|
113
|
+
permissions: any[];
|
|
114
|
+
} | {
|
|
115
|
+
authMode: string;
|
|
116
|
+
kind: string;
|
|
117
|
+
issuer: string;
|
|
118
|
+
/**
|
|
119
|
+
* user id, or api-key id
|
|
120
|
+
*/
|
|
121
|
+
subject?: string;
|
|
122
|
+
/**
|
|
123
|
+
* stable identity across requests: `<authMode>|<issuer>|<subject>`.
|
|
124
|
+
* Key your own records (audit, ownership, preferences) on this —
|
|
125
|
+
* a Blu user needs no account in your system first.
|
|
126
|
+
*/
|
|
127
|
+
key: string;
|
|
128
|
+
displayName?: string;
|
|
129
|
+
email?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Blu only: the app that obtained the token ("blu-host",
|
|
132
|
+
* "blu-concierge", or your own standalone app)
|
|
133
|
+
*/
|
|
134
|
+
via?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Blu only: software acting for the user (e.g. "blu-concierge")
|
|
137
|
+
*/
|
|
138
|
+
actor?: string;
|
|
139
|
+
/**
|
|
140
|
+
* active client (from the credential, never the request — except see api keys)
|
|
141
|
+
*/
|
|
142
|
+
clientId?: string | null;
|
|
143
|
+
allowedClients?: string[];
|
|
144
|
+
roles?: string[];
|
|
145
|
+
/**
|
|
146
|
+
* resolved; may contain "*"
|
|
147
|
+
*/
|
|
148
|
+
permissions?: string[];
|
|
149
|
+
devIdentity: any;
|
|
150
|
+
} | {
|
|
151
|
+
subject: any;
|
|
152
|
+
displayName: any;
|
|
153
|
+
email: any;
|
|
154
|
+
clientId: any;
|
|
155
|
+
allowedClients: any;
|
|
156
|
+
roles: any[];
|
|
157
|
+
permissions: any;
|
|
158
|
+
authMode: string;
|
|
159
|
+
kind: string;
|
|
160
|
+
issuer: string;
|
|
161
|
+
key: string;
|
|
162
|
+
devIdentity: any;
|
|
163
|
+
} | {
|
|
164
|
+
authMode: string;
|
|
165
|
+
kind: string;
|
|
166
|
+
issuer: string;
|
|
167
|
+
subject: string;
|
|
168
|
+
key: string;
|
|
169
|
+
displayName: string;
|
|
170
|
+
clientId: any;
|
|
171
|
+
allowedClients: string[];
|
|
172
|
+
roles: any[];
|
|
173
|
+
permissions: string[];
|
|
174
|
+
clientFromRequest: boolean;
|
|
175
|
+
}>;
|
|
176
|
+
actionsRoute: (modules: any) => {
|
|
177
|
+
OPTIONS: (request: any) => Response;
|
|
178
|
+
POST(request: any): Promise<any>;
|
|
179
|
+
};
|
|
180
|
+
allowedOrigins: string[];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export { bluPreflight, createBluNextServer, withBluCors };
|
package/types/vite.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as vite from 'vite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ name: string, entry?: string, root?: string, actionsDir?: string, globalsCss?: string,
|
|
5
|
+
* scope?: string, tailwind?: object, aliases?: Record<string, string>, outDir?: string }} opts
|
|
6
|
+
* @returns {import("vite").UserConfig}
|
|
7
|
+
*/
|
|
8
|
+
declare function bluModule({ name, entry, root, actionsDir, globalsCss, scope, tailwind, aliases, outDir, }: {
|
|
9
|
+
name: string;
|
|
10
|
+
entry?: string;
|
|
11
|
+
root?: string;
|
|
12
|
+
actionsDir?: string;
|
|
13
|
+
globalsCss?: string;
|
|
14
|
+
scope?: string;
|
|
15
|
+
tailwind?: object;
|
|
16
|
+
aliases?: Record<string, string>;
|
|
17
|
+
outDir?: string;
|
|
18
|
+
}): vite.UserConfig;
|
|
19
|
+
declare const DEFAULT_SCOPE: "blu-module";
|
|
20
|
+
|
|
21
|
+
export { DEFAULT_SCOPE, bluModule };
|
package/vite.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// src/vite.js
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import react from "@vitejs/plugin-react";
|
|
6
|
+
import { federation } from "@module-federation/vite";
|
|
7
|
+
import tailwindcss from "tailwindcss";
|
|
8
|
+
|
|
9
|
+
// src/scope-css.js
|
|
10
|
+
function scopeCss(code, scope) {
|
|
11
|
+
const cls = `.${scope}`;
|
|
12
|
+
const tokensScoped = code.replace(/@tailwind\s+base\s*;?/g, "").replace(/:root\b/g, cls).replace(/(^|[\s,}])\.dark\b/g, `$1.dark ${cls}`).replace(/\bhtml\s*,\s*body\b/g, cls);
|
|
13
|
+
return unwrapBaseLayers(tokensScoped, cls);
|
|
14
|
+
}
|
|
15
|
+
var scopedReset = (cls) => `
|
|
16
|
+
${cls} { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; }
|
|
17
|
+
${cls} *, ${cls} *::before, ${cls} *::after { box-sizing: border-box; border: 0 solid var(--border-default, #e5e7eb); }
|
|
18
|
+
${cls} h1, ${cls} h2, ${cls} h3, ${cls} p { margin: 0; font-size: inherit; font-weight: inherit; }
|
|
19
|
+
${cls} ul, ${cls} ol { list-style: none; margin: 0; padding: 0; }
|
|
20
|
+
${cls} a { color: inherit; text-decoration: none; }
|
|
21
|
+
${cls} button { font: inherit; color: inherit; background: transparent; padding: 0; border-radius: 0; cursor: pointer; }
|
|
22
|
+
${cls} input, ${cls} select, ${cls} textarea { font: inherit; color: inherit; }
|
|
23
|
+
${cls} table { border-collapse: collapse; }
|
|
24
|
+
${cls} th { font-weight: inherit; text-align: inherit; }
|
|
25
|
+
${cls} svg, ${cls} img { display: inline-block; vertical-align: middle; }
|
|
26
|
+
`;
|
|
27
|
+
function unwrapBaseLayers(css, cls) {
|
|
28
|
+
let out = "";
|
|
29
|
+
let i = 0;
|
|
30
|
+
const re = /@layer\s+base\s*\{/g;
|
|
31
|
+
let m;
|
|
32
|
+
while (m = re.exec(css)) {
|
|
33
|
+
const open = m.index + m[0].length - 1;
|
|
34
|
+
const close = matchingBrace(css, open);
|
|
35
|
+
if (close < 0) break;
|
|
36
|
+
out += css.slice(i, m.index) + scopeRules(css.slice(open + 1, close), cls);
|
|
37
|
+
i = close + 1;
|
|
38
|
+
re.lastIndex = i;
|
|
39
|
+
}
|
|
40
|
+
return out + css.slice(i);
|
|
41
|
+
}
|
|
42
|
+
function scopeRules(block, cls) {
|
|
43
|
+
let out = "";
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < block.length) {
|
|
46
|
+
const ws = block.slice(i).match(/^\s*/)[0].length;
|
|
47
|
+
if (block.startsWith("/*", i + ws)) {
|
|
48
|
+
const end = block.indexOf("*/", i + ws + 2);
|
|
49
|
+
if (end < 0) return out + block.slice(i);
|
|
50
|
+
out += block.slice(i, end + 2);
|
|
51
|
+
i = end + 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const open = nextBrace(block, i);
|
|
55
|
+
if (open < 0) return out + block.slice(i);
|
|
56
|
+
const close = matchingBrace(block, open);
|
|
57
|
+
if (close < 0) return out + block.slice(i);
|
|
58
|
+
const prelude = block.slice(i, open);
|
|
59
|
+
const body = block.slice(open + 1, close);
|
|
60
|
+
const head = prelude.trim();
|
|
61
|
+
const lead = prelude.slice(0, prelude.length - prelude.trimStart().length);
|
|
62
|
+
if (head.startsWith("@")) out += `${lead}${head} {${scopeRules(body, cls)}}`;
|
|
63
|
+
else out += `${lead}${scopeSelectorList(head, cls)} {${body}}`;
|
|
64
|
+
i = close + 1;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function scopeSelectorList(list, cls) {
|
|
69
|
+
return splitSelectors(list).map((s) => s.trim()).filter(Boolean).map((s) => scopeSelector(s, cls)).join(", ");
|
|
70
|
+
}
|
|
71
|
+
function scopeSelector(sel, cls) {
|
|
72
|
+
if (sel.includes(cls)) return sel;
|
|
73
|
+
if (/^(html|body)$/.test(sel)) return cls;
|
|
74
|
+
if (/^(?:\.[\w-]+|\[[^\]]+\])+$/.test(sel)) return `${sel} ${cls}`;
|
|
75
|
+
return `${cls} ${sel}`;
|
|
76
|
+
}
|
|
77
|
+
function splitSelectors(list) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
let depth = 0;
|
|
80
|
+
let quote = null;
|
|
81
|
+
let start = 0;
|
|
82
|
+
for (let i = 0; i < list.length; i++) {
|
|
83
|
+
const c = list[i];
|
|
84
|
+
if (quote) {
|
|
85
|
+
if (c === quote && list[i - 1] !== "\\") quote = null;
|
|
86
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
87
|
+
else if (c === "(" || c === "[") depth++;
|
|
88
|
+
else if (c === ")" || c === "]") depth--;
|
|
89
|
+
else if (c === "," && depth === 0) {
|
|
90
|
+
parts.push(list.slice(start, i));
|
|
91
|
+
start = i + 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
parts.push(list.slice(start));
|
|
95
|
+
return parts;
|
|
96
|
+
}
|
|
97
|
+
function nextBrace(css, from) {
|
|
98
|
+
for (let i = from; i < css.length; i++) {
|
|
99
|
+
const c = css[i];
|
|
100
|
+
if (c === "/" && css[i + 1] === "*") {
|
|
101
|
+
const end = css.indexOf("*/", i + 2);
|
|
102
|
+
if (end < 0) return -1;
|
|
103
|
+
i = end + 1;
|
|
104
|
+
} else if (c === '"' || c === "'") {
|
|
105
|
+
let j = i + 1;
|
|
106
|
+
while (j < css.length && !(css[j] === c && css[j - 1] !== "\\")) j++;
|
|
107
|
+
i = j;
|
|
108
|
+
} else if (c === "{") return i;
|
|
109
|
+
}
|
|
110
|
+
return -1;
|
|
111
|
+
}
|
|
112
|
+
function matchingBrace(css, open) {
|
|
113
|
+
let depth = 0;
|
|
114
|
+
for (let i = open; i < css.length; i++) {
|
|
115
|
+
const c = css[i];
|
|
116
|
+
if (c === "/" && css[i + 1] === "*") {
|
|
117
|
+
const end = css.indexOf("*/", i + 2);
|
|
118
|
+
if (end < 0) return -1;
|
|
119
|
+
i = end + 1;
|
|
120
|
+
} else if (c === '"' || c === "'") {
|
|
121
|
+
let j = i + 1;
|
|
122
|
+
while (j < css.length && !(css[j] === c && css[j - 1] !== "\\")) j++;
|
|
123
|
+
i = j;
|
|
124
|
+
} else if (c === "{") depth++;
|
|
125
|
+
else if (c === "}" && --depth === 0) return i;
|
|
126
|
+
}
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/vite.js
|
|
131
|
+
import autoprefixer from "autoprefixer";
|
|
132
|
+
var here = path.dirname(fileURLToPath(import.meta.url));
|
|
133
|
+
var shim = (name) => {
|
|
134
|
+
for (const ext of [".js", ".tsx", ".ts"]) {
|
|
135
|
+
const file = path.join(here, "shims", name + ext);
|
|
136
|
+
if (existsSync(file)) return file;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`[@blu/next-adapter] missing shim "${name}"`);
|
|
139
|
+
};
|
|
140
|
+
var DEFAULT_SCOPE = "blu-module";
|
|
141
|
+
function bluModule({
|
|
142
|
+
name,
|
|
143
|
+
entry = "./blu/App.tsx",
|
|
144
|
+
root = process.cwd(),
|
|
145
|
+
actionsDir = "app/actions",
|
|
146
|
+
globalsCss = "app/globals.css",
|
|
147
|
+
scope = DEFAULT_SCOPE,
|
|
148
|
+
tailwind,
|
|
149
|
+
aliases = {},
|
|
150
|
+
outDir = "public/blu"
|
|
151
|
+
}) {
|
|
152
|
+
const abs = (p) => path.resolve(root, p);
|
|
153
|
+
return {
|
|
154
|
+
root,
|
|
155
|
+
base: "./",
|
|
156
|
+
// chunks resolve relative to remoteEntry — one build for every environment
|
|
157
|
+
publicDir: false,
|
|
158
|
+
plugins: [
|
|
159
|
+
react(),
|
|
160
|
+
serverActionsAsHttp(abs(actionsDir)),
|
|
161
|
+
scopeGlobalsCss(abs(globalsCss), scope),
|
|
162
|
+
federation({
|
|
163
|
+
name,
|
|
164
|
+
filename: "remoteEntry.js",
|
|
165
|
+
manifest: true,
|
|
166
|
+
dts: false,
|
|
167
|
+
exposes: { "./App": entry },
|
|
168
|
+
shared: { react: { singleton: true }, "react-dom": { singleton: true }, "@blu/sdk": { singleton: true } }
|
|
169
|
+
})
|
|
170
|
+
],
|
|
171
|
+
resolve: {
|
|
172
|
+
alias: [
|
|
173
|
+
...Object.entries(aliases).map(([find, to]) => ({ find: new RegExp(`^${find.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}$`), replacement: abs(to) })),
|
|
174
|
+
{ find: /^next\/link$/, replacement: shim("link") },
|
|
175
|
+
{ find: /^next\/navigation$/, replacement: shim("navigation") },
|
|
176
|
+
{ find: /^next\/image$/, replacement: shim("image") },
|
|
177
|
+
{ find: /^@\//, replacement: `${root}/` }
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
css: {
|
|
181
|
+
postcss: {
|
|
182
|
+
plugins: [tailwindcss({ ...tailwind ?? {}, important: `.${scope}`, corePlugins: { ...tailwind?.corePlugins ?? {}, preflight: false } }), autoprefixer()]
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
build: { target: "esnext", outDir: abs(outDir), emptyOutDir: true, rollupOptions: { input: {} } }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function serverActionsAsHttp(actionsDir) {
|
|
189
|
+
return {
|
|
190
|
+
name: "blu:server-actions-as-http",
|
|
191
|
+
enforce: "pre",
|
|
192
|
+
load(id) {
|
|
193
|
+
const file = id.split("?")[0];
|
|
194
|
+
if (!file.startsWith(actionsDir + path.sep) || !/\.(ts|tsx|js|mjs)$/.test(file)) return null;
|
|
195
|
+
const source = readFileSync(file, "utf8");
|
|
196
|
+
if (!/^\s*(\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*["']use server["']/.test(source)) return null;
|
|
197
|
+
const names = /* @__PURE__ */ new Set([
|
|
198
|
+
...[...source.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)].map((m) => m[1]),
|
|
199
|
+
...[...source.matchAll(/export\s+const\s+(\w+)\s*=/g)].map((m) => m[1])
|
|
200
|
+
]);
|
|
201
|
+
const mod = path.relative(actionsDir, file).replace(/\.(ts|tsx|js|mjs)$/, "").split(path.sep).join("/");
|
|
202
|
+
return [
|
|
203
|
+
`import { callServerAction } from "@blu/next-adapter/client";`,
|
|
204
|
+
...[...names].map((n) => `export const ${n} = (...args) => callServerAction(${JSON.stringify(mod)}, ${JSON.stringify(n)}, args);`)
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function scopeGlobalsCss(globalsCss, scope) {
|
|
210
|
+
const cls = `.${scope}`;
|
|
211
|
+
return {
|
|
212
|
+
name: "blu:scope-globals-css",
|
|
213
|
+
enforce: "pre",
|
|
214
|
+
transform(code, id) {
|
|
215
|
+
if (id.split("?")[0] !== globalsCss) return null;
|
|
216
|
+
return `${scopeCss(code, scope)}
|
|
217
|
+
${scopedReset(cls)}`;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
DEFAULT_SCOPE,
|
|
223
|
+
bluModule
|
|
224
|
+
};
|