@fluid-app/fluid-cli-portal 0.1.52 → 0.1.53
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 +3 -0
- package/dist/build-manifest.d.mts +91 -0
- package/dist/build-manifest.d.mts.map +1 -0
- package/dist/build-manifest.mjs +180 -0
- package/dist/build-manifest.mjs.map +1 -0
- package/dist/index.d.mts +234 -519
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1467 -573
- package/dist/index.mjs.map +1 -1
- package/dist/portal-dev-plugin-ByYelgFg.mjs +232 -0
- package/dist/portal-dev-plugin-ByYelgFg.mjs.map +1 -0
- package/dist/{pull-XOsFaqz2.mjs → pull-C3yj9L07.mjs} +44 -16
- package/dist/pull-C3yj9L07.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +12 -56
- package/dist/vite-plugin.d.mts.map +1 -1
- package/dist/vite-plugin.mjs +3 -2
- package/package.json +8 -1
- package/templates/base/.gitignore.template +6 -0
- package/templates/base/.oxlintrc.json +1 -2
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +126 -10
- package/templates/starter/.env.example +4 -3
- package/templates/starter/README.md.template +14 -1
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs +0 -289
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs.map +0 -1
- package/dist/pull-XOsFaqz2.mjs.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
|
|
2
|
+
import { buildManifestFromLocalFiles } from "./build-manifest.mjs";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { getAuthToken, getFluidApiBase } from "@fluid-app/fluid-cli";
|
|
6
|
+
//#region src/vite-plugin/portal-dev-plugin.ts
|
|
7
|
+
/**
|
|
8
|
+
* Vite plugin for local portal content development.
|
|
9
|
+
*
|
|
10
|
+
* Intercepts the manifest API request and serves content assembled from the
|
|
11
|
+
* local `portal/` directory. Watches portal files for changes and triggers a
|
|
12
|
+
* full page reload.
|
|
13
|
+
*
|
|
14
|
+
* The manifest is intercepted on BOTH paths the SDK has used across versions
|
|
15
|
+
* (`/api/app/manifest` since portal-sdk 0.1.4xx, `/api/fluid_os/definitions/
|
|
16
|
+
* active` before that) so a newer SDK against an older-generation app — or
|
|
17
|
+
* vice versa — still gets the local draft.
|
|
18
|
+
*/
|
|
19
|
+
var portal_dev_plugin_exports = /* @__PURE__ */ __exportAll({
|
|
20
|
+
portalDevPlugin: () => portalDevPlugin,
|
|
21
|
+
resolvePortalTenantApiTarget: () => resolvePortalTenantApiTarget
|
|
22
|
+
});
|
|
23
|
+
const DEBOUNCE_MS = 100;
|
|
24
|
+
/** Manifest endpoints across SDK generations — all serve the local draft. */
|
|
25
|
+
const MANIFEST_PATHS = ["/api/app/manifest", "/api/fluid_os/definitions/active"];
|
|
26
|
+
const DEFAULT_API_URL = "https://api.fluid.app";
|
|
27
|
+
const TENANT_LOOKUP_TIMEOUT_MS = 3e3;
|
|
28
|
+
const tenantTargetPromises = /* @__PURE__ */ new WeakMap();
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the portal tenant BFF for the CLI's project-aware Fluid profile.
|
|
31
|
+
*
|
|
32
|
+
* Portal member APIs do not live on api.fluid.app or `<tenant>.fluid.app`.
|
|
33
|
+
* Their OpenAPI server is `<tenant>.portal.fluid.app`, and using either of
|
|
34
|
+
* the former hosts produces a wall of unrelated 404s in otherwise-valid
|
|
35
|
+
* local portals.
|
|
36
|
+
*/
|
|
37
|
+
async function resolvePortalTenantApiTarget(dependencies = {}) {
|
|
38
|
+
const token = dependencies.token !== void 0 ? dependencies.token : getAuthToken();
|
|
39
|
+
if (!token) return void 0;
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), dependencies.timeoutMs ?? TENANT_LOOKUP_TIMEOUT_MS);
|
|
42
|
+
try {
|
|
43
|
+
const response = await (dependencies.fetchImpl ?? fetch)(`${(dependencies.apiBase ?? getFluidApiBase()).replace(/\/+$/, "")}/api/company/v1/companies/me`, {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${token}`,
|
|
46
|
+
Accept: "application/json"
|
|
47
|
+
},
|
|
48
|
+
signal: controller.signal
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) return void 0;
|
|
51
|
+
const subdomain = (await response.json()).data?.company?.subdomain;
|
|
52
|
+
if (typeof subdomain !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(subdomain)) return;
|
|
53
|
+
return `https://${subdomain}.portal.fluid.app`;
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fallback .env reader for VITE_API_URL, used only when vite's `loadEnv`
|
|
62
|
+
* can't be imported (it's a declared dependency, so this is defensive — but
|
|
63
|
+
* a broken install must degrade to "wrong host" rather than "dev server
|
|
64
|
+
* won't boot"). Mirrors the basic lookup order: live env > .env.local >
|
|
65
|
+
* .env. Unlike loadEnv it ignores .env.[mode] files and variable expansion.
|
|
66
|
+
*/
|
|
67
|
+
function readViteApiUrlFallback(root) {
|
|
68
|
+
if (process.env.VITE_API_URL) return process.env.VITE_API_URL;
|
|
69
|
+
if (process.env.NEXT_PUBLIC_API_BASE_URL) return process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
70
|
+
for (const file of [".env.local", ".env"]) {
|
|
71
|
+
const path = join(root, file);
|
|
72
|
+
if (!existsSync(path)) continue;
|
|
73
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
74
|
+
const match = /^\s*VITE_API_URL\s*=\s*(?:"([^"]*)"|(.*?))\s*(?:#.*)?$/.exec(line);
|
|
75
|
+
const value = match?.[1] ?? match?.[2];
|
|
76
|
+
if (value) return value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function storefrontSubdomainFromTarget(target) {
|
|
81
|
+
try {
|
|
82
|
+
const hostname = new URL(target).hostname;
|
|
83
|
+
const subdomain = /^([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\.fluid\.app$/.exec(hostname)?.[1];
|
|
84
|
+
if (!subdomain || [
|
|
85
|
+
"admin",
|
|
86
|
+
"api",
|
|
87
|
+
"app",
|
|
88
|
+
"docs",
|
|
89
|
+
"portal",
|
|
90
|
+
"upload"
|
|
91
|
+
].includes(subdomain)) return;
|
|
92
|
+
return subdomain;
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve VITE_API_URL the way vite itself would — `loadEnv` honors
|
|
99
|
+
* .env.[mode], .env.[mode].local, envDir, and variable expansion, which the
|
|
100
|
+
* fallback parser does not (a mode-specific staging API must not silently
|
|
101
|
+
* fall back to production).
|
|
102
|
+
*/
|
|
103
|
+
async function resolveViteApiUrl(root, mode, envDir) {
|
|
104
|
+
try {
|
|
105
|
+
const { loadEnv } = await import("vite");
|
|
106
|
+
const env = loadEnv(mode, envDir ?? root, ["VITE_", "NEXT_PUBLIC_"]);
|
|
107
|
+
return env.VITE_API_URL || process.env.VITE_API_URL || env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || void 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return readViteApiUrlFallback(root);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function portalDevPlugin(options) {
|
|
113
|
+
let portalDir;
|
|
114
|
+
let cachedManifest = null;
|
|
115
|
+
let buildInFlight = null;
|
|
116
|
+
/** Bumped on every watched file change. A build carries the value it
|
|
117
|
+
* started under; if it no longer matches when the build resolves, the
|
|
118
|
+
* build read pre-edit state and must not be cached. */
|
|
119
|
+
let manifestGeneration = 0;
|
|
120
|
+
const resolveTenantTarget = options?.resolveTenantApiTarget ?? resolvePortalTenantApiTarget;
|
|
121
|
+
return {
|
|
122
|
+
name: "fluid-portal-dev",
|
|
123
|
+
enforce: "pre",
|
|
124
|
+
async config(config, env) {
|
|
125
|
+
if (env?.command !== "serve" || options?.sameOriginDev === false) return {};
|
|
126
|
+
const root = config.root ?? process.cwd();
|
|
127
|
+
const appDefinesProxy = config.server?.proxy?.["/api"] !== void 0;
|
|
128
|
+
const configuredTarget = options?.apiProxyTarget || appDefinesProxy ? void 0 : await resolveViteApiUrl(root, env?.mode ?? "development", config.envDir);
|
|
129
|
+
const tenantTarget = options?.apiProxyTarget || appDefinesProxy || configuredTarget ? void 0 : await (() => {
|
|
130
|
+
const existing = tenantTargetPromises.get(resolveTenantTarget);
|
|
131
|
+
if (existing) return existing;
|
|
132
|
+
const pending = resolveTenantTarget();
|
|
133
|
+
tenantTargetPromises.set(resolveTenantTarget, pending);
|
|
134
|
+
return pending;
|
|
135
|
+
})();
|
|
136
|
+
const target = options?.apiProxyTarget ?? configuredTarget ?? tenantTarget ?? DEFAULT_API_URL;
|
|
137
|
+
const storefrontSubdomain = storefrontSubdomainFromTarget(target);
|
|
138
|
+
if (tenantTarget) {
|
|
139
|
+
console.log(`[fluid-portal-dev] Tenant API proxy: ${tenantTarget}`);
|
|
140
|
+
console.log("[fluid-portal-dev] Member data still requires a portal handoff session; localhost requests may return 401.");
|
|
141
|
+
} else if (!options?.apiProxyTarget && !appDefinesProxy && !configuredTarget) console.warn(`[fluid-portal-dev] Could not resolve the company tenant API; falling back to ${DEFAULT_API_URL}. Built-in member screens may not load. Set VITE_API_URL explicitly or sign in with the project's Fluid profile.`);
|
|
142
|
+
else if (storefrontSubdomain) console.warn(`[fluid-portal-dev] ${target} is the storefront host, not the Portal tenant BFF. Built-in member APIs live at https://${storefrontSubdomain}.portal.fluid.app; remove the override to let the CLI resolve it.`);
|
|
143
|
+
const defines = {};
|
|
144
|
+
for (const key of ["import.meta.env.VITE_API_URL", "import.meta.env.NEXT_PUBLIC_API_BASE_URL"]) if (config.define?.[key] === void 0) defines[key] = JSON.stringify("");
|
|
145
|
+
return {
|
|
146
|
+
define: defines,
|
|
147
|
+
server: { proxy: !appDefinesProxy ? { "/api": {
|
|
148
|
+
target,
|
|
149
|
+
changeOrigin: true
|
|
150
|
+
} } : {} }
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
configResolved(config) {
|
|
154
|
+
const root = config.root ?? process.cwd();
|
|
155
|
+
portalDir = options?.portalDir ? resolve(root, options.portalDir) : join(root, "portal");
|
|
156
|
+
},
|
|
157
|
+
configureServer(server) {
|
|
158
|
+
if (server.__fluidPortalDevWired) return;
|
|
159
|
+
server.__fluidPortalDevWired = true;
|
|
160
|
+
const watchDir = portalDir;
|
|
161
|
+
if (existsSync(watchDir)) {
|
|
162
|
+
server.watcher.add(watchDir);
|
|
163
|
+
let reloadTimer = null;
|
|
164
|
+
const onFileChange = (filePath) => {
|
|
165
|
+
if (!filePath.startsWith(watchDir)) return;
|
|
166
|
+
cachedManifest = null;
|
|
167
|
+
buildInFlight = null;
|
|
168
|
+
manifestGeneration += 1;
|
|
169
|
+
console.log(`[fluid-portal-dev] File changed: ${filePath.replace(watchDir + sep, "")}`);
|
|
170
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
171
|
+
reloadTimer = setTimeout(() => {
|
|
172
|
+
reloadTimer = null;
|
|
173
|
+
server.ws.send({
|
|
174
|
+
type: "full-reload",
|
|
175
|
+
path: "*"
|
|
176
|
+
});
|
|
177
|
+
}, DEBOUNCE_MS);
|
|
178
|
+
};
|
|
179
|
+
server.watcher.on("change", onFileChange);
|
|
180
|
+
server.watcher.on("add", onFileChange);
|
|
181
|
+
server.watcher.on("unlink", onFileChange);
|
|
182
|
+
}
|
|
183
|
+
server.middlewares.use((req, res, next) => {
|
|
184
|
+
const url = req.url;
|
|
185
|
+
if (!url || !MANIFEST_PATHS.some((path) => url.includes(path))) {
|
|
186
|
+
next();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
(async () => {
|
|
190
|
+
try {
|
|
191
|
+
if (!existsSync(portalDir)) {
|
|
192
|
+
next();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let manifestJson = cachedManifest;
|
|
196
|
+
if (manifestJson == null) {
|
|
197
|
+
if (!buildInFlight) {
|
|
198
|
+
const generation = manifestGeneration;
|
|
199
|
+
buildInFlight = buildManifestFromLocalFiles(portalDir).then((m) => {
|
|
200
|
+
const json = JSON.stringify(m);
|
|
201
|
+
if (generation === manifestGeneration) {
|
|
202
|
+
cachedManifest = json;
|
|
203
|
+
buildInFlight = null;
|
|
204
|
+
}
|
|
205
|
+
return json;
|
|
206
|
+
}).catch((err) => {
|
|
207
|
+
if (generation === manifestGeneration) buildInFlight = null;
|
|
208
|
+
throw err;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
manifestJson = await buildInFlight;
|
|
212
|
+
}
|
|
213
|
+
res.writeHead(200, {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
"X-Portal-Dev-Mode": "true",
|
|
216
|
+
"Cache-Control": "no-store"
|
|
217
|
+
});
|
|
218
|
+
res.end(manifestJson);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error("[fluid-portal-dev] Error building manifest:", err);
|
|
221
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
222
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Failed to build manifest from local files" }));
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { portal_dev_plugin_exports as n, portalDevPlugin as t };
|
|
231
|
+
|
|
232
|
+
//# sourceMappingURL=portal-dev-plugin-ByYelgFg.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portal-dev-plugin-ByYelgFg.mjs","names":[],"sources":["../src/vite-plugin/portal-dev-plugin.ts"],"sourcesContent":["/**\n * Vite plugin for local portal content development.\n *\n * Intercepts the manifest API request and serves content assembled from the\n * local `portal/` directory. Watches portal files for changes and triggers a\n * full page reload.\n *\n * The manifest is intercepted on BOTH paths the SDK has used across versions\n * (`/api/app/manifest` since portal-sdk 0.1.4xx, `/api/fluid_os/definitions/\n * active` before that) so a newer SDK against an older-generation app — or\n * vice versa — still gets the local draft.\n */\n\nimport { join, resolve, sep } from \"node:path\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { getAuthToken, getFluidApiBase } from \"@fluid-app/fluid-cli\";\nimport { buildManifestFromLocalFiles } from \"./build-manifest.js\";\n\n/** Options for the portal dev plugin. */\nexport interface PortalDevPluginOptions {\n portalDir?: string;\n /**\n * When true (default), dev-server requests are forced same-origin: the\n * plugin blanks `import.meta.env.VITE_API_URL` in serve mode and proxies\n * `/api` to the configured Fluid host instead. Without this, a `.env`\n * pointing VITE_API_URL at a remote API makes the SPA fetch its manifest\n * cross-origin — silently bypassing this plugin and rendering the remote\n * app instead of the local `portal/` draft. Set false to opt out and let\n * the SPA call the remote API directly.\n */\n sameOriginDev?: boolean;\n /**\n * Override the `/api` dev-proxy target. Defaults to VITE_API_URL from the\n * process env / .env files, then the authenticated company's\n * `<subdomain>.portal.fluid.app` tenant BFF, then https://api.fluid.app.\n */\n apiProxyTarget?: string;\n /**\n * Override tenant-target discovery. Primarily useful to keep embedded plugin\n * integrations and tests deterministic.\n */\n resolveTenantApiTarget?: () => Promise<string | undefined>;\n}\n\nconst DEBOUNCE_MS = 100;\n\n/** Manifest endpoints across SDK generations — all serve the local draft. */\nconst MANIFEST_PATHS = [\n \"/api/app/manifest\",\n \"/api/fluid_os/definitions/active\",\n];\n\nconst DEFAULT_API_URL = \"https://api.fluid.app\";\nconst TENANT_LOOKUP_TIMEOUT_MS = 3_000;\nconst tenantTargetPromises = new WeakMap<\n () => Promise<string | undefined>,\n Promise<string | undefined>\n>();\n\ninterface CompanyMeResponse {\n data?: {\n company?: {\n subdomain?: unknown;\n };\n };\n}\n\nexport interface TenantApiTargetDependencies {\n token?: string | null;\n apiBase?: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}\n\n/**\n * Resolve the portal tenant BFF for the CLI's project-aware Fluid profile.\n *\n * Portal member APIs do not live on api.fluid.app or `<tenant>.fluid.app`.\n * Their OpenAPI server is `<tenant>.portal.fluid.app`, and using either of\n * the former hosts produces a wall of unrelated 404s in otherwise-valid\n * local portals.\n */\nexport async function resolvePortalTenantApiTarget(\n dependencies: TenantApiTargetDependencies = {},\n): Promise<string | undefined> {\n const token =\n dependencies.token !== undefined ? dependencies.token : getAuthToken();\n if (!token) return undefined;\n\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(),\n dependencies.timeoutMs ?? TENANT_LOOKUP_TIMEOUT_MS,\n );\n\n try {\n const response = await (dependencies.fetchImpl ?? fetch)(\n `${(dependencies.apiBase ?? getFluidApiBase()).replace(/\\/+$/, \"\")}/api/company/v1/companies/me`,\n {\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n },\n signal: controller.signal,\n },\n );\n if (!response.ok) return undefined;\n\n const body = (await response.json()) as CompanyMeResponse;\n const subdomain = body.data?.company?.subdomain;\n if (\n typeof subdomain !== \"string\" ||\n !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(subdomain)\n ) {\n return undefined;\n }\n\n return `https://${subdomain}.portal.fluid.app`;\n } catch {\n return undefined;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Fallback .env reader for VITE_API_URL, used only when vite's `loadEnv`\n * can't be imported (it's a declared dependency, so this is defensive — but\n * a broken install must degrade to \"wrong host\" rather than \"dev server\n * won't boot\"). Mirrors the basic lookup order: live env > .env.local >\n * .env. Unlike loadEnv it ignores .env.[mode] files and variable expansion.\n */\nfunction readViteApiUrlFallback(root: string): string | undefined {\n if (process.env.VITE_API_URL) return process.env.VITE_API_URL;\n if (process.env.NEXT_PUBLIC_API_BASE_URL) {\n return process.env.NEXT_PUBLIC_API_BASE_URL;\n }\n for (const file of [\".env.local\", \".env\"]) {\n const path = join(root, file);\n if (!existsSync(path)) continue;\n for (const line of readFileSync(path, \"utf8\").split(\"\\n\")) {\n // Quoted values stop at the closing `\"`; unquoted values stop before\n // an inline ` # comment` tail (dotenv semantics — a trailing comment\n // must not leak into the proxy target URL).\n const match =\n /^\\s*VITE_API_URL\\s*=\\s*(?:\"([^\"]*)\"|(.*?))\\s*(?:#.*)?$/.exec(line);\n const value = match?.[1] ?? match?.[2];\n if (value) return value;\n }\n }\n return undefined;\n}\n\nfunction storefrontSubdomainFromTarget(target: string): string | undefined {\n try {\n const hostname = new URL(target).hostname;\n const match = /^([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\\.fluid\\.app$/.exec(\n hostname,\n );\n const subdomain = match?.[1];\n if (\n !subdomain ||\n [\"admin\", \"api\", \"app\", \"docs\", \"portal\", \"upload\"].includes(subdomain)\n ) {\n return undefined;\n }\n return subdomain;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve VITE_API_URL the way vite itself would — `loadEnv` honors\n * .env.[mode], .env.[mode].local, envDir, and variable expansion, which the\n * fallback parser does not (a mode-specific staging API must not silently\n * fall back to production).\n */\nasync function resolveViteApiUrl(\n root: string,\n mode: string,\n envDir: string | undefined,\n): Promise<string | undefined> {\n try {\n const { loadEnv } = await import(\"vite\");\n const env = loadEnv(mode, envDir ?? root, [\"VITE_\", \"NEXT_PUBLIC_\"]);\n return (\n env.VITE_API_URL ||\n process.env.VITE_API_URL ||\n // Portals migrated from Next.js conventions configure the API host as\n // NEXT_PUBLIC_API_BASE_URL — honored by `fluid portal dev` since it\n // was introduced, so it stays a recognized source here.\n env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n undefined\n );\n } catch {\n return readViteApiUrlFallback(root);\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function portalDevPlugin(options?: PortalDevPluginOptions): any {\n let portalDir: string;\n let cachedManifest: string | null = null;\n let buildInFlight: Promise<string> | null = null;\n /** Bumped on every watched file change. A build carries the value it\n * started under; if it no longer matches when the build resolves, the\n * build read pre-edit state and must not be cached. */\n let manifestGeneration = 0;\n const resolveTenantTarget =\n options?.resolveTenantApiTarget ?? resolvePortalTenantApiTarget;\n\n return {\n name: \"fluid-portal-dev\",\n enforce: \"pre\" as const,\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async config(config: any, env: any) {\n if (env?.command !== \"serve\" || options?.sameOriginDev === false) {\n return {};\n }\n\n const root: string = config.root ?? process.cwd();\n const appDefinesProxy = config.server?.proxy?.[\"/api\"] !== undefined;\n const configuredTarget =\n options?.apiProxyTarget || appDefinesProxy\n ? undefined\n : await resolveViteApiUrl(\n root,\n env?.mode ?? \"development\",\n config.envDir,\n );\n const tenantTarget =\n options?.apiProxyTarget || appDefinesProxy || configuredTarget\n ? undefined\n : await (() => {\n const existing = tenantTargetPromises.get(resolveTenantTarget);\n if (existing) return existing;\n\n const pending = resolveTenantTarget();\n tenantTargetPromises.set(resolveTenantTarget, pending);\n return pending;\n })();\n const target =\n options?.apiProxyTarget ??\n configuredTarget ??\n tenantTarget ??\n DEFAULT_API_URL;\n const storefrontSubdomain = storefrontSubdomainFromTarget(target);\n\n if (tenantTarget) {\n console.log(`[fluid-portal-dev] Tenant API proxy: ${tenantTarget}`);\n console.log(\n \"[fluid-portal-dev] Member data still requires a portal handoff session; localhost requests may return 401.\",\n );\n } else if (\n !options?.apiProxyTarget &&\n !appDefinesProxy &&\n !configuredTarget\n ) {\n console.warn(\n `[fluid-portal-dev] Could not resolve the company tenant API; falling back to ${DEFAULT_API_URL}. ` +\n \"Built-in member screens may not load. Set VITE_API_URL explicitly or sign in with the project's Fluid profile.\",\n );\n } else if (storefrontSubdomain) {\n console.warn(\n `[fluid-portal-dev] ${target} is the storefront host, not the Portal tenant BFF. ` +\n `Built-in member APIs live at https://${storefrontSubdomain}.portal.fluid.app; remove the override to let the CLI resolve it.`,\n );\n }\n\n // Vite merges a plugin's returned config OVER the user config, so an\n // app's own explicit define / /api proxy is only preserved by checking\n // for it and yielding here. BOTH API-host env vars must be blanked:\n // dev.ts's envPrefix exposes NEXT_PUBLIC_API_BASE_URL to the SPA too,\n // so leaving it defined would recreate the exact cross-origin bypass\n // this hook exists to prevent, just for Next.js-migrated portals.\n const defines: Record<string, string> = {};\n for (const key of [\n \"import.meta.env.VITE_API_URL\",\n \"import.meta.env.NEXT_PUBLIC_API_BASE_URL\",\n ]) {\n if (config.define?.[key] === undefined) {\n defines[key] = JSON.stringify(\"\");\n }\n }\n const proxy = !appDefinesProxy\n ? { \"/api\": { target, changeOrigin: true } }\n : {};\n\n return {\n define: defines,\n server: { proxy },\n };\n },\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configResolved(config: any) {\n const root: string = config.root ?? process.cwd();\n portalDir = options?.portalDir\n ? resolve(root, options.portalDir)\n : join(root, \"portal\");\n },\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configureServer(server: any) {\n // Idempotency: `fluid portal dev` injects this plugin, and a portal's\n // own vite.config may also wire it. Only the first instance registers\n // the watcher + middleware so we don't double-reload or double-serve.\n if (server.__fluidPortalDevWired) return;\n server.__fluidPortalDevWired = true;\n\n const watchDir = portalDir;\n\n if (existsSync(watchDir)) {\n server.watcher.add(watchDir);\n\n let reloadTimer: ReturnType<typeof setTimeout> | null = null;\n\n const onFileChange = (filePath: string) => {\n if (!filePath.startsWith(watchDir)) return;\n\n cachedManifest = null;\n buildInFlight = null;\n manifestGeneration += 1;\n\n console.log(\n `[fluid-portal-dev] File changed: ${filePath.replace(watchDir + sep, \"\")}`,\n );\n\n if (reloadTimer) clearTimeout(reloadTimer);\n reloadTimer = setTimeout(() => {\n reloadTimer = null;\n server.ws.send({ type: \"full-reload\", path: \"*\" });\n }, DEBOUNCE_MS);\n };\n\n server.watcher.on(\"change\", onFileChange);\n server.watcher.on(\"add\", onFileChange);\n server.watcher.on(\"unlink\", onFileChange);\n }\n\n // ── Middleware to intercept manifest requests ───────────────────\n // Every MANIFEST_PATHS route returns the SAME `{ manifest }` payload:\n // the portal-tenant BFF route the current SDK runtime fetches\n // (useFluidApp → app_manifest_show) and the legacy FluidOS route, so\n // the local portal/ draft feeds the running app regardless of which\n // runtime generation it uses (the SDK adapter reads\n // `response.manifest`, matching buildManifestFromLocalFiles' output).\n server.middlewares.use(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (req: any, res: any, next: () => void) => {\n const url: string | undefined = req.url;\n\n if (!url || !MANIFEST_PATHS.some((path) => url.includes(path))) {\n next();\n return;\n }\n\n (async () => {\n try {\n if (!existsSync(portalDir)) {\n // Fall through to the Vite proxy so requests reach the real\n // API when local portal content is unavailable (e.g. --skip-pull).\n next();\n return;\n }\n\n let manifestJson = cachedManifest;\n if (manifestJson == null) {\n if (!buildInFlight) {\n // Stamp the build with the generation it was started\n // under. onFileChange bumps that counter, so a build\n // whose inputs changed underneath it can be recognized\n // when it finally resolves.\n const generation = manifestGeneration;\n buildInFlight = buildManifestFromLocalFiles(portalDir)\n .then((m) => {\n const json = JSON.stringify(m);\n // Only a build still current may populate the\n // cache. Caching a superseded one pinned the\n // pre-edit snapshot permanently: a non-null cache\n // means no later request ever rebuilds, so the\n // edit stayed invisible until some unrelated file\n // happened to change outside a build window.\n if (generation === manifestGeneration) {\n cachedManifest = json;\n buildInFlight = null;\n }\n return json;\n })\n .catch((err) => {\n // Same guard: a superseded build must not clear a\n // newer one's handle.\n if (generation === manifestGeneration) {\n buildInFlight = null; // allow retry on next request\n }\n throw err;\n });\n }\n // This response serves what was built for it, which is\n // correct even when superseded — the request predates\n // the edit. The point is that the NEXT one rebuilds.\n manifestJson = await buildInFlight;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"X-Portal-Dev-Mode\": \"true\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(manifestJson);\n } catch (err) {\n console.error(\"[fluid-portal-dev] Error building manifest:\", err);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error:\n err instanceof Error\n ? err.message\n : \"Failed to build manifest from local files\",\n }),\n );\n }\n })();\n },\n );\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -3,12 +3,43 @@ import { Command } from "commander";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
8
|
import { getActiveProfile, getAuthToken } from "@fluid-app/fluid-cli";
|
|
8
9
|
import prompts from "prompts";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
10
|
-
import pLimit from "p-limit";
|
|
11
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import pLimit from "p-limit";
|
|
12
|
+
//#region src/commands/pull-definition.ts
|
|
13
|
+
/** Pick a definition by explicit id, else by name.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors clone.ts's resolver of the same name, and for the same
|
|
16
|
+
* reason: definition NAMES REPEAT. clone's `--id` help says so
|
|
17
|
+
* outright ("avoids ambiguity when names repeat") and clone.test.ts
|
|
18
|
+
* pins it. pull only ever looked up by name — first match wins — so a
|
|
19
|
+
* caller holding an unambiguous id had no way to use it, and Mist
|
|
20
|
+
* called pull by name immediately after cloning by id.
|
|
21
|
+
*
|
|
22
|
+
* Returns null when neither is supplied, so the caller falls through
|
|
23
|
+
* to the interactive selector. */
|
|
24
|
+
function resolveDefinition(definitions, appName, rawDefinitionId) {
|
|
25
|
+
if (rawDefinitionId !== void 0) {
|
|
26
|
+
if (!/^[1-9]\d*$/.test(rawDefinitionId)) throw new Error("--id must be a positive integer");
|
|
27
|
+
const definitionId = Number.parseInt(rawDefinitionId, 10);
|
|
28
|
+
const match = definitions.find((d) => d.id === definitionId);
|
|
29
|
+
if (!match) throw new Error(`No definition found with id ${definitionId}.`);
|
|
30
|
+
return match;
|
|
31
|
+
}
|
|
32
|
+
if (appName) {
|
|
33
|
+
const match = definitions.find((d) => d.name?.toLowerCase() === appName.toLowerCase());
|
|
34
|
+
if (!match) {
|
|
35
|
+
const availableNames = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
36
|
+
throw new Error(`No definition found matching "${appName}". Available: ${availableNames}`);
|
|
37
|
+
}
|
|
38
|
+
return match;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
12
43
|
//#region ../../platform/api-client-core/src/fetch-client.ts
|
|
13
44
|
/**
|
|
14
45
|
* API Error class compatible with fluid-admin's ApiError
|
|
@@ -118,7 +149,10 @@ function createFetchClient(config) {
|
|
|
118
149
|
throw new ApiError(errorText.slice(0, 200) || `${method} request failed with status ${response.status}`, response.status, null, headerRequestId);
|
|
119
150
|
}
|
|
120
151
|
const nestedError = typeof data.error === "object" && data.error !== null ? data.error.message : void 0;
|
|
121
|
-
|
|
152
|
+
const directError = typeof data.error === "string" ? data.error : void 0;
|
|
153
|
+
const message = typeof data.message === "string" ? data.message : void 0;
|
|
154
|
+
const errorMessage = typeof data.error_message === "string" ? data.error_message : void 0;
|
|
155
|
+
throw new ApiError(message || errorMessage || directError || (typeof nestedError === "string" ? nestedError : void 0) || `${method} request failed with status ${response.status}`, response.status, data.errors || data, headerRequestId ?? getRequestIdFromJsonBody(data));
|
|
122
156
|
} else throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null, headerRequestId);
|
|
123
157
|
}
|
|
124
158
|
if (response.status === 204 || response.headers.get("content-length") === "0") return null;
|
|
@@ -950,17 +984,11 @@ async function fetchAllDefinitions(client) {
|
|
|
950
984
|
}
|
|
951
985
|
return all;
|
|
952
986
|
}
|
|
953
|
-
async function selectDefinition(client, appName) {
|
|
987
|
+
async function selectDefinition(client, appName, rawDefinitionId) {
|
|
954
988
|
const definitions = await fetchAllDefinitions(client);
|
|
955
989
|
if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (!match) {
|
|
959
|
-
const availableNames = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
960
|
-
throw new Error(`No definition found matching "${appName}". Available: ${availableNames}`);
|
|
961
|
-
}
|
|
962
|
-
return match;
|
|
963
|
-
}
|
|
990
|
+
const resolved = resolveDefinition(definitions, appName, rawDefinitionId);
|
|
991
|
+
if (resolved) return resolved;
|
|
964
992
|
const { definitionId } = await prompts({
|
|
965
993
|
type: "select",
|
|
966
994
|
name: "definitionId",
|
|
@@ -1008,7 +1036,7 @@ async function writePortalFile(portalDir, relativePath, data) {
|
|
|
1008
1036
|
await mkdir(join(filePath, ".."), { recursive: true });
|
|
1009
1037
|
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1010
1038
|
}
|
|
1011
|
-
const pullCommand = new Command("pull").description("Pull a Fluid OS definition's resources to the local portal/ directory").option("--app <name>", "Definition name (skips interactive selector)").option("--force", "Overwrite local changes when switching definitions").action(async (options) => {
|
|
1039
|
+
const pullCommand = new Command("pull").description("Pull a Fluid OS definition's resources to the local portal/ directory").option("--app <name>", "Definition name (skips interactive selector)").option("--id <id>", "Definition id (avoids ambiguity when names repeat)").option("--force", "Overwrite local changes when switching definitions").action(async (options) => {
|
|
1012
1040
|
const cwd = process.cwd();
|
|
1013
1041
|
console.log();
|
|
1014
1042
|
console.log(chalk.blue.bold("Fluid Portal Pull"));
|
|
@@ -1028,7 +1056,7 @@ const pullCommand = new Command("pull").description("Pull a Fluid OS definition'
|
|
|
1028
1056
|
}
|
|
1029
1057
|
let definition;
|
|
1030
1058
|
try {
|
|
1031
|
-
definition = await selectDefinition(client, options.app);
|
|
1059
|
+
definition = await selectDefinition(client, options.app, options.id);
|
|
1032
1060
|
} catch (err) {
|
|
1033
1061
|
console.log();
|
|
1034
1062
|
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
@@ -1199,6 +1227,6 @@ const pullCommand = new Command("pull").description("Pull a Fluid OS definition'
|
|
|
1199
1227
|
console.log();
|
|
1200
1228
|
});
|
|
1201
1229
|
//#endregion
|
|
1202
|
-
export {
|
|
1230
|
+
export { fluid_os_v0_create_fluid_ostheme as A, fluid_os_v0_list_fluid_osversions as B, writeMappings as C, fluid_os_v0_create_fluid_osnavigation_item as D, fluid_os_v0_create_fluid_osnavigation as E, fluid_os_v0_delete_fluid_osprofile as F, fluid_os_v0_update_fluid_ostheme as G, fluid_os_v0_update_fluid_osnavigation_item as H, fluid_os_v0_delete_fluid_osscreen as I, fluid_os_v0_update_fluid_osversion as K, fluid_os_v0_delete_fluid_ostheme as L, fluid_os_v0_create_widget_package_version as M, fluid_os_v0_delete_fluid_osnavigation as N, fluid_os_v0_create_fluid_osprofile as O, fluid_os_v0_delete_fluid_osnavigation_item as P, fluid_os_v0_list_fluid_osdefinitions as R, updateMapping as S, fluid_os_v0_complete_widget_package_version_upload as T, fluid_os_v0_update_fluid_osprofile as U, fluid_os_v0_update_fluid_osnavigation as V, fluid_os_v0_update_fluid_osscreen as W, deriveSlug as _, buildThemeIdToSlugMap as a, resolveIdToSlug as b, transformNavigationItems as c, transformTheme as d, buildSnapshot as f, writeSnapshot as g, readSnapshot as h, buildNavigationIdToSlugMap as i, fluid_os_v0_create_fluid_osversion as j, fluid_os_v0_create_fluid_osscreen as k, transformProfile as l, diffAgainstSnapshot as m, pull_exports as n, deriveScreenSlug as o, computeFileHash as p, createFetchClient as q, buildIdToSlugMap as r, transformNavigation as s, pullCommand as t, transformScreen as u, readMappings as v, atomicWriteFile as w, resolveSlugToId as x, removeMapping as y, fluid_os_v0_list_fluid_osnavigation_items as z };
|
|
1203
1231
|
|
|
1204
|
-
//# sourceMappingURL=pull-
|
|
1232
|
+
//# sourceMappingURL=pull-C3yj9L07.mjs.map
|