@fluid-app/fluid-cli-portal 0.1.53 → 0.1.55
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/dist/{backend-dev-plugin-B9-S4Is-.mjs → backend-dev-plugin-CY3yxgJ1.mjs} +1 -1
- package/dist/{backend-dev-plugin-B9-S4Is-.mjs.map → backend-dev-plugin-CY3yxgJ1.mjs.map} +1 -1
- package/dist/index.d.mts +622 -421
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +719 -1815
- package/dist/index.mjs.map +1 -1
- package/dist/{portal-dev-plugin-ByYelgFg.mjs → portal-dev-plugin-CHlxpXc5.mjs} +1 -1
- package/dist/{portal-dev-plugin-ByYelgFg.mjs.map → portal-dev-plugin-CHlxpXc5.mjs.map} +1 -1
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs +144 -0
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs.map +1 -0
- package/dist/pull-CUR8tbW9.mjs +1633 -0
- package/dist/pull-CUR8tbW9.mjs.map +1 -0
- package/dist/sdk.gen-DuPhfdxN.mjs +469 -0
- package/dist/sdk.gen-DuPhfdxN.mjs.map +1 -0
- package/dist/src-CpH5SpDH.mjs +2157 -0
- package/dist/src-CpH5SpDH.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +4 -1
- package/dist/vite-plugin.d.mts.map +1 -1
- package/dist/vite-plugin.mjs +5 -3
- package/package.json +6 -4
- package/templates/base/AGENTS.md +4 -0
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +113 -1
- package/dist/pull-C3yj9L07.mjs +0 -1232
- package/dist/pull-C3yj9L07.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import {
|
|
1
|
+
import { A as STANDALONE_PORTAL_MARKER, C as writeMappings, D as toFluidOsDynamicObject, E as fluidOsApi, O as createFluidOsHeyApiClient, S as updateMapping, T as createFluidOsClient, _ as deriveSlug, a as buildThemeIdToSlugMap, b as resolveIdToSlug, c as transformNavigationItems, d as transformTheme, f as buildSnapshot, g as writeSnapshot, h as readSnapshot, i as buildNavigationIdToSlugMap, j as assertStandalonePortalRemoteActionAllowed, k as ApiError, l as transformProfile, m as diffAgainstSnapshot, o as deriveScreenSlug, p as computeFileHash, r as buildIdToSlugMap, s as transformNavigation, t as pullCommand, u as transformScreen, v as readMappings, w as atomicWriteFile, x as resolveSlugToId, y as removeMapping } from "./pull-CUR8tbW9.mjs";
|
|
2
|
+
import { l as fluidOsV0DefinitionGitCredentialCreate } from "./sdk.gen-DuPhfdxN.mjs";
|
|
3
|
+
import { a as resolvePublishVersion, i as readCompanyWidgetPackages, n as preflightCompanyPackage, o as loadAndValidateWidgetPackage, r as readCompanyWidgetPackage, s as publishWidgetPackage, t as formatPackageKeyMismatch } from "./src-CpH5SpDH.mjs";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
import ora from "ora";
|
|
6
7
|
import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { fileURLToPath
|
|
8
|
-
import { chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { chmodSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10
11
|
import os, { tmpdir } from "node:os";
|
|
11
12
|
import { failure, findProjectConfig, getActiveProfile, getAuthToken, listProfileNames, readConfig, success } from "@fluid-app/fluid-cli";
|
|
12
13
|
import prompts from "prompts";
|
|
13
14
|
import Handlebars from "handlebars";
|
|
14
15
|
import { execa } from "execa";
|
|
15
|
-
import {
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
16
17
|
import fs from "fs-extra";
|
|
17
18
|
//#region src/types.ts
|
|
18
19
|
/**
|
|
@@ -95,9 +96,9 @@ async function promptProjectConfig(projectName, options) {
|
|
|
95
96
|
//#endregion
|
|
96
97
|
//#region src/utils/file-system.ts
|
|
97
98
|
const _currentDir$1 = dirname(fileURLToPath(import.meta.url));
|
|
98
|
-
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.
|
|
99
|
+
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.464";
|
|
99
100
|
const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
|
|
100
|
-
const EMBEDDED_CLI_PORTAL_VERSION = "0.1.
|
|
101
|
+
const EMBEDDED_CLI_PORTAL_VERSION = "0.1.55";
|
|
101
102
|
/**
|
|
102
103
|
* Find the package root by walking up from the current directory to the nearest package.json.
|
|
103
104
|
* Works whether running from dist/ (bundled) or src/utils/ (tsx dev mode).
|
|
@@ -1598,6 +1599,15 @@ function portalDevServerOptions(port, exposeToNetwork, cwd = process.cwd()) {
|
|
|
1598
1599
|
watch: { ignored: createPortalWatchIgnored(cwd) }
|
|
1599
1600
|
};
|
|
1600
1601
|
}
|
|
1602
|
+
function canFallbackToBareVite(cwd) {
|
|
1603
|
+
try {
|
|
1604
|
+
lstatSync(join(cwd, STANDALONE_PORTAL_MARKER));
|
|
1605
|
+
return false;
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return true;
|
|
1608
|
+
throw error;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1601
1611
|
/**
|
|
1602
1612
|
* Check if the portal directory exists and has content files.
|
|
1603
1613
|
* Returns true if at minimum `portal/definition.json` exists.
|
|
@@ -1637,7 +1647,7 @@ async function autoPull(cwd) {
|
|
|
1637
1647
|
console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
|
|
1638
1648
|
console.log();
|
|
1639
1649
|
try {
|
|
1640
|
-
const { pullCommand } = await import("./pull-
|
|
1650
|
+
const { pullCommand } = await import("./pull-CUR8tbW9.mjs").then((n) => n.n);
|
|
1641
1651
|
await pullCommand.parseAsync([], { from: "user" });
|
|
1642
1652
|
return hasPortalContent(cwd);
|
|
1643
1653
|
} catch (err) {
|
|
@@ -1690,12 +1700,17 @@ const devCommand = new Command("dev").description("Start the development server
|
|
|
1690
1700
|
const port = Number.isFinite(parsedPort) ? parsedPort : 5173;
|
|
1691
1701
|
try {
|
|
1692
1702
|
const { createServer } = await import("vite");
|
|
1693
|
-
const { portalDevPlugin } = await import("./portal-dev-plugin-
|
|
1694
|
-
const { backendDevPlugin } = await import("./backend-dev-plugin-
|
|
1703
|
+
const { portalDevPlugin } = await import("./portal-dev-plugin-CHlxpXc5.mjs").then((n) => n.n);
|
|
1704
|
+
const { backendDevPlugin } = await import("./backend-dev-plugin-CY3yxgJ1.mjs").then((n) => n.n);
|
|
1705
|
+
const { portalWidgetDevPlugin } = await import("./portal-widget-dev-plugin-B53Lf9Ke.mjs").then((n) => n.n);
|
|
1695
1706
|
const server = await createServer({
|
|
1696
1707
|
configFile: viteConfigPath,
|
|
1697
1708
|
envPrefix: ["VITE_", "NEXT_PUBLIC_"],
|
|
1698
|
-
plugins: [
|
|
1709
|
+
plugins: [
|
|
1710
|
+
portalDevPlugin(),
|
|
1711
|
+
backendDevPlugin(),
|
|
1712
|
+
...portalWidgetDevPlugin()
|
|
1713
|
+
],
|
|
1699
1714
|
server: portalDevServerOptions(port, options.host === true, cwd)
|
|
1700
1715
|
});
|
|
1701
1716
|
await server.listen();
|
|
@@ -1707,6 +1722,12 @@ const devCommand = new Command("dev").description("Start the development server
|
|
|
1707
1722
|
process.on("SIGTERM", shutdown);
|
|
1708
1723
|
await new Promise(() => {});
|
|
1709
1724
|
} catch (error) {
|
|
1725
|
+
if (!canFallbackToBareVite(cwd)) {
|
|
1726
|
+
console.error(chalk.red("Could not start the enriched standalone portal dev server."));
|
|
1727
|
+
console.error(chalk.yellow("Standalone portals require the local manifest and widget plugins; bare Vite cannot serve this project correctly."));
|
|
1728
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1729
|
+
process.exit(1);
|
|
1730
|
+
}
|
|
1710
1731
|
console.warn(chalk.yellow("Could not start the enriched dev server; falling back to `vite`. Local portal content requires portalDevPlugin in vite.config for the fallback path."), error instanceof Error ? error.message : String(error));
|
|
1711
1732
|
try {
|
|
1712
1733
|
await execa("pnpm", viteArgs, {
|
|
@@ -1760,6 +1781,43 @@ const buildCommand = new Command("build").description("Build the application for
|
|
|
1760
1781
|
}
|
|
1761
1782
|
});
|
|
1762
1783
|
//#endregion
|
|
1784
|
+
//#region ../../portal/core/src/registries/container-registry.ts
|
|
1785
|
+
/**
|
|
1786
|
+
* Default container registry with LayoutWidget and ContainerWidget
|
|
1787
|
+
*/
|
|
1788
|
+
const DEFAULT_CONTAINER_REGISTRY = new Map([
|
|
1789
|
+
["LayoutWidget", {
|
|
1790
|
+
isContainer: true,
|
|
1791
|
+
getChildren: (props) => props.children || [],
|
|
1792
|
+
setChildren: (props, children) => ({
|
|
1793
|
+
...props,
|
|
1794
|
+
children
|
|
1795
|
+
})
|
|
1796
|
+
}],
|
|
1797
|
+
["CardWidget", {
|
|
1798
|
+
isContainer: true,
|
|
1799
|
+
getChildren: (props) => props.children || [],
|
|
1800
|
+
setChildren: (props, children) => ({
|
|
1801
|
+
...props,
|
|
1802
|
+
children
|
|
1803
|
+
})
|
|
1804
|
+
}],
|
|
1805
|
+
["ContainerWidget", {
|
|
1806
|
+
isContainer: true,
|
|
1807
|
+
getChildren: (props) => props.children || [],
|
|
1808
|
+
setChildren: (props, children) => ({
|
|
1809
|
+
...props,
|
|
1810
|
+
children
|
|
1811
|
+
})
|
|
1812
|
+
}]
|
|
1813
|
+
]);
|
|
1814
|
+
/**
|
|
1815
|
+
* Check if a widget type is a container
|
|
1816
|
+
*/
|
|
1817
|
+
function isContainerWidget(widgetType, containerRegistry) {
|
|
1818
|
+
return containerRegistry.has(widgetType);
|
|
1819
|
+
}
|
|
1820
|
+
//#endregion
|
|
1763
1821
|
//#region src/utils/push-validation.ts
|
|
1764
1822
|
/**
|
|
1765
1823
|
* Cross-reference validation and change categorization utilities for the push command.
|
|
@@ -1829,10 +1887,67 @@ function categorizeChanges(diff) {
|
|
|
1829
1887
|
return result;
|
|
1830
1888
|
}
|
|
1831
1889
|
/**
|
|
1890
|
+
* Faults in a screen's widget tree, as `component_tree[1].props.children[0]`
|
|
1891
|
+
* style paths.
|
|
1892
|
+
*
|
|
1893
|
+
* The screen check used to stop at "name is a string, component_tree is an
|
|
1894
|
+
* array" and never look inside. A node missing its `id` published happily —
|
|
1895
|
+
* `✔ Portal files valid`, live version activated — and then the admin App
|
|
1896
|
+
* Builder rendered the whole portal blank, in a different application, with
|
|
1897
|
+
* nothing pointing back at the screen. The builder backfills a missing id
|
|
1898
|
+
* (`widget-utils.ts`), but regenerates it per render while identity lookups
|
|
1899
|
+
* match on it, so such a node never reconciles.
|
|
1900
|
+
*
|
|
1901
|
+
* This matters because hand-written `component_tree` JSON is a first-class
|
|
1902
|
+
* path, not an edge case: it is how Mist's agent places widgets, and how
|
|
1903
|
+
* anything the builder cannot express gets authored.
|
|
1904
|
+
*
|
|
1905
|
+
* Deliberately conservative about what counts as a node. Only entries of
|
|
1906
|
+
* `component_tree` and of a node's `props.children` are checked — the two
|
|
1907
|
+
* places every container in `DEFAULT_CONTAINER_REGISTRY` keeps them. A blind
|
|
1908
|
+
* walk of every nested object would be wrong: screens are full of `type`-
|
|
1909
|
+
* bearing values that are NOT widgets (`background: { type: "solid" }`,
|
|
1910
|
+
* `dataSource: { type: "api" }`), and demanding an `id` of those would reject
|
|
1911
|
+
* screens that work today. A container using some other prop key is a missed
|
|
1912
|
+
* check, which is the safe direction for a gate that blocks publishing.
|
|
1913
|
+
*/
|
|
1914
|
+
function collectWidgetNodeErrors(nodes) {
|
|
1915
|
+
return walkWidgetNodes(nodes, "component_tree", false, /* @__PURE__ */ new Map());
|
|
1916
|
+
}
|
|
1917
|
+
function walkWidgetNodes(nodes, path, nullIsAnEmptySlot, seenIds) {
|
|
1918
|
+
const errors = [];
|
|
1919
|
+
nodes.forEach((node, index) => {
|
|
1920
|
+
const at = `${path}[${index}]`;
|
|
1921
|
+
if (node === null || node === void 0) {
|
|
1922
|
+
if (!nullIsAnEmptySlot) errors.push(`${at} must be a widget object.`);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (typeof node !== "object" || Array.isArray(node)) {
|
|
1926
|
+
errors.push(`${at} must be a widget object.`);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const record = node;
|
|
1930
|
+
if (typeof record.type !== "string" || record.type.trim().length === 0) errors.push(`${at} is missing a "type".`);
|
|
1931
|
+
else if (record.type !== record.type.trim()) errors.push(`${at} has a "type" with surrounding whitespace (${JSON.stringify(record.type)}). Widget types are matched exactly, so this resolves to no component and renders blank.`);
|
|
1932
|
+
if (typeof record.id !== "string" || record.id.trim().length === 0) errors.push(`${at} is missing an "id". Every widget needs a stable id — without one the screen publishes but renders blank in the app builder.`);
|
|
1933
|
+
else {
|
|
1934
|
+
const firstClaim = seenIds.get(record.id);
|
|
1935
|
+
if (firstClaim === void 0) seenIds.set(record.id, at);
|
|
1936
|
+
else errors.push(`${at} reuses the id ${JSON.stringify(record.id)}, already used by ${firstClaim}. Ids must be unique within a screen — the builder resolves a widget by id and takes the first match, so duplicates make select, delete and drag-drop act on the wrong widget.`);
|
|
1937
|
+
}
|
|
1938
|
+
const props = record.props;
|
|
1939
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) return;
|
|
1940
|
+
const children = props.children;
|
|
1941
|
+
if (Array.isArray(children)) errors.push(...walkWidgetNodes(children, `${at}.props.children`, true, seenIds));
|
|
1942
|
+
});
|
|
1943
|
+
return errors;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1832
1946
|
* Validate that all cross-references between local portal files are valid.
|
|
1833
1947
|
*
|
|
1834
1948
|
* Checks:
|
|
1835
1949
|
* - New and changed resources contain the required local-file fields
|
|
1950
|
+
* - Every widget node in a changed screen has a string "id" and "type"
|
|
1836
1951
|
* - New and changed themes include the required config object
|
|
1837
1952
|
* - Navigation items' "screen" slugs reference existing screen files or mappings
|
|
1838
1953
|
* - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
|
|
@@ -1853,6 +1968,16 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1853
1968
|
file,
|
|
1854
1969
|
message: "Screen requires a string \"name\" and array \"component_tree\"."
|
|
1855
1970
|
});
|
|
1971
|
+
else {
|
|
1972
|
+
if (screen.component_tree.length > 1) errors.push({
|
|
1973
|
+
file,
|
|
1974
|
+
message: `"component_tree" has ${screen.component_tree.length} top-level nodes, and pushing cannot preserve that. More than one root is serialized into an untyped wrapper the admin builder cannot render, so the screen would arrive blank and uneditable. Wrap them in a single ${CONTAINER_TYPE_LIST} node and move them into its "props.children".`
|
|
1975
|
+
});
|
|
1976
|
+
for (const error of collectWidgetNodeErrors(screen.component_tree)) errors.push({
|
|
1977
|
+
file,
|
|
1978
|
+
message: error
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1856
1981
|
} catch {
|
|
1857
1982
|
errors.push({
|
|
1858
1983
|
file,
|
|
@@ -1921,6 +2046,59 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1921
2046
|
return errors;
|
|
1922
2047
|
}
|
|
1923
2048
|
/**
|
|
2049
|
+
* Warns about screens the admin visual builder will not accept drops on.
|
|
2050
|
+
*
|
|
2051
|
+
* Only container widgets receive the builder's edit-mode child-management
|
|
2052
|
+
* callbacks (`onAddChild`, child select/delete/duplicate), so a screen whose
|
|
2053
|
+
* `component_tree` has no container at the top level renders perfectly — in
|
|
2054
|
+
* local preview and in the live portal — while offering no drop zones at all.
|
|
2055
|
+
* A human opening it in the builder cannot drag anything onto the page.
|
|
2056
|
+
*
|
|
2057
|
+
* That combination (correct everywhere you can look, broken in the one place
|
|
2058
|
+
* you cannot) is why this is worth reporting at push time. It is the most
|
|
2059
|
+
* likely defect in a hand- or agent-authored screen.
|
|
2060
|
+
*
|
|
2061
|
+
* Deliberately a WARNING, never an error. Screens without a container root are
|
|
2062
|
+
* valid definitions that render correctly, and a repository may hold ones that
|
|
2063
|
+
* predate this check; failing their push would break working portals to enforce
|
|
2064
|
+
* an authoring convention.
|
|
2065
|
+
*
|
|
2066
|
+
* Silent on anything it cannot parse. Malformed and unreadable screens are
|
|
2067
|
+
* already reported as hard errors by {@link validateCrossReferences}, and a
|
|
2068
|
+
* second complaint about a file that never parsed would be noise.
|
|
2069
|
+
*/
|
|
2070
|
+
async function validateScreenStructure(portalDir, changes) {
|
|
2071
|
+
const warnings = [];
|
|
2072
|
+
const files = [...changes.screens.new, ...changes.screens.changed];
|
|
2073
|
+
for (const file of files) {
|
|
2074
|
+
let screen;
|
|
2075
|
+
try {
|
|
2076
|
+
screen = await readPortalFile$1(portalDir, file);
|
|
2077
|
+
} catch {
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
const tree = screen.component_tree;
|
|
2081
|
+
if (!Array.isArray(tree) || tree.length === 0) continue;
|
|
2082
|
+
if (tree.length !== 1) continue;
|
|
2083
|
+
const root = tree[0];
|
|
2084
|
+
const rootType = root && typeof root === "object" ? root.type : void 0;
|
|
2085
|
+
if (typeof rootType === "string" && isContainerWidget(rootType, CONTAINER_REGISTRY)) continue;
|
|
2086
|
+
warnings.push({
|
|
2087
|
+
file,
|
|
2088
|
+
message: `No container at the top of "component_tree", so the admin builder will show no drop zones on this screen. Wrap the widgets in a ${CONTAINER_TYPE_LIST} node and move them into its "props.children".`
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
return warnings;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Shared with the portal runtime so this check cannot drift from the behaviour
|
|
2095
|
+
* it predicts. `DEFAULT_CONTAINER_REGISTRY` is the same map the builder consults
|
|
2096
|
+
* via `isContainerWidget`.
|
|
2097
|
+
*/
|
|
2098
|
+
const CONTAINER_REGISTRY = DEFAULT_CONTAINER_REGISTRY;
|
|
2099
|
+
/** Rendered into the warning so it names the real options, not a stale list. */
|
|
2100
|
+
const CONTAINER_TYPE_LIST = [...DEFAULT_CONTAINER_REGISTRY.keys()].map((type) => `"${type}"`).join(" / ");
|
|
2101
|
+
/**
|
|
1924
2102
|
* Build a set of valid slugs for a resource type by combining
|
|
1925
2103
|
* existing mapping slugs with local file slugs on disk.
|
|
1926
2104
|
*/
|
|
@@ -1949,6 +2127,26 @@ function validateNavigationItems(items, file, validScreenSlugs, errors) {
|
|
|
1949
2127
|
//#endregion
|
|
1950
2128
|
//#region src/api/git-credential.ts
|
|
1951
2129
|
/**
|
|
2130
|
+
* Portal git-credential adapter (the "invisible git" seam).
|
|
2131
|
+
*
|
|
2132
|
+
* Mints a short-lived (~1h) remote URL with an embedded installation
|
|
2133
|
+
* token for the portal's provisioned GitHub repo. The URL is used
|
|
2134
|
+
* immediately for one git op and discarded — NEVER persisted or logged.
|
|
2135
|
+
*
|
|
2136
|
+
* The backend endpoint (GitSync phase 006, portal adapter) mirrors the
|
|
2137
|
+
* widget contract exactly:
|
|
2138
|
+
*
|
|
2139
|
+
* POST <endpoint> -> 201 { git_credential: { remote_url, expires_at }, status, meta }
|
|
2140
|
+
* errors: 409 not_provisioned/not_connected, 502 vendor_error, 422 else
|
|
2141
|
+
* { error_message, errors: { type: [type] }, meta }
|
|
2142
|
+
*
|
|
2143
|
+
* The exact path is still being finalized as 006 lands, so it lives in
|
|
2144
|
+
* one constant here. Until the endpoint ships to prod a request 404s —
|
|
2145
|
+
* which we surface as `unavailable` so callers degrade gracefully
|
|
2146
|
+
* (warn + skip the git sync, proceed with the normal content push).
|
|
2147
|
+
*/
|
|
2148
|
+
/** Minted credential — token embedded in `remote_url`. Treat as secret. */
|
|
2149
|
+
/**
|
|
1952
2150
|
* Build the portal git-credential path for a definition id.
|
|
1953
2151
|
*
|
|
1954
2152
|
* Matches GitSync Phase 006 (fluid-commerce/fluid #19596): a member
|
|
@@ -1969,7 +2167,8 @@ const DEFAULT_RETRY_DELAYS_MS = [
|
|
|
1969
2167
|
15e3
|
|
1970
2168
|
];
|
|
1971
2169
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
1972
|
-
const RETRYABLE_CONFLICT_TYPES = new Set(["not_provisioned"]);
|
|
2170
|
+
const RETRYABLE_CONFLICT_TYPES = new Set(["not_provisioned", "not_connected"]);
|
|
2171
|
+
const DURABLE_REPO_STATUSES = new Set(["disconnected"]);
|
|
1973
2172
|
function nonEmptyString(value) {
|
|
1974
2173
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1975
2174
|
}
|
|
@@ -1977,6 +2176,13 @@ function firstString(value) {
|
|
|
1977
2176
|
if (Array.isArray(value)) return nonEmptyString(value[0]);
|
|
1978
2177
|
return nonEmptyString(value);
|
|
1979
2178
|
}
|
|
2179
|
+
function apiErrorBody(error) {
|
|
2180
|
+
if (error.data !== null && typeof error.data === "object" && ("error" in error.data || "error_message" in error.data || "errors" in error.data)) return error.data;
|
|
2181
|
+
return {
|
|
2182
|
+
error_message: error.message,
|
|
2183
|
+
errors: error.data
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
1980
2186
|
function extractErrorDetails(body, fallback) {
|
|
1981
2187
|
if (!body || typeof body !== "object") return { reason: fallback };
|
|
1982
2188
|
const envelope = body;
|
|
@@ -1985,9 +2191,11 @@ function extractErrorDetails(body, fallback) {
|
|
|
1985
2191
|
const errors = envelope.errors && typeof envelope.errors === "object" ? envelope.errors : void 0;
|
|
1986
2192
|
const reason = nonEmptyString(envelope.error_message) ?? nonEmptyString(error?.message) ?? nonEmptyString(envelope.error) ?? fallback;
|
|
1987
2193
|
const failureType = firstString(errors?.type) ?? nonEmptyString(nestedDetails?.type);
|
|
2194
|
+
const repoStatus = firstString(errors?.repo_status) ?? nonEmptyString(nestedDetails?.repo_status);
|
|
1988
2195
|
return {
|
|
1989
2196
|
reason,
|
|
1990
|
-
...failureType ? { failureType } : {}
|
|
2197
|
+
...failureType ? { failureType } : {},
|
|
2198
|
+
...repoStatus ? { repoStatus } : {}
|
|
1991
2199
|
};
|
|
1992
2200
|
}
|
|
1993
2201
|
function sleep(delayMs) {
|
|
@@ -2034,64 +2242,64 @@ async function createPortalGitCredential(options) {
|
|
|
2034
2242
|
const { baseUrl, token, definitionId } = options;
|
|
2035
2243
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2036
2244
|
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2037
|
-
const url = `${baseUrl.replace(/\/$/, "")}${portalGitCredentialPath(definitionId)}`;
|
|
2038
2245
|
const abortController = new AbortController();
|
|
2039
2246
|
let requestTimedOut = false;
|
|
2040
2247
|
const timeout = setTimeout(() => {
|
|
2041
2248
|
requestTimedOut = true;
|
|
2042
2249
|
abortController.abort();
|
|
2043
2250
|
}, requestTimeoutMs);
|
|
2044
|
-
let resp;
|
|
2045
|
-
let body = null;
|
|
2046
2251
|
try {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
},
|
|
2252
|
+
const credential = (await fluidOsV0DefinitionGitCredentialCreate({
|
|
2253
|
+
client: createFluidOsHeyApiClient({
|
|
2254
|
+
baseUrl,
|
|
2255
|
+
fetch: fetchImpl,
|
|
2256
|
+
getAuthToken: () => token
|
|
2257
|
+
}),
|
|
2258
|
+
path: { id: definitionId },
|
|
2053
2259
|
signal: abortController.signal
|
|
2054
|
-
});
|
|
2055
|
-
if (
|
|
2056
|
-
body = await resp.json();
|
|
2057
|
-
} catch (err) {
|
|
2058
|
-
if (requestTimedOut) throw err;
|
|
2059
|
-
}
|
|
2060
|
-
} catch (err) {
|
|
2061
|
-
return {
|
|
2260
|
+
})).git_credential;
|
|
2261
|
+
if (!credential?.remote_url) return {
|
|
2062
2262
|
status: "error",
|
|
2063
|
-
reason:
|
|
2263
|
+
reason: "git credential response missing remote_url"
|
|
2064
2264
|
};
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
if (resp.status === 404) return {
|
|
2069
|
-
status: "unavailable",
|
|
2070
|
-
reason: "git sync isn't available for portals on this Fluid API yet",
|
|
2071
|
-
retryable: false
|
|
2072
|
-
};
|
|
2073
|
-
if (!resp.ok) {
|
|
2074
|
-
const details = extractErrorDetails(body, `git credential request failed (HTTP ${resp.status})`);
|
|
2075
|
-
if (resp.status === 409) return {
|
|
2076
|
-
status: "unavailable",
|
|
2077
|
-
reason: details.reason,
|
|
2078
|
-
retryable: details.failureType !== void 0 && RETRYABLE_CONFLICT_TYPES.has(details.failureType),
|
|
2079
|
-
...details.failureType !== void 0 ? { conflictType: details.failureType } : {}
|
|
2265
|
+
return {
|
|
2266
|
+
status: "ok",
|
|
2267
|
+
credential
|
|
2080
2268
|
};
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
if (requestTimedOut) return {
|
|
2271
|
+
status: "error",
|
|
2272
|
+
reason: `git credential request timed out after ${requestTimeoutMs}ms`
|
|
2273
|
+
};
|
|
2274
|
+
if (err instanceof ApiError) {
|
|
2275
|
+
if (err.status === 404) return {
|
|
2276
|
+
status: "unavailable",
|
|
2277
|
+
reason: "git sync isn't available for portals on this Fluid API yet",
|
|
2278
|
+
retryable: false
|
|
2279
|
+
};
|
|
2280
|
+
const details = extractErrorDetails(apiErrorBody(err), `git credential request failed (HTTP ${err.status})`);
|
|
2281
|
+
if (err.status === 409) {
|
|
2282
|
+
const durable = details.repoStatus !== void 0 && DURABLE_REPO_STATUSES.has(details.repoStatus);
|
|
2283
|
+
return {
|
|
2284
|
+
status: "unavailable",
|
|
2285
|
+
reason: details.reason,
|
|
2286
|
+
retryable: !durable && details.failureType !== void 0 && RETRYABLE_CONFLICT_TYPES.has(details.failureType),
|
|
2287
|
+
...details.failureType !== void 0 ? { conflictType: details.failureType } : {},
|
|
2288
|
+
...details.repoStatus !== void 0 ? { repoStatus: details.repoStatus } : {}
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
status: "error",
|
|
2293
|
+
reason: details.reason
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2081
2296
|
return {
|
|
2082
2297
|
status: "error",
|
|
2083
|
-
reason:
|
|
2298
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2084
2299
|
};
|
|
2300
|
+
} finally {
|
|
2301
|
+
clearTimeout(timeout);
|
|
2085
2302
|
}
|
|
2086
|
-
const credential = body?.git_credential;
|
|
2087
|
-
if (!credential?.remote_url) return {
|
|
2088
|
-
status: "error",
|
|
2089
|
-
reason: "git credential response missing remote_url"
|
|
2090
|
-
};
|
|
2091
|
-
return {
|
|
2092
|
-
status: "ok",
|
|
2093
|
-
credential
|
|
2094
|
-
};
|
|
2095
2303
|
}
|
|
2096
2304
|
//#endregion
|
|
2097
2305
|
//#region src/lib/git-sync.ts
|
|
@@ -2226,7 +2434,7 @@ function toApiComponentTree(tree) {
|
|
|
2226
2434
|
return { children: tree };
|
|
2227
2435
|
}
|
|
2228
2436
|
/**
|
|
2229
|
-
* Create an authenticated
|
|
2437
|
+
* Create an authenticated FluidOsClient using the stored CLI profile.
|
|
2230
2438
|
*/
|
|
2231
2439
|
function createClient$2() {
|
|
2232
2440
|
const token = getAuthToken();
|
|
@@ -2235,15 +2443,15 @@ function createClient$2() {
|
|
|
2235
2443
|
if (!profile) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
2236
2444
|
throw new Error("No auth token found for profile " + chalk.cyan(profile.name) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
2237
2445
|
}
|
|
2238
|
-
return
|
|
2446
|
+
return createFluidOsClient({
|
|
2239
2447
|
baseUrl: process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
|
|
2240
|
-
|
|
2448
|
+
token
|
|
2241
2449
|
});
|
|
2242
2450
|
}
|
|
2243
2451
|
async function createAndActivateVersion(client, definitionId) {
|
|
2244
|
-
const versionId = (await
|
|
2452
|
+
const versionId = (await fluidOsApi.createVersion(client, definitionId)).version?.id;
|
|
2245
2453
|
if (!versionId) throw new Error("the version API returned no version ID");
|
|
2246
|
-
await
|
|
2454
|
+
await fluidOsApi.updateVersion(client, definitionId, versionId, { version: { active: true } });
|
|
2247
2455
|
return String(versionId);
|
|
2248
2456
|
}
|
|
2249
2457
|
async function activateDraft(definitionId, existingClient) {
|
|
@@ -2331,10 +2539,10 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
|
|
|
2331
2539
|
const slug = slugFromPath(file);
|
|
2332
2540
|
try {
|
|
2333
2541
|
const local = await readPortalFile(portalDir, file);
|
|
2334
|
-
const newId = (await
|
|
2542
|
+
const newId = (await fluidOsApi.createScreen(client, defId, { screen: {
|
|
2335
2543
|
name: local.name,
|
|
2336
2544
|
slug,
|
|
2337
|
-
component_tree: toApiComponentTree(local.component_tree)
|
|
2545
|
+
component_tree: toFluidOsDynamicObject(toApiComponentTree(local.component_tree))
|
|
2338
2546
|
} })).screen?.id;
|
|
2339
2547
|
if (newId != null) currentMappings = updateMapping(currentMappings, "screens", slug, newId);
|
|
2340
2548
|
results.push({
|
|
@@ -2372,10 +2580,10 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
|
|
|
2372
2580
|
}
|
|
2373
2581
|
try {
|
|
2374
2582
|
const local = await readPortalFile(portalDir, file);
|
|
2375
|
-
await
|
|
2583
|
+
await fluidOsApi.updateScreen(client, defId, screenId, { screen: {
|
|
2376
2584
|
name: local.name,
|
|
2377
2585
|
slug,
|
|
2378
|
-
component_tree: toApiComponentTree(local.component_tree)
|
|
2586
|
+
component_tree: toFluidOsDynamicObject(toApiComponentTree(local.component_tree))
|
|
2379
2587
|
} });
|
|
2380
2588
|
results.push({
|
|
2381
2589
|
file,
|
|
@@ -2411,7 +2619,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
|
|
|
2411
2619
|
};
|
|
2412
2620
|
}
|
|
2413
2621
|
try {
|
|
2414
|
-
await
|
|
2622
|
+
await fluidOsApi.deleteScreen(client, defId, screenId);
|
|
2415
2623
|
currentMappings = removeMapping(currentMappings, "screens", slug);
|
|
2416
2624
|
results.push({
|
|
2417
2625
|
file,
|
|
@@ -2446,10 +2654,10 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
|
|
|
2446
2654
|
const slug = slugFromPath(file);
|
|
2447
2655
|
try {
|
|
2448
2656
|
const local = await readPortalFile(portalDir, file);
|
|
2449
|
-
const newId = (await
|
|
2657
|
+
const newId = (await fluidOsApi.createTheme(client, defId, { theme: {
|
|
2450
2658
|
name: local.name,
|
|
2451
2659
|
active: local.active,
|
|
2452
|
-
config: local.config
|
|
2660
|
+
config: toFluidOsDynamicObject(local.config)
|
|
2453
2661
|
} })).theme?.id;
|
|
2454
2662
|
if (newId != null) currentMappings = updateMapping(currentMappings, "themes", slug, newId);
|
|
2455
2663
|
results.push({
|
|
@@ -2487,10 +2695,10 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
|
|
|
2487
2695
|
}
|
|
2488
2696
|
try {
|
|
2489
2697
|
const local = await readPortalFile(portalDir, file);
|
|
2490
|
-
await
|
|
2698
|
+
await fluidOsApi.updateTheme(client, defId, themeId, { theme: {
|
|
2491
2699
|
name: local.name,
|
|
2492
2700
|
active: local.active,
|
|
2493
|
-
config: local.config
|
|
2701
|
+
config: toFluidOsDynamicObject(local.config)
|
|
2494
2702
|
} });
|
|
2495
2703
|
results.push({
|
|
2496
2704
|
file,
|
|
@@ -2526,7 +2734,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
|
|
|
2526
2734
|
};
|
|
2527
2735
|
}
|
|
2528
2736
|
try {
|
|
2529
|
-
await
|
|
2737
|
+
await fluidOsApi.deleteTheme(client, defId, themeId);
|
|
2530
2738
|
currentMappings = removeMapping(currentMappings, "themes", slug);
|
|
2531
2739
|
results.push({
|
|
2532
2740
|
file,
|
|
@@ -2616,7 +2824,7 @@ async function resolveCreatedNavigationItemId(client, defId, navigationId, respo
|
|
|
2616
2824
|
}
|
|
2617
2825
|
let response;
|
|
2618
2826
|
try {
|
|
2619
|
-
response = await
|
|
2827
|
+
response = await fluidOsApi.listNavigationItems(client, defId, navigationId);
|
|
2620
2828
|
} catch (recoveryError) {
|
|
2621
2829
|
throw new NavigationRemoteStateUnknownError(`Remote navigation state is unknown because item creation succeeded without returning its required ID, and the recovery read failed: ${enrichedErrorMessage(recoveryError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2622
2830
|
}
|
|
@@ -2666,7 +2874,7 @@ async function persistCreatedNavigationItemId(client, defId, navigationId, porta
|
|
|
2666
2874
|
await atomicWriteFile(join(portalDir, file), JSON.stringify(updated, null, 2) + "\n");
|
|
2667
2875
|
} catch (persistError) {
|
|
2668
2876
|
try {
|
|
2669
|
-
await
|
|
2877
|
+
await fluidOsApi.deleteNavigationItem(client, defId, navigationId, serverId);
|
|
2670
2878
|
} catch (rollbackError) {
|
|
2671
2879
|
throw new NavigationRemoteStateUnknownError(`Failed to persist navigation item ${serverId} locally: ${enrichedErrorMessage(persistError)}. Automatic rollback also failed: ${enrichedErrorMessage(rollbackError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2672
2880
|
}
|
|
@@ -2680,7 +2888,7 @@ async function persistCreatedNavigationItemId(client, defId, navigationId, porta
|
|
|
2680
2888
|
* must be reconciled before the push snapshot hashes it.
|
|
2681
2889
|
*/
|
|
2682
2890
|
async function reconcileNavigationFile(client, defId, navId, portalDir, file, local, mappings) {
|
|
2683
|
-
const response = await
|
|
2891
|
+
const response = await fluidOsApi.listNavigationItems(client, defId, navId);
|
|
2684
2892
|
const reconciled = {
|
|
2685
2893
|
...local,
|
|
2686
2894
|
navigation_items: transformNavigationItems(response.navigation_items ?? [], buildIdToSlugMap(mappings.screens))
|
|
@@ -2700,7 +2908,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2700
2908
|
let remoteChangesComplete = false;
|
|
2701
2909
|
try {
|
|
2702
2910
|
let local = await readPortalFile(portalDir, file);
|
|
2703
|
-
const newId = (await
|
|
2911
|
+
const newId = (await fluidOsApi.createNavigation(client, defId, { navigation: {
|
|
2704
2912
|
name: local.name,
|
|
2705
2913
|
platform: local.platform
|
|
2706
2914
|
} })).navigation?.id;
|
|
@@ -2720,7 +2928,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2720
2928
|
source: item.source ?? void 0,
|
|
2721
2929
|
parent_id: resolvedParentId
|
|
2722
2930
|
};
|
|
2723
|
-
const createdId = await resolveCreatedNavigationItemId(client, defId, newId, (await
|
|
2931
|
+
const createdId = await resolveCreatedNavigationItemId(client, defId, newId, (await fluidOsApi.createNavigationItem(client, defId, newId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
|
|
2724
2932
|
local = await persistCreatedNavigationItemId(client, defId, newId, portalDir, file, local, itemIndex, createdId);
|
|
2725
2933
|
serverIdByItemIndex.set(itemIndex, createdId);
|
|
2726
2934
|
}
|
|
@@ -2763,12 +2971,12 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2763
2971
|
let remoteChangesComplete = false;
|
|
2764
2972
|
try {
|
|
2765
2973
|
let local = await readPortalFile(portalDir, file);
|
|
2766
|
-
await
|
|
2974
|
+
await fluidOsApi.updateNavigation(client, defId, navId, { navigation: {
|
|
2767
2975
|
name: local.name,
|
|
2768
2976
|
platform: local.platform
|
|
2769
2977
|
} });
|
|
2770
2978
|
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2771
|
-
const serverItems = flattenServerNavigationItems((await
|
|
2979
|
+
const serverItems = flattenServerNavigationItems((await fluidOsApi.listNavigationItems(client, defId, navId)).navigation_items ?? []);
|
|
2772
2980
|
const serverById = new Map(serverItems.map((s) => [s.id, s]));
|
|
2773
2981
|
const knownServerIds = new Set(serverById.keys());
|
|
2774
2982
|
const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
|
|
@@ -2785,7 +2993,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2785
2993
|
parent_id: resolvedParentId
|
|
2786
2994
|
};
|
|
2787
2995
|
if (item.id && serverById.has(item.id)) {
|
|
2788
|
-
await
|
|
2996
|
+
await fluidOsApi.updateNavigationItem(client, defId, navId, item.id, { navigation_item: body });
|
|
2789
2997
|
serverIdByItemIndex.set(itemIndex, item.id);
|
|
2790
2998
|
} else {
|
|
2791
2999
|
const createBody = {
|
|
@@ -2793,12 +3001,12 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2793
3001
|
label: body.label ?? "",
|
|
2794
3002
|
position: body.position ?? 0
|
|
2795
3003
|
};
|
|
2796
|
-
const createdId = await resolveCreatedNavigationItemId(client, defId, navId, (await
|
|
3004
|
+
const createdId = await resolveCreatedNavigationItemId(client, defId, navId, (await fluidOsApi.createNavigationItem(client, defId, navId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
|
|
2797
3005
|
local = await persistCreatedNavigationItemId(client, defId, navId, portalDir, file, local, itemIndex, createdId);
|
|
2798
3006
|
serverIdByItemIndex.set(itemIndex, createdId);
|
|
2799
3007
|
}
|
|
2800
3008
|
}
|
|
2801
|
-
for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await
|
|
3009
|
+
for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluidOsApi.deleteNavigationItem(client, defId, navId, serverItem.id);
|
|
2802
3010
|
remoteChangesComplete = true;
|
|
2803
3011
|
await reconcileNavigationFile(client, defId, navId, portalDir, file, local, currentMappings);
|
|
2804
3012
|
results.push({
|
|
@@ -2835,7 +3043,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2835
3043
|
};
|
|
2836
3044
|
}
|
|
2837
3045
|
try {
|
|
2838
|
-
await
|
|
3046
|
+
await fluidOsApi.deleteNavigation(client, defId, navId);
|
|
2839
3047
|
currentMappings = removeMapping(currentMappings, "navigations", slug);
|
|
2840
3048
|
results.push({
|
|
2841
3049
|
file,
|
|
@@ -2869,7 +3077,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
|
|
|
2869
3077
|
for (const file of changes.new) {
|
|
2870
3078
|
const slug = slugFromPath(file);
|
|
2871
3079
|
try {
|
|
2872
|
-
const
|
|
3080
|
+
const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
|
|
3081
|
+
const newId = (await fluidOsApi.createProfile(client, defId, { profile: body })).profile?.id;
|
|
2873
3082
|
if (newId != null) currentMappings = updateMapping(currentMappings, "profiles", slug, newId);
|
|
2874
3083
|
results.push({
|
|
2875
3084
|
file,
|
|
@@ -2905,7 +3114,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
|
|
|
2905
3114
|
};
|
|
2906
3115
|
}
|
|
2907
3116
|
try {
|
|
2908
|
-
|
|
3117
|
+
const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
|
|
3118
|
+
await fluidOsApi.updateProfile(client, defId, profileId, { profile: body });
|
|
2909
3119
|
results.push({
|
|
2910
3120
|
file,
|
|
2911
3121
|
action: "updated",
|
|
@@ -2940,7 +3150,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
|
|
|
2940
3150
|
};
|
|
2941
3151
|
}
|
|
2942
3152
|
try {
|
|
2943
|
-
await
|
|
3153
|
+
await fluidOsApi.deleteProfile(client, defId, profileId);
|
|
2944
3154
|
currentMappings = removeMapping(currentMappings, "profiles", slug);
|
|
2945
3155
|
results.push({
|
|
2946
3156
|
file,
|
|
@@ -3013,6 +3223,7 @@ function printPushReport(results, skippedPhases) {
|
|
|
3013
3223
|
}
|
|
3014
3224
|
const pushCommand = new Command("push").description("Push local portal content changes to the Fluid OS API").option("--yes", "Skip confirmation prompt").option("--activate", "Create and activate a live version after a successful push").action(async (options) => {
|
|
3015
3225
|
const cwd = process.cwd();
|
|
3226
|
+
await assertStandalonePortalRemoteActionAllowed(cwd, "portal push");
|
|
3016
3227
|
const portalDir = join(cwd, PORTAL_DIR);
|
|
3017
3228
|
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR$1);
|
|
3018
3229
|
console.log();
|
|
@@ -3085,6 +3296,12 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
3085
3296
|
process.exit(1);
|
|
3086
3297
|
}
|
|
3087
3298
|
spinner.succeed("Portal files valid");
|
|
3299
|
+
const structureWarnings = await validateScreenStructure(portalDir, changes);
|
|
3300
|
+
if (structureWarnings.length > 0) {
|
|
3301
|
+
console.log();
|
|
3302
|
+
for (const warning of structureWarnings) console.log(chalk.yellow(" warning ") + chalk.white(warning.file + ": ") + warning.message);
|
|
3303
|
+
console.log();
|
|
3304
|
+
}
|
|
3088
3305
|
spinner.start("Authenticating...");
|
|
3089
3306
|
let client;
|
|
3090
3307
|
try {
|
|
@@ -3193,13 +3410,14 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
3193
3410
|
* against the portal git-credential endpoint.
|
|
3194
3411
|
*
|
|
3195
3412
|
* When the credential endpoint reports that no repository is available
|
|
3196
|
-
* (the
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3199
|
-
*
|
|
3413
|
+
* (the repo was torn down, provisioning failed, it is still not ready
|
|
3414
|
+
* after the retry budget, or the endpoint isn't deployed), clone falls
|
|
3415
|
+
* back to scaffolding the portal locally from the bundled starter
|
|
3416
|
+
* template instead of failing. Local files plus API sync
|
|
3200
3417
|
* (`fluid portal push`/`pull`) are a fully functional portal — Git is
|
|
3201
3418
|
* an optional sync channel, not a prerequisite — so the user still
|
|
3202
|
-
* lands in a working project and Git history can attach later
|
|
3419
|
+
* lands in a working project and Git history can attach later via
|
|
3420
|
+
* `fluid portal push`, which adopts the remote's history.
|
|
3203
3421
|
*/
|
|
3204
3422
|
function getBaseUrl() {
|
|
3205
3423
|
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
@@ -3216,9 +3434,9 @@ async function removeIncompleteCheckout(targetDir) {
|
|
|
3216
3434
|
}
|
|
3217
3435
|
}
|
|
3218
3436
|
function createClient$1(token) {
|
|
3219
|
-
return
|
|
3437
|
+
return createFluidOsClient({
|
|
3220
3438
|
baseUrl: getBaseUrl(),
|
|
3221
|
-
|
|
3439
|
+
token
|
|
3222
3440
|
});
|
|
3223
3441
|
}
|
|
3224
3442
|
/**
|
|
@@ -3228,9 +3446,12 @@ function createClient$1(token) {
|
|
|
3228
3446
|
* `not_provisioned` genuinely may resolve later, so it says so.
|
|
3229
3447
|
*/
|
|
3230
3448
|
function gitSyncUnavailableNotice(outcome) {
|
|
3231
|
-
|
|
3232
|
-
if (outcome.
|
|
3233
|
-
|
|
3449
|
+
const attachHint = `Creating the portal locally from the starter template so you can start now — ${chalk.cyan("fluid portal push")}/${chalk.cyan("pull")} sync content without Git, and Git history can attach later with ${chalk.cyan("fluid portal push")}.`;
|
|
3450
|
+
if (outcome.repoStatus === "disconnected") return `The portal's Git repository was disconnected, so there is nothing to clone. ${attachHint}`;
|
|
3451
|
+
if (outcome.repoStatus === "error") return `The portal's Git repository failed to provision. ${attachHint}`;
|
|
3452
|
+
if (outcome.conflictType === "not_connected") return "The portal's Git repository is still being provisioned — it didn't come online in time. " + attachHint;
|
|
3453
|
+
if (outcome.conflictType === "not_provisioned") return `The portal's Git repository isn't provisioned yet. ${attachHint}`;
|
|
3454
|
+
return `Git sync isn't available (${outcome.reason}). ${attachHint}`;
|
|
3234
3455
|
}
|
|
3235
3456
|
/**
|
|
3236
3457
|
* A git-less fallback scaffold that failed or was interrupted leaves the
|
|
@@ -3274,7 +3495,7 @@ async function selectDefinition(client, appName, rawDefinitionId) {
|
|
|
3274
3495
|
const definitions = [];
|
|
3275
3496
|
let page = 1;
|
|
3276
3497
|
while (true) {
|
|
3277
|
-
const response = await
|
|
3498
|
+
const response = await fluidOsApi.listDefinitions(client, {
|
|
3278
3499
|
page,
|
|
3279
3500
|
per_page: 500
|
|
3280
3501
|
});
|
|
@@ -3414,6 +3635,115 @@ const cloneCommand = new Command("clone").description("Clone a portal definition
|
|
|
3414
3635
|
}
|
|
3415
3636
|
});
|
|
3416
3637
|
//#endregion
|
|
3638
|
+
//#region src/commands/widget-package-publish.ts
|
|
3639
|
+
async function publishWidgetRuntimeArtifacts(options) {
|
|
3640
|
+
await assertStandalonePortalRemoteActionAllowed(options.projectDir, "portal widget package publish");
|
|
3641
|
+
const isDryRun = options.dryRun === true;
|
|
3642
|
+
const client = options.client;
|
|
3643
|
+
if (!isDryRun && !client) throw new Error("A FluidOS API client is required when dryRun is false.");
|
|
3644
|
+
const cliVersion = await getPortalCliVersion();
|
|
3645
|
+
if (isDryRun) return await publishWidgetPackage({
|
|
3646
|
+
projectDir: options.projectDir,
|
|
3647
|
+
outDir: options.outDir,
|
|
3648
|
+
owner: options.buildOwner,
|
|
3649
|
+
uploadOwner: options.uploadOwner,
|
|
3650
|
+
cliVersion,
|
|
3651
|
+
dryRun: true
|
|
3652
|
+
});
|
|
3653
|
+
if (!client) throw new Error("A FluidOS API client is required when dryRun is false.");
|
|
3654
|
+
return await publishWidgetPackage({
|
|
3655
|
+
projectDir: options.projectDir,
|
|
3656
|
+
outDir: options.outDir,
|
|
3657
|
+
owner: options.buildOwner,
|
|
3658
|
+
uploadOwner: options.uploadOwner,
|
|
3659
|
+
cliVersion,
|
|
3660
|
+
...options.version ? { version: options.version } : {},
|
|
3661
|
+
client
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* Prints a publish failure with its `details` block, which is where the
|
|
3666
|
+
* validation tooling names the offending widget and field.
|
|
3667
|
+
*/
|
|
3668
|
+
function printWidgetPublishError(error) {
|
|
3669
|
+
console.error(chalk.red("Error:") + " " + (error instanceof Error ? error.message : String(error)));
|
|
3670
|
+
if (typeof error === "object" && error !== null && "details" in error && typeof error.details === "string" && error.details.length > 0) console.error(error.details);
|
|
3671
|
+
}
|
|
3672
|
+
const DEFAULT_API_BASE = "https://api.fluid.app";
|
|
3673
|
+
/**
|
|
3674
|
+
* Builds the client used to publish widget artifacts.
|
|
3675
|
+
*
|
|
3676
|
+
* `FLUID_TOKEN` / `FLUID_API_TOKEN` win over any stored profile, matching the
|
|
3677
|
+
* resolution order the rest of the CLI documents and implements (see
|
|
3678
|
+
* `getAuthToken` in `@fluid-app/fluid-cli`). This path used to read only
|
|
3679
|
+
* `~/.fluid/config.json`, which made it the one command a caller could not
|
|
3680
|
+
* point at a chosen company: a host that spawns the CLI would publish to
|
|
3681
|
+
* whichever profile the terminal happened to have active, and shipping a
|
|
3682
|
+
* company's widgets to the wrong tenant is not a failure they could detect from
|
|
3683
|
+
* the output.
|
|
3684
|
+
*
|
|
3685
|
+
* With the env token set, the base URL comes from `FLUID_API_BASE` rather than
|
|
3686
|
+
* the stored profile's — a token issued elsewhere has no relationship to the
|
|
3687
|
+
* host that profile points at.
|
|
3688
|
+
*/
|
|
3689
|
+
function createAuthenticatedWidgetPackageClient() {
|
|
3690
|
+
const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"] ?? "";
|
|
3691
|
+
if (envToken.length > 0) return createFluidOsHeyApiClient({
|
|
3692
|
+
baseUrl: process.env["FLUID_API_BASE"] ?? DEFAULT_API_BASE,
|
|
3693
|
+
getAuthToken: () => envToken
|
|
3694
|
+
});
|
|
3695
|
+
const { profile, profileName, token } = resolveAuthenticatedProfile();
|
|
3696
|
+
if (!token) {
|
|
3697
|
+
if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
3698
|
+
throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
3699
|
+
}
|
|
3700
|
+
return createFluidOsHeyApiClient({
|
|
3701
|
+
baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? DEFAULT_API_BASE,
|
|
3702
|
+
getAuthToken: () => token
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
function resolveAuthenticatedProfile() {
|
|
3706
|
+
const config = readConfig();
|
|
3707
|
+
const projectConfig = findProjectConfig(process.cwd());
|
|
3708
|
+
if (projectConfig?.profile) {
|
|
3709
|
+
const profile = config.profiles[projectConfig.profile];
|
|
3710
|
+
if (profile) return {
|
|
3711
|
+
profile,
|
|
3712
|
+
profileName: projectConfig.profile,
|
|
3713
|
+
token: profile.token
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
if (!config.activeProfile) return {};
|
|
3717
|
+
const profile = config.profiles[config.activeProfile];
|
|
3718
|
+
if (!profile) return {};
|
|
3719
|
+
return {
|
|
3720
|
+
profile,
|
|
3721
|
+
profileName: config.activeProfile,
|
|
3722
|
+
token: profile.token
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
function printWidgetPublishSummary(options) {
|
|
3726
|
+
console.log();
|
|
3727
|
+
console.log(chalk.green.bold(`${options.title} complete`));
|
|
3728
|
+
console.log();
|
|
3729
|
+
console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
|
|
3730
|
+
if (options.environment) console.log(chalk.gray("Environment: ") + chalk.white(options.environment));
|
|
3731
|
+
console.log(chalk.gray("Package: ") + chalk.white(options.packageId));
|
|
3732
|
+
console.log(chalk.gray("Version: ") + chalk.white(options.version));
|
|
3733
|
+
console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
|
|
3734
|
+
console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.artifactCount)));
|
|
3735
|
+
console.log();
|
|
3736
|
+
}
|
|
3737
|
+
function printRuntimeOnlyNotice() {
|
|
3738
|
+
console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
|
|
3739
|
+
console.log();
|
|
3740
|
+
}
|
|
3741
|
+
function printDryRunSessionPayload(sessionPayload) {
|
|
3742
|
+
console.log(chalk.bold("Dry-run upload session payload:"));
|
|
3743
|
+
console.log(JSON.stringify(sessionPayload, null, 2));
|
|
3744
|
+
console.log();
|
|
3745
|
+
}
|
|
3746
|
+
//#endregion
|
|
3417
3747
|
//#region src/utils/widget-helpers.ts
|
|
3418
3748
|
/**
|
|
3419
3749
|
* Pure helper functions for widget scaffolding.
|
|
@@ -3503,12 +3833,14 @@ function insertIntoWidgetPackage(source, exportName) {
|
|
|
3503
3833
|
}
|
|
3504
3834
|
//#endregion
|
|
3505
3835
|
//#region src/commands/widget-create.ts
|
|
3506
|
-
const WIDGET_CONFIG_PATH
|
|
3836
|
+
const WIDGET_CONFIG_PATH = path.join("src", "widgets.config.ts");
|
|
3507
3837
|
const createSubcommand = new Command("create").description("Scaffold a new Remote DOM widget").argument("<name>", "Widget name in kebab-case (e.g., stock-ticker)").option("-c, --category <category>", "Widget category for palette grouping", "components").action(async (name, options) => {
|
|
3508
3838
|
const cwd = process.cwd();
|
|
3509
3839
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
3510
|
-
const configPath = path.join(cwd, WIDGET_CONFIG_PATH
|
|
3840
|
+
const configPath = path.join(cwd, WIDGET_CONFIG_PATH);
|
|
3511
3841
|
const widgetDir = path.join(cwd, "src", "widgets", name);
|
|
3842
|
+
let stylesAppended = false;
|
|
3843
|
+
let resolvedPackageKey = null;
|
|
3512
3844
|
const validationError = validateWidgetName(name);
|
|
3513
3845
|
if (validationError) {
|
|
3514
3846
|
console.error(chalk.red(`Error: ${validationError}`));
|
|
@@ -3540,28 +3872,38 @@ const createSubcommand = new Command("create").description("Scaffold a new Remot
|
|
|
3540
3872
|
displayName,
|
|
3541
3873
|
category
|
|
3542
3874
|
}), "utf-8");
|
|
3875
|
+
await appendWidgetStyles(cwd, name);
|
|
3876
|
+
stylesAppended = true;
|
|
3543
3877
|
if (await fs.pathExists(configPath)) {
|
|
3544
3878
|
const configSource = await fs.readFile(configPath, "utf-8");
|
|
3545
|
-
if (isEmptyWidgetConfig(configSource))
|
|
3546
|
-
|
|
3879
|
+
if (isEmptyWidgetConfig(configSource)) {
|
|
3880
|
+
resolvedPackageKey = await resolveCompanyPackageKey();
|
|
3881
|
+
await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName, resolvedPackageKey ?? void 0), "utf-8");
|
|
3882
|
+
} else {
|
|
3547
3883
|
const withImport = insertImport(configSource, importLine);
|
|
3548
3884
|
const updated = withImport ? insertIntoWidgetPackage(withImport, exportName) : null;
|
|
3549
|
-
if (!updated) throw new Error(`${WIDGET_CONFIG_PATH
|
|
3885
|
+
if (!updated) throw new Error(`${WIDGET_CONFIG_PATH} must export the starter widgetPackages placeholder or contain a defineWidgetPackage({ widgets: [...] }) declaration.`);
|
|
3550
3886
|
await fs.writeFile(configPath, updated, "utf-8");
|
|
3551
3887
|
}
|
|
3552
|
-
} else
|
|
3888
|
+
} else {
|
|
3889
|
+
resolvedPackageKey = await resolveCompanyPackageKey();
|
|
3890
|
+
await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName, resolvedPackageKey ?? void 0), "utf-8");
|
|
3891
|
+
}
|
|
3553
3892
|
} catch (error) {
|
|
3554
3893
|
await fs.remove(widgetDir).catch(() => {});
|
|
3894
|
+
if (stylesAppended) await removeWidgetStyles(cwd, name).catch(() => {});
|
|
3555
3895
|
console.error(chalk.red("Error: Failed to scaffold Remote DOM widget."));
|
|
3556
3896
|
console.error(error);
|
|
3557
3897
|
process.exit(1);
|
|
3558
3898
|
}
|
|
3559
3899
|
console.log();
|
|
3560
3900
|
console.log(chalk.green("Created widget:") + ` src/widgets/${name}/index.tsx`);
|
|
3561
|
-
console.log(chalk.green("Registered") + ` in ${chalk.cyan(WIDGET_CONFIG_PATH
|
|
3901
|
+
console.log(chalk.green("Registered") + ` in ${chalk.cyan(WIDGET_CONFIG_PATH)}`);
|
|
3902
|
+
if (resolvedPackageKey) console.log(chalk.green("Package key:") + ` ${chalk.cyan(resolvedPackageKey)} (this company's existing widget package)`);
|
|
3562
3903
|
console.log();
|
|
3563
3904
|
console.log(chalk.yellow("Next steps:"));
|
|
3564
3905
|
console.log(` 1. Edit ${chalk.cyan(`src/widgets/${name}/index.tsx`)}`);
|
|
3906
|
+
console.log(` Styles live in ${chalk.cyan("styles.css")} at the project root — widgets must not import CSS.`);
|
|
3565
3907
|
console.log(` 2. Run ${chalk.cyan("fluid portal deploy --dry-run")} to verify the package`);
|
|
3566
3908
|
});
|
|
3567
3909
|
function createWidgetSource(options) {
|
|
@@ -3571,11 +3913,23 @@ interface ${options.widgetType}Props {
|
|
|
3571
3913
|
readonly title?: string;
|
|
3572
3914
|
}
|
|
3573
3915
|
|
|
3916
|
+
// Styling: a stylesheet is the ONLY way to style a widget. The host sanitizes
|
|
3917
|
+
// every Remote DOM mutation before it reaches the shadow root, so a
|
|
3918
|
+
// \`style={{ ... }}\` prop is silently dropped and \`<style>\` is a banned tag.
|
|
3919
|
+
// Give elements classNames and put the rules in the project-root styles.css
|
|
3920
|
+
// (this widget's rules were appended there when it was scaffolded).
|
|
3921
|
+
//
|
|
3922
|
+
// Do NOT add \`import "./styles.css"\` here. In dev, Vite answers a CSS import
|
|
3923
|
+
// with a browser style-injector that loads \`/@vite/client\`, which touches
|
|
3924
|
+
// HTMLElement — absent in a Web Worker — and throws while the package is being
|
|
3925
|
+
// evaluated, taking down every widget in the project, not just this one.
|
|
3574
3926
|
function ${options.componentName}({ title = ${JSON.stringify(options.displayName)} }: ${options.widgetType}Props) {
|
|
3575
3927
|
return (
|
|
3576
|
-
<section>
|
|
3577
|
-
<h3>{title}</h3>
|
|
3578
|
-
<p
|
|
3928
|
+
<section className="${options.name}">
|
|
3929
|
+
<h3 className="${options.name}__title">{title}</h3>
|
|
3930
|
+
<p className="${options.name}__body">
|
|
3931
|
+
Edit this widget in src/widgets/${options.name}/index.tsx
|
|
3932
|
+
</p>
|
|
3579
3933
|
</section>
|
|
3580
3934
|
);
|
|
3581
3935
|
}
|
|
@@ -3596,1743 +3950,283 @@ export const ${options.exportName} = defineWidget({
|
|
|
3596
3950
|
});
|
|
3597
3951
|
`;
|
|
3598
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Appends a scaffolded widget's rules to the project-root `styles.css`,
|
|
3955
|
+
* creating the file when a project has none yet.
|
|
3956
|
+
*
|
|
3957
|
+
* The root stylesheet is the single sheet the tooling delivers — dev serves it
|
|
3958
|
+
* as the descriptor's `cssUrls`, and the build entry imports it — so every
|
|
3959
|
+
* widget in a project shares it. Appending (rather than overwriting) keeps
|
|
3960
|
+
* earlier widgets' rules intact; re-scaffolding a name that is already present
|
|
3961
|
+
* is a no-op rather than a duplicate block.
|
|
3962
|
+
*/
|
|
3963
|
+
/**
|
|
3964
|
+
* Removes a widget's appended rules from the project-root `styles.css`.
|
|
3965
|
+
*
|
|
3966
|
+
* Only used to undo a failed scaffold — `appendWidgetStyles` runs before the
|
|
3967
|
+
* config update, so a config failure would otherwise strand rules for a widget
|
|
3968
|
+
* directory that has just been deleted. Matches the exact block the append
|
|
3969
|
+
* writes and leaves everything else, including a project's own rules and other
|
|
3970
|
+
* widgets', untouched.
|
|
3971
|
+
*/
|
|
3972
|
+
async function removeWidgetStyles(projectDir, name) {
|
|
3973
|
+
const stylesPath = path.join(projectDir, "styles.css");
|
|
3974
|
+
if (!await fs.pathExists(stylesPath)) return;
|
|
3975
|
+
const existing = await fs.readFile(stylesPath, "utf-8");
|
|
3976
|
+
const block = createWidgetStylesSource(name);
|
|
3977
|
+
if (!existing.includes(block)) return;
|
|
3978
|
+
const withoutBlock = existing.replace(block, "");
|
|
3979
|
+
await fs.writeFile(stylesPath, withoutBlock.replace(/\n{3,}/gu, "\n\n"), "utf-8");
|
|
3980
|
+
}
|
|
3981
|
+
async function appendWidgetStyles(projectDir, name) {
|
|
3982
|
+
const stylesPath = path.join(projectDir, "styles.css");
|
|
3983
|
+
const block = createWidgetStylesSource(name);
|
|
3984
|
+
if (!await fs.pathExists(stylesPath)) {
|
|
3985
|
+
await fs.writeFile(stylesPath, `${ROOT_STYLES_HEADER}\n${block}`, "utf-8");
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
const existing = await fs.readFile(stylesPath, "utf-8");
|
|
3989
|
+
if (existing.includes(`.${name} {`)) return;
|
|
3990
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
3991
|
+
await fs.appendFile(stylesPath, `${separator}${block}`, "utf-8");
|
|
3992
|
+
}
|
|
3993
|
+
const ROOT_STYLES_HEADER = `/* Runtime styles for this portal's widgets.
|
|
3994
|
+
*
|
|
3995
|
+
* One sheet per project: dev serves it to the widget shadow root as the
|
|
3996
|
+
* package descriptor's cssUrls, and the package build imports it. Widget
|
|
3997
|
+
* modules must NOT import CSS themselves — in dev that pulls \`/@vite/client\`
|
|
3998
|
+
* into the Web Worker, which throws and stops every widget in the project from
|
|
3999
|
+
* registering.
|
|
4000
|
+
*/
|
|
4001
|
+
`;
|
|
4002
|
+
/**
|
|
4003
|
+
* Starter rules for a scaffolded widget.
|
|
4004
|
+
*
|
|
4005
|
+
* Colors, type, and radii come from the host portal's semantic CSS variables so
|
|
4006
|
+
* the widget matches whatever portal it lands on, in both light and dark,
|
|
4007
|
+
* without a second hand-written palette. Each `var()` carries a fallback so the
|
|
4008
|
+
* widget stays legible in a host that has not adopted the portal theme.
|
|
4009
|
+
*
|
|
4010
|
+
* Selectors are prefixed with the widget name so nothing leaks into the host.
|
|
4011
|
+
*/
|
|
4012
|
+
function createWidgetStylesSource(name) {
|
|
4013
|
+
return `.${name} {
|
|
4014
|
+
padding: 24px;
|
|
4015
|
+
border: 1px solid var(--border, #e5e7eb);
|
|
4016
|
+
border-radius: var(--radius-lg, 12px);
|
|
4017
|
+
background: var(--card, #ffffff);
|
|
4018
|
+
color: var(--card-foreground, #111827);
|
|
4019
|
+
font-family: var(--font-body, ui-sans-serif, system-ui, sans-serif);
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
.${name}__title {
|
|
4023
|
+
margin: 0 0 8px;
|
|
4024
|
+
color: var(--card-foreground, #111827);
|
|
4025
|
+
font-family: var(--font-header, inherit);
|
|
4026
|
+
font-size: 1.25rem;
|
|
4027
|
+
font-weight: 700;
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
.${name}__body {
|
|
4031
|
+
margin: 0;
|
|
4032
|
+
color: var(--muted-foreground, #6b7280);
|
|
4033
|
+
}
|
|
4034
|
+
`;
|
|
4035
|
+
}
|
|
3599
4036
|
function isEmptyWidgetConfig(source) {
|
|
3600
4037
|
return /export\s+const\s+widgetPackages\s*=\s*\[\]\s+as\s+const\s*;/u.test(source);
|
|
3601
4038
|
}
|
|
3602
|
-
|
|
4039
|
+
/**
|
|
4040
|
+
* The key written when the company's real one cannot be resolved.
|
|
4041
|
+
*
|
|
4042
|
+
* A guess, and one that fails for any company that has already published — a
|
|
4043
|
+
* company has exactly ONE widget package and its key is fixed by whichever
|
|
4044
|
+
* project published first, so this value is only ever correct by accident.
|
|
4045
|
+
*/
|
|
4046
|
+
const PLACEHOLDER_PACKAGE_KEY = "portal-widgets";
|
|
4047
|
+
function createWidgetConfigSource(importLine, exportName, packageKey = PLACEHOLDER_PACKAGE_KEY) {
|
|
3603
4048
|
return `import { defineWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
|
|
3604
4049
|
${importLine}
|
|
3605
4050
|
|
|
3606
4051
|
export const widgetPackage = defineWidgetPackage({
|
|
3607
4052
|
scope: "company",
|
|
3608
|
-
packageStableId:
|
|
4053
|
+
packageStableId: ${JSON.stringify(packageKey)},
|
|
3609
4054
|
version: "0.0.0-dev",
|
|
3610
4055
|
widgets: [${exportName}],
|
|
3611
4056
|
});
|
|
3612
4057
|
`;
|
|
3613
4058
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
4059
|
+
/**
|
|
4060
|
+
* Reads the company's existing widget package key, for a config being CREATED.
|
|
4061
|
+
*
|
|
4062
|
+
* Scaffolding is the one moment this can be done safely. A widget's type is
|
|
4063
|
+
* `company.<key>.<Name>` and those strings are written into screen JSON, so
|
|
4064
|
+
* changing the key later orphans every screen using it — which is why
|
|
4065
|
+
* `fluid portal deploy` reports a mismatch rather than rewriting it. At
|
|
4066
|
+
* `widget create` no screen references the type yet, so resolving it here
|
|
4067
|
+
* prevents the mismatch instead of explaining it afterwards.
|
|
4068
|
+
*
|
|
4069
|
+
* Best-effort by design: this command made no network calls before, and a
|
|
4070
|
+
* scaffold must not fail because a lookup did. Every uncertainty — no token, no
|
|
4071
|
+
* network, a company that has never published, a slow answer — falls back to the
|
|
4072
|
+
* placeholder, which is exactly today's behaviour.
|
|
4073
|
+
*
|
|
4074
|
+
* The deadline is deliberate here, unlike the SDK's dev-server probe where a
|
|
4075
|
+
* timeout discarded a real answer. The fallback IS the correct result for an
|
|
4076
|
+
* offline scaffold, so bounding the wait costs nothing and stops a hung API
|
|
4077
|
+
* from wedging a local command.
|
|
4078
|
+
*/
|
|
4079
|
+
async function resolveCompanyPackageKey() {
|
|
4080
|
+
try {
|
|
4081
|
+
const client = createAuthenticatedWidgetPackageClient();
|
|
4082
|
+
return (await Promise.race([readCompanyWidgetPackages(client), new Promise((resolve) => {
|
|
4083
|
+
setTimeout(() => resolve([]), PACKAGE_KEY_LOOKUP_TIMEOUT_MS);
|
|
4084
|
+
})]))[0]?.key ?? null;
|
|
4085
|
+
} catch {
|
|
4086
|
+
return null;
|
|
3627
4087
|
}
|
|
3628
|
-
};
|
|
3629
|
-
const CONTENT_TYPES = {
|
|
3630
|
-
".css": "text/css",
|
|
3631
|
-
".js": "application/javascript",
|
|
3632
|
-
".json": "application/json",
|
|
3633
|
-
".map": "application/json"
|
|
3634
|
-
};
|
|
3635
|
-
const CSS_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.css$/;
|
|
3636
|
-
const JS_MAP_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.js\.map$/;
|
|
3637
|
-
async function computeArtifactMetadata(filePath, baseDir) {
|
|
3638
|
-
const relativePath = toPosixPath$1(path.relative(baseDir, filePath));
|
|
3639
|
-
assertAllowedArtifactPath(relativePath);
|
|
3640
|
-
await assertRegularArtifactFile(filePath, relativePath);
|
|
3641
|
-
const buffer = await fs.readFile(filePath);
|
|
3642
|
-
return {
|
|
3643
|
-
path: relativePath,
|
|
3644
|
-
sha256: sha256(buffer),
|
|
3645
|
-
bytes: buffer.byteLength,
|
|
3646
|
-
contentType: getArtifactContentType(filePath)
|
|
3647
|
-
};
|
|
3648
|
-
}
|
|
3649
|
-
async function collectWidgetPackageArtifacts(publishDir) {
|
|
3650
|
-
const artifactPaths = await findArtifactPaths(publishDir);
|
|
3651
|
-
const artifacts = await Promise.all(artifactPaths.map((artifactPath) => computeArtifactMetadata(artifactPath, publishDir)));
|
|
3652
|
-
assertRequiredUploadArtifacts(artifacts);
|
|
3653
|
-
return artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
3654
|
-
}
|
|
3655
|
-
function createPublishManifestMetadata(options) {
|
|
3656
|
-
return {
|
|
3657
|
-
packageId: options.packageId,
|
|
3658
|
-
version: options.version,
|
|
3659
|
-
builderVersion: options.builderVersion ?? "0.1.0",
|
|
3660
|
-
cliVersion: options.cliVersion,
|
|
3661
|
-
runtimeVersion: options.runtimeVersion,
|
|
3662
|
-
manifestHash: sha256(options.manifestContent),
|
|
3663
|
-
artifacts: options.artifacts
|
|
3664
|
-
};
|
|
3665
4088
|
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
if (
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
if (!entry.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
|
|
3693
|
-
return [entryPath];
|
|
4089
|
+
const PACKAGE_KEY_LOOKUP_TIMEOUT_MS = 5e3;
|
|
4090
|
+
const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
|
|
4091
|
+
//#endregion
|
|
4092
|
+
//#region src/commands/deploy.ts
|
|
4093
|
+
const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
|
|
4094
|
+
const DEFAULT_ENVIRONMENT = "production";
|
|
4095
|
+
const deployCommand = new Command("deploy").description("Publish company-owned portal widget runtime artifacts").option("-e, --environment <name>", "Target environment label for output reporting", DEFAULT_ENVIRONMENT).option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR$1).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
|
|
4096
|
+
const environment = options.environment ?? DEFAULT_ENVIRONMENT;
|
|
4097
|
+
const outDir = options.outDir ?? DEFAULT_OUT_DIR$1;
|
|
4098
|
+
const dryRun = options.dryRun === true;
|
|
4099
|
+
console.log();
|
|
4100
|
+
console.log(chalk.blue.bold("Fluid Portal Deploy"));
|
|
4101
|
+
console.log();
|
|
4102
|
+
printRuntimeOnlyNotice();
|
|
4103
|
+
console.log(chalk.gray("Environment: ") + chalk.white(environment));
|
|
4104
|
+
console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
|
|
4105
|
+
if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
|
|
4106
|
+
console.log();
|
|
4107
|
+
const spinner = ora("Building company-owned widget package...").start();
|
|
4108
|
+
const result = await runPortalDeploy({
|
|
4109
|
+
projectDir: process.cwd(),
|
|
4110
|
+
outDir,
|
|
4111
|
+
environment,
|
|
4112
|
+
dryRun,
|
|
4113
|
+
onProgress: (label) => {
|
|
4114
|
+
spinner.text = label;
|
|
3694
4115
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
4116
|
+
});
|
|
4117
|
+
if (!result.ok) {
|
|
4118
|
+
spinner.fail("Portal deploy failed");
|
|
4119
|
+
console.log();
|
|
4120
|
+
if (result.code === "PACKAGE_KEY_MISMATCH") console.log(chalk.red("Package key does not match this company."));
|
|
4121
|
+
else console.error(chalk.red("Error:") + " " + result.message);
|
|
4122
|
+
if (result.hint) {
|
|
4123
|
+
console.log();
|
|
4124
|
+
console.log(result.hint);
|
|
3699
4125
|
}
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
}))).flat();
|
|
3703
|
-
}
|
|
3704
|
-
function assertRequiredUploadArtifacts(artifacts) {
|
|
3705
|
-
const artifactPaths = new Set(artifacts.map((artifact) => artifact.path));
|
|
3706
|
-
for (const requiredPath of ["widget.js", "manifest.json"]) {
|
|
3707
|
-
if (artifactPaths.has(requiredPath)) continue;
|
|
3708
|
-
throw new Error(`Widget package publish output must include regular file ${JSON.stringify(requiredPath)}.`);
|
|
4126
|
+
console.log();
|
|
4127
|
+
process.exit(1);
|
|
3709
4128
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
}
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
}
|
|
3730
|
-
//#endregion
|
|
3731
|
-
//#region ../../portal/core/src/remote-dom-widget-package.ts
|
|
3732
|
-
const UNSAFE_JSON_KEYS = new Set([
|
|
3733
|
-
"__proto__",
|
|
3734
|
-
"constructor",
|
|
3735
|
-
"prototype"
|
|
3736
|
-
]);
|
|
3737
|
-
function isJsonValue(value) {
|
|
3738
|
-
return isJsonValueAtDepth(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
3739
|
-
}
|
|
3740
|
-
function isJsonValueAtDepth(value, ancestors, depth) {
|
|
3741
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
3742
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
3743
|
-
if (typeof value !== "object" || depth > 50 || ancestors.has(value)) return false;
|
|
3744
|
-
ancestors.add(value);
|
|
3745
|
-
const prototype = Object.getPrototypeOf(value);
|
|
3746
|
-
const isJson = Array.isArray(value) ? value.every((item) => isJsonValueAtDepth(item, ancestors, depth + 1)) : (prototype === Object.prototype || prototype === null) && isRecord$5(value) && Object.entries(value).every(([key, item]) => !UNSAFE_JSON_KEYS.has(key) && isJsonValueAtDepth(item, ancestors, depth + 1));
|
|
3747
|
-
ancestors.delete(value);
|
|
3748
|
-
return isJson;
|
|
3749
|
-
}
|
|
3750
|
-
function isRecord$5(value) {
|
|
3751
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3752
|
-
}
|
|
3753
|
-
//#endregion
|
|
3754
|
-
//#region ../widget-tooling/src/internal/widget-package-validation.ts
|
|
3755
|
-
const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
|
|
3756
|
-
const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
|
|
3757
|
-
const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
|
|
3758
|
-
const SEMVER_CORE_PATTERN = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)";
|
|
3759
|
-
const SEMVER_PRERELEASE_IDENTIFIER_PATTERN = "(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)";
|
|
3760
|
-
const SEMVER_URL_SAFE_PATTERN = new RegExp(`^${SEMVER_CORE_PATTERN}(?:-${SEMVER_PRERELEASE_IDENTIFIER_PATTERN}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER_PATTERN})*)?$`);
|
|
3761
|
-
const WIDGET_PACKAGE_MANIFEST_VERSION = 1;
|
|
3762
|
-
const DEFAULT_WIDGET_ICON = "box";
|
|
3763
|
-
const DEFAULT_WIDGET_CATEGORY = "components";
|
|
3764
|
-
const DEFAULT_WIDGET_CONTAINER = "block";
|
|
3765
|
-
const DEFAULT_MIN_SDK_VERSION = "0.0.0";
|
|
3766
|
-
function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
|
|
3767
|
-
if (sourcePackages.length === 0) return validationFailure({
|
|
3768
|
-
code: "NO_SOURCE_PACKAGE",
|
|
3769
|
-
message: "No defineWidgetPackage source package was found. Export widgetPackage, widgetPackages, or a default widget package from src/widgets.config.ts."
|
|
3770
|
-
});
|
|
3771
|
-
if (sourcePackages.length > 1) return validationFailure({
|
|
3772
|
-
code: "MULTIPLE_SOURCE_PACKAGES",
|
|
3773
|
-
message: "Publish/deploy supports exactly one defineWidgetPackage source package. Export one package or split packages into separate builds."
|
|
3774
|
-
});
|
|
3775
|
-
const sourcePackageValue = sourcePackages[0];
|
|
3776
|
-
if (!sourcePackageValue) return validationFailure({
|
|
3777
|
-
code: "NO_SOURCE_PACKAGE",
|
|
3778
|
-
message: "No defineWidgetPackage source package was found."
|
|
3779
|
-
});
|
|
3780
|
-
const shapeErrors = validateSourcePackageShape(sourcePackageValue);
|
|
3781
|
-
if (shapeErrors.length > 0) return {
|
|
3782
|
-
success: false,
|
|
3783
|
-
errors: shapeErrors
|
|
3784
|
-
};
|
|
3785
|
-
const sourcePackage = sourcePackageValue;
|
|
3786
|
-
const errors = [];
|
|
3787
|
-
const owner = options.owner ?? sourcePackage.packageType;
|
|
3788
|
-
if (owner !== "company" && owner !== "droplet") errors.push({
|
|
3789
|
-
code: "UNSUPPORTED_OWNER",
|
|
3790
|
-
path: "packageType",
|
|
3791
|
-
message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
|
|
3792
|
-
});
|
|
3793
|
-
else if (options.owner !== void 0 && sourcePackage.packageType !== options.owner) errors.push({
|
|
3794
|
-
code: "UNSUPPORTED_OWNER",
|
|
3795
|
-
path: "packageType",
|
|
3796
|
-
message: `Widget package metadata must set packageType to "${options.owner}" for this workflow.`
|
|
3797
|
-
});
|
|
3798
|
-
const resolvedOwner = owner === "droplet" ? "droplet" : "company";
|
|
3799
|
-
if (resolvedOwner === "droplet" && !readNonEmptyTrimmedString(sourcePackage.packageStableId)) errors.push({
|
|
3800
|
-
code: "INVALID_PACKAGE_KEY",
|
|
3801
|
-
path: "packageStableId",
|
|
3802
|
-
message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
|
|
3803
|
-
});
|
|
3804
|
-
const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
|
|
3805
|
-
const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
|
|
3806
|
-
if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
|
|
3807
|
-
code: "INVALID_PACKAGE_KEY",
|
|
3808
|
-
path: packageKeyPath,
|
|
3809
|
-
message: "Widget package key must be URL-safe dot-separated text (letters, numbers, '.', '_', '~', and '-' only)."
|
|
3810
|
-
});
|
|
3811
|
-
if (!isUrlSafeSemver(sourcePackage.version)) errors.push({
|
|
3812
|
-
code: "INVALID_VERSION",
|
|
3813
|
-
path: "version",
|
|
3814
|
-
message: "Widget package version must be a URL-safe SemVer version without build metadata (for example 1.2.3 or 1.2.3-beta.1)."
|
|
3815
|
-
});
|
|
3816
|
-
if (sourcePackage.widgets.length === 0) errors.push({
|
|
3817
|
-
code: "NO_WIDGETS",
|
|
3818
|
-
path: "widgets",
|
|
3819
|
-
message: "Widget package must include at least one widget."
|
|
4129
|
+
spinner.succeed("Built widget runtime artifacts");
|
|
4130
|
+
if (dryRun) {
|
|
4131
|
+
console.log(chalk.yellow("Dry run complete — upload session was not requested."));
|
|
4132
|
+
console.log();
|
|
4133
|
+
if (result.sessionPayload !== void 0) printDryRunSessionPayload(result.sessionPayload);
|
|
4134
|
+
} else console.log(chalk.green("Published widget package version."));
|
|
4135
|
+
if (result.requestedVersion) {
|
|
4136
|
+
console.log(chalk.yellow(`Version ${result.requestedVersion} is already published for this company, so this went out as ${result.version}.`));
|
|
4137
|
+
console.log(chalk.gray("Every portal project in a company shares one widget package, so its versions are shared too."));
|
|
4138
|
+
console.log();
|
|
4139
|
+
}
|
|
4140
|
+
printWidgetPublishSummary({
|
|
4141
|
+
title: dryRun ? "Portal deploy dry run" : "Portal deploy",
|
|
4142
|
+
ownerLabel: "company",
|
|
4143
|
+
environment,
|
|
4144
|
+
outDir,
|
|
4145
|
+
packageId: result.packageId,
|
|
4146
|
+
version: result.version,
|
|
4147
|
+
artifactCount: result.artifactCount
|
|
3820
4148
|
});
|
|
3821
|
-
const packageId = `${resolvedOwner}.${packageKey}`;
|
|
3822
|
-
const seenNames = /* @__PURE__ */ new Set();
|
|
3823
|
-
const seenTypes = /* @__PURE__ */ new Set();
|
|
3824
|
-
const widgets = [];
|
|
3825
|
-
sourcePackage.widgets.forEach((widget, index) => {
|
|
3826
|
-
const path = `widgets[${index}]`;
|
|
3827
|
-
if (!isUrlSafeWidgetName(widget.name)) {
|
|
3828
|
-
errors.push({
|
|
3829
|
-
code: "INVALID_WIDGET_NAME",
|
|
3830
|
-
path: `${path}.name`,
|
|
3831
|
-
message: "Widget name must be URL-safe text (letters, numbers, '_', '~', and '-' only) and cannot be empty."
|
|
3832
|
-
});
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3835
|
-
if (seenNames.has(widget.name)) {
|
|
3836
|
-
errors.push({
|
|
3837
|
-
code: "DUPLICATE_WIDGET",
|
|
3838
|
-
path: `${path}.name`,
|
|
3839
|
-
message: `Duplicate widget name "${widget.name}" found in package ${packageKey}.`
|
|
3840
|
-
});
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
seenNames.add(widget.name);
|
|
3844
|
-
const type = `${packageId}.${widget.name}`;
|
|
3845
|
-
if (!isUrlSafeDottedIdentifier(type)) {
|
|
3846
|
-
errors.push({
|
|
3847
|
-
code: "INVALID_WIDGET_TYPE",
|
|
3848
|
-
path: `${path}.type`,
|
|
3849
|
-
message: `Generated widget type "${type}" is not URL-safe.`
|
|
3850
|
-
});
|
|
3851
|
-
return;
|
|
3852
|
-
}
|
|
3853
|
-
if (seenTypes.has(type)) {
|
|
3854
|
-
errors.push({
|
|
3855
|
-
code: "DUPLICATE_WIDGET",
|
|
3856
|
-
path: `${path}.type`,
|
|
3857
|
-
message: `Duplicate generated widget type "${type}" found.`
|
|
3858
|
-
});
|
|
3859
|
-
return;
|
|
3860
|
-
}
|
|
3861
|
-
seenTypes.add(type);
|
|
3862
|
-
widgets.push(sourceWidgetToDescriptor(widget, type));
|
|
3863
|
-
});
|
|
3864
|
-
if (errors.length > 0) return {
|
|
3865
|
-
success: false,
|
|
3866
|
-
errors
|
|
3867
|
-
};
|
|
3868
|
-
return {
|
|
3869
|
-
success: true,
|
|
3870
|
-
value: {
|
|
3871
|
-
sourcePackage,
|
|
3872
|
-
packageKey,
|
|
3873
|
-
packageId,
|
|
3874
|
-
owner: resolvedOwner,
|
|
3875
|
-
version: sourcePackage.version,
|
|
3876
|
-
widgets
|
|
3877
|
-
},
|
|
3878
|
-
errors: []
|
|
3879
|
-
};
|
|
3880
|
-
}
|
|
3881
|
-
function buildWidgetPackagePublishManifest(validated, options = {}) {
|
|
3882
|
-
return {
|
|
3883
|
-
manifestVersion: WIDGET_PACKAGE_MANIFEST_VERSION,
|
|
3884
|
-
runtime: "remote-dom",
|
|
3885
|
-
packageId: validated.packageId,
|
|
3886
|
-
source: validated.owner,
|
|
3887
|
-
version: validated.version,
|
|
3888
|
-
workerEntryUrl: toWidgetPackageArtifactPath("widget.js"),
|
|
3889
|
-
cssUrls: mergeCssUrls(validated.sourcePackage.cssUrls, options.cssUrls),
|
|
3890
|
-
assetUrls: [],
|
|
3891
|
-
widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
|
|
3892
|
-
};
|
|
3893
|
-
}
|
|
3894
|
-
function sourceWidgetToDescriptor(widget, type) {
|
|
3895
|
-
const displayName = widget.displayName ?? widget.name;
|
|
3896
|
-
return {
|
|
3897
|
-
type,
|
|
3898
|
-
name: widget.name,
|
|
3899
|
-
displayName,
|
|
3900
|
-
description: widget.description ?? `Custom widget ${displayName}`,
|
|
3901
|
-
icon: widget.icon ?? DEFAULT_WIDGET_ICON,
|
|
3902
|
-
category: widget.category ?? DEFAULT_WIDGET_CATEGORY,
|
|
3903
|
-
propertySchema: normalizePropertySchema(widget.propertySchema, type),
|
|
3904
|
-
defaultProps: widget.defaultProps ?? {},
|
|
3905
|
-
container: normalizeContainer(widget.container),
|
|
3906
|
-
minSdkVersion: widget.minSdkVersion ?? DEFAULT_MIN_SDK_VERSION,
|
|
3907
|
-
resizable: normalizeResizable(widget.resizable),
|
|
3908
|
-
capabilities: normalizeCapabilities(widget.capabilities ?? [])
|
|
3909
|
-
};
|
|
3910
|
-
}
|
|
3911
|
-
function normalizeCapabilities(capabilities) {
|
|
3912
|
-
return capabilities.map((capability) => ({
|
|
3913
|
-
name: capability.name,
|
|
3914
|
-
version: capability.version ?? "1"
|
|
3915
|
-
}));
|
|
3916
|
-
}
|
|
3917
|
-
function validationFailure(error) {
|
|
3918
|
-
return {
|
|
3919
|
-
success: false,
|
|
3920
|
-
errors: [error]
|
|
3921
|
-
};
|
|
3922
|
-
}
|
|
3923
|
-
function validateSourcePackageShape(value) {
|
|
3924
|
-
if (!isRecord$4(value)) return [{
|
|
3925
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
3926
|
-
message: "Widget source package must be a JSON object."
|
|
3927
|
-
}];
|
|
3928
|
-
const errors = [];
|
|
3929
|
-
requireStringField(value, "packageId", "packageId", errors);
|
|
3930
|
-
requireStringField(value, "version", "version", errors);
|
|
3931
|
-
validateOptionalStringField(value, "scope", "scope", errors);
|
|
3932
|
-
validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
|
|
3933
|
-
validateOptionalStringField(value, "packageType", "packageType", errors);
|
|
3934
|
-
if (Object.prototype.hasOwnProperty.call(value, "navigationOrigins")) errors.push({
|
|
3935
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
3936
|
-
path: "navigationOrigins",
|
|
3937
|
-
message: "navigationOrigins has been removed. Declare the externalNavigation capability and call allowAnchorUrl(url) for each external link."
|
|
3938
|
-
});
|
|
3939
|
-
if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
|
|
3940
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
3941
|
-
path: "cssUrls",
|
|
3942
|
-
message: "Widget package cssUrls must be an array of strings when present."
|
|
3943
|
-
});
|
|
3944
|
-
else value.cssUrls.forEach((cssUrl, index) => {
|
|
3945
|
-
validateCssUrl(cssUrl, `cssUrls[${index}]`, errors);
|
|
3946
|
-
});
|
|
3947
|
-
if (!Array.isArray(value.widgets)) {
|
|
3948
|
-
errors.push({
|
|
3949
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
3950
|
-
path: "widgets",
|
|
3951
|
-
message: "Widget package widgets must be an array."
|
|
3952
|
-
});
|
|
3953
|
-
return errors;
|
|
3954
|
-
}
|
|
3955
|
-
value.widgets.forEach((widget, index) => {
|
|
3956
|
-
const widgetPath = `widgets[${index}]`;
|
|
3957
|
-
if (!isRecord$4(widget)) {
|
|
3958
|
-
errors.push({
|
|
3959
|
-
code: "INVALID_WIDGET_METADATA",
|
|
3960
|
-
path: widgetPath,
|
|
3961
|
-
message: "Widget metadata must be a JSON object."
|
|
3962
|
-
});
|
|
3963
|
-
return;
|
|
3964
|
-
}
|
|
3965
|
-
requireStringField(widget, "name", `${widgetPath}.name`, errors, {
|
|
3966
|
-
code: "INVALID_WIDGET_NAME",
|
|
3967
|
-
message: "Widget name must be a string and cannot be empty."
|
|
3968
|
-
});
|
|
3969
|
-
validateOptionalStringField(widget, "displayName", `${widgetPath}.displayName`, errors, { requireNonEmpty: true });
|
|
3970
|
-
validateOptionalStringField(widget, "description", `${widgetPath}.description`, errors, { requireNonEmpty: true });
|
|
3971
|
-
validateOptionalStringField(widget, "icon", `${widgetPath}.icon`, errors, { requireNonEmpty: true });
|
|
3972
|
-
validateOptionalStringField(widget, "category", `${widgetPath}.category`, errors, { requireNonEmpty: true });
|
|
3973
|
-
validateOptionalStringField(widget, "minSdkVersion", `${widgetPath}.minSdkVersion`, errors, { requireNonEmpty: true });
|
|
3974
|
-
validateOptionalJsonRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
|
|
3975
|
-
validateOptionalJsonRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
|
|
3976
|
-
validateOptionalCapabilities(widget, "capabilities", `${widgetPath}.capabilities`, errors);
|
|
3977
|
-
});
|
|
3978
|
-
return errors;
|
|
3979
|
-
}
|
|
3980
|
-
function requireStringField(record, key, path, errors, override) {
|
|
3981
|
-
const value = record[key];
|
|
3982
|
-
if (typeof value === "string" && value.trim().length > 0) return;
|
|
3983
|
-
errors.push({
|
|
3984
|
-
code: override?.code ?? "INVALID_SOURCE_PACKAGE",
|
|
3985
|
-
path,
|
|
3986
|
-
message: override?.message ?? `${path} must be a string and cannot be empty.`
|
|
3987
|
-
});
|
|
3988
|
-
}
|
|
3989
|
-
function validateOptionalStringField(record, key, path, errors, options = {}) {
|
|
3990
|
-
const value = record[key];
|
|
3991
|
-
if (value === void 0) return;
|
|
3992
|
-
if (typeof value === "string") {
|
|
3993
|
-
if (!options.requireNonEmpty || value.trim().length > 0) return;
|
|
3994
|
-
errors.push({
|
|
3995
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
3996
|
-
path,
|
|
3997
|
-
message: `${path} must be a non-empty string when present.`
|
|
3998
|
-
});
|
|
3999
|
-
return;
|
|
4000
|
-
}
|
|
4001
|
-
errors.push({
|
|
4002
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
4003
|
-
path,
|
|
4004
|
-
message: `${path} must be a string when present.`
|
|
4005
|
-
});
|
|
4006
|
-
}
|
|
4007
|
-
function validateCssUrl(value, cssUrlPath, errors) {
|
|
4008
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4009
|
-
errors.push({
|
|
4010
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
4011
|
-
path: cssUrlPath,
|
|
4012
|
-
message: "Widget package cssUrls entries must be non-empty strings."
|
|
4013
|
-
});
|
|
4014
|
-
return;
|
|
4015
|
-
}
|
|
4016
|
-
if (!isWidgetPackageCssArtifactPath(value)) errors.push({
|
|
4017
|
-
code: "INVALID_SOURCE_PACKAGE",
|
|
4018
|
-
path: cssUrlPath,
|
|
4019
|
-
message: "Widget package cssUrls entries must be top-level generated CSS artifact filenames matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them."
|
|
4020
|
-
});
|
|
4021
|
-
}
|
|
4022
|
-
function validateOptionalJsonRecordField(record, key, path, errors) {
|
|
4023
|
-
const value = record[key];
|
|
4024
|
-
if (value === void 0) return;
|
|
4025
|
-
if (isRecord$4(value) && isJsonValue(value)) return;
|
|
4026
|
-
errors.push({
|
|
4027
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4028
|
-
path,
|
|
4029
|
-
message: `${path} must be a JSON object when present.`
|
|
4030
|
-
});
|
|
4031
|
-
}
|
|
4032
|
-
function validateOptionalCapabilities(record, key, path, errors) {
|
|
4033
|
-
const value = record[key];
|
|
4034
|
-
if (value === void 0) return;
|
|
4035
|
-
if (!Array.isArray(value)) {
|
|
4036
|
-
errors.push({
|
|
4037
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4038
|
-
path,
|
|
4039
|
-
message: `${path} must be an array when present.`
|
|
4040
|
-
});
|
|
4041
|
-
return;
|
|
4042
|
-
}
|
|
4043
|
-
const names = /* @__PURE__ */ new Set();
|
|
4044
|
-
value.forEach((candidate, index) => {
|
|
4045
|
-
const capabilityPath = `${path}[${index}]`;
|
|
4046
|
-
if (!isRecord$4(candidate)) {
|
|
4047
|
-
errors.push({
|
|
4048
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4049
|
-
path: capabilityPath,
|
|
4050
|
-
message: `${capabilityPath} must be a JSON object.`
|
|
4051
|
-
});
|
|
4052
|
-
return;
|
|
4053
|
-
}
|
|
4054
|
-
const name = candidate.name;
|
|
4055
|
-
if (!isCanonicalMetadataString(name)) {
|
|
4056
|
-
errors.push({
|
|
4057
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4058
|
-
path: `${capabilityPath}.name`,
|
|
4059
|
-
message: `${capabilityPath}.name must be a non-empty string without surrounding whitespace or control characters.`
|
|
4060
|
-
});
|
|
4061
|
-
return;
|
|
4062
|
-
}
|
|
4063
|
-
const version = candidate.version;
|
|
4064
|
-
if (version !== void 0 && !isCanonicalMetadataString(version)) errors.push({
|
|
4065
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4066
|
-
path: `${capabilityPath}.version`,
|
|
4067
|
-
message: `${capabilityPath}.version must be a non-empty string without surrounding whitespace or control characters when present.`
|
|
4068
|
-
});
|
|
4069
|
-
if (names.has(name)) {
|
|
4070
|
-
errors.push({
|
|
4071
|
-
code: "INVALID_WIDGET_METADATA",
|
|
4072
|
-
path: `${capabilityPath}.name`,
|
|
4073
|
-
message: `${path} must not contain duplicate capability name ${JSON.stringify(name)}.`
|
|
4074
|
-
});
|
|
4075
|
-
return;
|
|
4076
|
-
}
|
|
4077
|
-
names.add(name);
|
|
4078
|
-
});
|
|
4079
|
-
}
|
|
4080
|
-
function isCanonicalMetadataString(value) {
|
|
4081
|
-
return typeof value === "string" && value.length > 0 && value === value.trim() && !/\p{Cc}/u.test(value);
|
|
4082
|
-
}
|
|
4083
|
-
function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
|
|
4084
|
-
const generatedCssUrlSet = new Set(generatedCssUrls ?? []);
|
|
4085
|
-
generatedCssUrls?.forEach((cssUrl, index) => {
|
|
4086
|
-
validateGeneratedCssArtifactUrl(cssUrl, `generated cssUrls[${index}]`);
|
|
4087
|
-
});
|
|
4088
|
-
sourceCssUrls?.forEach((cssUrl, index) => {
|
|
4089
|
-
validateGeneratedCssArtifactUrl(cssUrl, `cssUrls[${index}]`);
|
|
4090
|
-
if (generatedCssUrlSet.has(cssUrl)) return;
|
|
4091
|
-
throw new Error(`cssUrls[${index}] URL ${JSON.stringify(cssUrl)} must match a generated top-level CSS artifact. Import the CSS from the widget package bundle or remove it from cssUrls.`);
|
|
4092
|
-
});
|
|
4093
|
-
return Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []])).map(toWidgetPackageArtifactPath);
|
|
4094
|
-
}
|
|
4095
|
-
function toWidgetPackageArtifactPath(value) {
|
|
4096
|
-
return value;
|
|
4097
|
-
}
|
|
4098
|
-
function validateGeneratedCssArtifactUrl(value, cssUrlPath) {
|
|
4099
|
-
if (value.trim().length === 0) throw new Error(`${cssUrlPath} must be a non-empty string.`);
|
|
4100
|
-
if (!isWidgetPackageCssArtifactPath(value)) throw new Error(`${cssUrlPath} must be a top-level generated CSS artifact filename matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them.`);
|
|
4101
|
-
}
|
|
4102
|
-
function normalizePropertySchema(value, widgetType) {
|
|
4103
|
-
return {
|
|
4104
|
-
...value ?? {},
|
|
4105
|
-
widgetType
|
|
4106
|
-
};
|
|
4107
|
-
}
|
|
4108
|
-
function normalizeContainer(value) {
|
|
4109
|
-
if (value === "inline" || value === "block" || value === "card" || value === "fullscreen") return value;
|
|
4110
|
-
return DEFAULT_WIDGET_CONTAINER;
|
|
4111
|
-
}
|
|
4112
|
-
function normalizeResizable(value) {
|
|
4113
|
-
if (typeof value === "boolean") return value;
|
|
4114
|
-
if (value === "horizontal" || value === "vertical" || value === "both") return value;
|
|
4115
|
-
if (!isRecord$4(value)) return false;
|
|
4116
|
-
const horizontal = value.horizontal === true;
|
|
4117
|
-
const vertical = value.vertical === true;
|
|
4118
|
-
if (horizontal && vertical) return "both";
|
|
4119
|
-
if (horizontal) return "horizontal";
|
|
4120
|
-
if (vertical) return "vertical";
|
|
4121
|
-
return false;
|
|
4122
|
-
}
|
|
4123
|
-
function resolvePackageKey(sourcePackage, owner) {
|
|
4124
|
-
const packageStableId = readNonEmptyTrimmedString(sourcePackage.packageStableId);
|
|
4125
|
-
if (packageStableId) return prefixPackageStableIdWithNonOwnerScope(packageStableId, sourcePackage.scope);
|
|
4126
|
-
return stripOwnerScopeFromPackageId(sourcePackage.packageId, sourcePackage.scope, owner);
|
|
4127
|
-
}
|
|
4128
|
-
function prefixPackageStableIdWithNonOwnerScope(packageStableId, sourceScope) {
|
|
4129
|
-
const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
|
|
4130
|
-
if (!sourceScopeText || isWidgetPackageOwnerKind(sourceScopeText)) return packageStableId;
|
|
4131
|
-
const sourceScopePrefix = `${sourceScopeText}.`;
|
|
4132
|
-
if (packageStableId.startsWith(sourceScopePrefix)) return packageStableId;
|
|
4133
|
-
return `${sourceScopeText}.${packageStableId}`;
|
|
4134
|
-
}
|
|
4135
|
-
function stripOwnerScopeFromPackageId(packageId, sourceScope, owner) {
|
|
4136
|
-
const ownerPrefix = `${owner}.`;
|
|
4137
|
-
if (packageId.startsWith(ownerPrefix) && packageId.length > ownerPrefix.length) return packageId.slice(ownerPrefix.length);
|
|
4138
|
-
const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
|
|
4139
|
-
if (isWidgetPackageOwnerKind(sourceScopeText) && packageId.startsWith(`${sourceScopeText}.`) && packageId.length > sourceScopeText.length + 1) return packageId.slice(sourceScopeText.length + 1);
|
|
4140
|
-
return packageId;
|
|
4141
|
-
}
|
|
4142
|
-
function readNonEmptyTrimmedString(value) {
|
|
4143
|
-
const trimmed = value?.trim();
|
|
4144
|
-
return trimmed ? trimmed : void 0;
|
|
4145
|
-
}
|
|
4146
|
-
function isWidgetPackageOwnerKind(value) {
|
|
4147
|
-
return value === "company" || value === "droplet";
|
|
4148
|
-
}
|
|
4149
|
-
function isUrlSafeDottedIdentifier(value) {
|
|
4150
|
-
return URL_SAFE_DOTTED_IDENTIFIER_PATTERN.test(value);
|
|
4151
|
-
}
|
|
4152
|
-
function isUrlSafeWidgetName(value) {
|
|
4153
|
-
return URL_SAFE_WIDGET_NAME_PATTERN.test(value);
|
|
4154
|
-
}
|
|
4155
|
-
function isUrlSafeSemver(value) {
|
|
4156
|
-
return SEMVER_URL_SAFE_PATTERN.test(value);
|
|
4157
|
-
}
|
|
4158
|
-
function isRecord$4(value) {
|
|
4159
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4160
|
-
}
|
|
4161
|
-
//#endregion
|
|
4162
|
-
//#region ../widget-tooling/src/internal/widget-package-config.ts
|
|
4163
|
-
const WIDGET_CONFIG_PATH = "src/widgets.config.ts";
|
|
4164
|
-
const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
|
|
4165
|
-
const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
|
|
4166
|
-
const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
|
|
4167
|
-
const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
|
|
4168
|
-
async function resolvePortalWidgetSourceConfig(projectDir) {
|
|
4169
|
-
const candidate = path.join(projectDir, WIDGET_CONFIG_PATH);
|
|
4170
|
-
return await fs.pathExists(candidate) ? {
|
|
4171
|
-
path: candidate,
|
|
4172
|
-
relativePath: WIDGET_CONFIG_PATH
|
|
4173
|
-
} : void 0;
|
|
4174
|
-
}
|
|
4175
|
-
async function loadSourceWidgetPackages(projectDir) {
|
|
4176
|
-
const config = await resolvePortalWidgetSourceConfig(projectDir);
|
|
4177
|
-
if (!config) return success([]);
|
|
4178
|
-
let tempDir;
|
|
4179
|
-
try {
|
|
4180
|
-
tempDir = await createTempDirectory(projectDir);
|
|
4181
|
-
const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
|
|
4182
|
-
const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
|
|
4183
|
-
await fs.writeFile(extractFile, createSourcePackageExtractorScript({
|
|
4184
|
-
projectDir,
|
|
4185
|
-
configPath: config.path
|
|
4186
|
-
}), {
|
|
4187
|
-
encoding: "utf-8",
|
|
4188
|
-
flag: "wx"
|
|
4189
|
-
});
|
|
4190
|
-
await execa(process.execPath, [
|
|
4191
|
-
TSX_CLI_PATH,
|
|
4192
|
-
extractFile,
|
|
4193
|
-
outputFile
|
|
4194
|
-
], {
|
|
4195
|
-
cwd: projectDir,
|
|
4196
|
-
stdio: "pipe",
|
|
4197
|
-
env: {
|
|
4198
|
-
...process.env,
|
|
4199
|
-
NODE_ENV: "production"
|
|
4200
|
-
}
|
|
4201
|
-
});
|
|
4202
|
-
const outputResult = await readSourcePackageExtractorOutput(outputFile);
|
|
4203
|
-
if (!outputResult.success) return outputResult;
|
|
4204
|
-
const parsed = outputResult.value;
|
|
4205
|
-
if (parsed.length === 0) return success([]);
|
|
4206
|
-
return success(parsed);
|
|
4207
|
-
} catch (err) {
|
|
4208
|
-
const error = err;
|
|
4209
|
-
return failure({
|
|
4210
|
-
code: "CONFIG_LOAD_FAILED",
|
|
4211
|
-
message: `Failed to load widget packages from ${config.relativePath}`,
|
|
4212
|
-
details: error.stderr ?? error.message ?? String(err)
|
|
4213
|
-
});
|
|
4214
|
-
} finally {
|
|
4215
|
-
if (tempDir) await fs.remove(tempDir).catch(() => {});
|
|
4216
|
-
}
|
|
4217
|
-
}
|
|
4218
|
-
async function createTempDirectory(projectDir) {
|
|
4219
|
-
const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
|
|
4220
|
-
try {
|
|
4221
|
-
await fs.ensureDir(projectTmpDir);
|
|
4222
|
-
return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
|
|
4223
|
-
} catch (err) {
|
|
4224
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4225
|
-
throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
async function readSourcePackageExtractorOutput(outputFile) {
|
|
4229
|
-
let output;
|
|
4230
|
-
try {
|
|
4231
|
-
output = await fs.readFile(outputFile, "utf-8");
|
|
4232
|
-
} catch (err) {
|
|
4233
|
-
return failure({
|
|
4234
|
-
code: "CONFIG_LOAD_FAILED",
|
|
4235
|
-
message: "Widget package extractor did not write an output file",
|
|
4236
|
-
details: err instanceof Error ? err.message : String(err)
|
|
4237
|
-
});
|
|
4238
|
-
}
|
|
4239
|
-
let parsed;
|
|
4240
|
-
try {
|
|
4241
|
-
parsed = JSON.parse(output);
|
|
4242
|
-
} catch {
|
|
4243
|
-
return failure({
|
|
4244
|
-
code: "INVALID_FORMAT",
|
|
4245
|
-
message: "Failed to parse widget package output file as JSON",
|
|
4246
|
-
details: `Output was: ${output.slice(0, 200)}`
|
|
4247
|
-
});
|
|
4248
|
-
}
|
|
4249
|
-
if (!isRecord$3(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
|
|
4250
|
-
code: "INVALID_FORMAT",
|
|
4251
|
-
message: "Widget package extractor output file had an invalid sentinel"
|
|
4252
|
-
});
|
|
4253
|
-
if (!Array.isArray(parsed.data)) return failure({
|
|
4254
|
-
code: "INVALID_FORMAT",
|
|
4255
|
-
message: "Extracted widget package output is not an array",
|
|
4256
|
-
details: `Expected an array, got: ${typeof parsed.data}`
|
|
4257
|
-
});
|
|
4258
|
-
return success(parsed.data);
|
|
4259
|
-
}
|
|
4260
|
-
function createSourcePackageExtractorScript(options) {
|
|
4261
|
-
return `
|
|
4262
|
-
import fs from "node:fs/promises";
|
|
4263
|
-
import path from "node:path";
|
|
4264
|
-
import { createServer, normalizePath } from "vite";
|
|
4265
|
-
|
|
4266
|
-
const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
|
|
4267
|
-
const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
|
|
4268
|
-
const projectRoot = ${JSON.stringify(options.projectDir)};
|
|
4269
|
-
const widgetConfigPath = ${JSON.stringify(options.configPath)};
|
|
4270
|
-
const outputPath = process.argv[2];
|
|
4271
|
-
if (!outputPath) throw new Error("Missing widget package extractor output path.");
|
|
4272
|
-
|
|
4273
|
-
function toViteModuleId(modulePath) {
|
|
4274
|
-
const relativePath = path.relative(projectRoot, modulePath);
|
|
4275
|
-
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
4276
|
-
return "/" + normalizePath(relativePath);
|
|
4277
|
-
}
|
|
4278
|
-
return normalizePath(modulePath);
|
|
4279
|
-
}
|
|
4280
|
-
|
|
4281
|
-
const server = await createServer({
|
|
4282
|
-
root: projectRoot,
|
|
4283
|
-
mode: "production",
|
|
4284
|
-
server: { middlewareMode: true },
|
|
4285
|
-
appType: "custom",
|
|
4286
|
-
logLevel: "error",
|
|
4287
|
-
clearScreen: false,
|
|
4288
|
-
resolve: {
|
|
4289
|
-
conditions: ["module", "browser", "development|production"],
|
|
4290
|
-
},
|
|
4291
|
-
ssr: {
|
|
4292
|
-
noExternal: ["@fluid-app/portal-sdk"],
|
|
4293
|
-
resolve: {
|
|
4294
|
-
conditions: ["module", "node", "development|production"],
|
|
4295
|
-
},
|
|
4296
|
-
},
|
|
4297
4149
|
});
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
function collectSourceWidgetPackages(mod) {
|
|
4307
|
-
const candidates = [
|
|
4308
|
-
mod.widgetPackage,
|
|
4309
|
-
...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
|
|
4310
|
-
mod.default,
|
|
4311
|
-
];
|
|
4312
|
-
const byPackageId = new Map();
|
|
4313
|
-
for (const candidate of candidates) {
|
|
4314
|
-
if (!isSourceWidgetPackage(candidate)) continue;
|
|
4315
|
-
if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
|
|
4316
|
-
}
|
|
4317
|
-
return Array.from(byPackageId.values());
|
|
4318
|
-
}
|
|
4319
|
-
|
|
4320
|
-
function serializeWidget(widget) {
|
|
4321
|
-
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
|
|
4322
|
-
return widget;
|
|
4323
|
-
}
|
|
4324
|
-
const { component, ...metadata } = widget;
|
|
4325
|
-
return metadata;
|
|
4326
|
-
}
|
|
4327
|
-
|
|
4328
|
-
function serializePackage(sourcePackage) {
|
|
4329
|
-
return {
|
|
4330
|
-
manifestVersion: sourcePackage.manifestVersion,
|
|
4331
|
-
scope: sourcePackage.scope,
|
|
4332
|
-
packageStableId: sourcePackage.packageStableId,
|
|
4333
|
-
packageId: sourcePackage.packageId,
|
|
4334
|
-
packageType: sourcePackage.packageType,
|
|
4335
|
-
version: sourcePackage.version,
|
|
4336
|
-
cssUrls: sourcePackage.cssUrls,
|
|
4337
|
-
...(Object.prototype.hasOwnProperty.call(sourcePackage, "navigationOrigins")
|
|
4338
|
-
? { navigationOrigins: sourcePackage.navigationOrigins }
|
|
4339
|
-
: {}),
|
|
4340
|
-
widgets: Array.isArray(sourcePackage.widgets)
|
|
4341
|
-
? sourcePackage.widgets.map(serializeWidget)
|
|
4342
|
-
: sourcePackage.widgets,
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
|
-
|
|
4346
|
-
await fs.writeFile(
|
|
4347
|
-
outputPath,
|
|
4348
|
-
JSON.stringify({
|
|
4349
|
-
sentinel: OUTPUT_SENTINEL,
|
|
4350
|
-
data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
|
|
4351
|
-
}),
|
|
4352
|
-
"utf-8",
|
|
4353
|
-
);
|
|
4354
|
-
} finally {
|
|
4355
|
-
await server.close();
|
|
4356
|
-
}
|
|
4357
|
-
`;
|
|
4358
|
-
}
|
|
4359
|
-
function isRecord$3(value) {
|
|
4360
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4361
|
-
}
|
|
4362
|
-
//#endregion
|
|
4363
|
-
//#region ../widget-tooling/src/internal/widget-package-builder.ts
|
|
4364
|
-
const DEFAULT_PUBLISH_DIR = ".fluid/widget-dist";
|
|
4365
|
-
const DEFAULT_RUNTIME_VERSION = "1";
|
|
4366
|
-
const TEMP_BUILD_DIR = ".fluid/widget-build";
|
|
4367
|
-
const TEMP_CLI_DIR = ".fluid/tmp";
|
|
4368
|
-
const PROJECT_VITE_CONFIG_CANDIDATES = [
|
|
4369
|
-
"vite.config.ts",
|
|
4370
|
-
"vite.config.mts",
|
|
4371
|
-
"vite.config.js",
|
|
4372
|
-
"vite.config.mjs"
|
|
4373
|
-
];
|
|
4374
|
-
const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
|
|
4375
|
-
const require = createRequire(import.meta.url);
|
|
4376
|
-
function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
|
|
4377
|
-
const label = options.optionName ?? "publishDir";
|
|
4378
|
-
if (path.isAbsolute(publishDir)) throw new Error(`${label} must be relative to the project directory.`);
|
|
4379
|
-
const normalizedPublishDir = normalizePublishDirForValidation(publishDir);
|
|
4380
|
-
if (normalizedPublishDir === ".fluid" || !normalizedPublishDir.startsWith(".fluid/")) throw new Error(`${label} must be a relative path under .fluid/.`);
|
|
4381
|
-
if (isPathWithinReservedDir(normalizedPublishDir, TEMP_BUILD_DIR)) throw new Error(`${label} must not be ${TEMP_BUILD_DIR} because it is reserved for temporary builder output.`);
|
|
4382
|
-
if (isPathWithinReservedDir(normalizedPublishDir, TEMP_CLI_DIR)) throw new Error(`${label} must not be ${TEMP_CLI_DIR} because it is reserved for Fluid CLI temporary files.`);
|
|
4383
|
-
const resolvedProjectDir = path.resolve(projectDir);
|
|
4384
|
-
const resolvedPublishDir = path.resolve(resolvedProjectDir, publishDir);
|
|
4385
|
-
const relative = path.relative(resolvedProjectDir, resolvedPublishDir);
|
|
4386
|
-
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${label} must stay inside the project directory.`);
|
|
4387
|
-
}
|
|
4388
|
-
async function buildSharedWidgetPackage(options) {
|
|
4389
|
-
const publishDirInput = options.publishDir ?? DEFAULT_PUBLISH_DIR;
|
|
4390
|
-
try {
|
|
4391
|
-
await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
|
|
4392
|
-
} catch (err) {
|
|
4393
|
-
return failure({
|
|
4394
|
-
code: "INVALID_PUBLISH_DIR",
|
|
4395
|
-
message: "Unsafe widget package publish directory",
|
|
4396
|
-
details: (err instanceof Error ? err : new Error(String(err))).message
|
|
4397
|
-
});
|
|
4398
|
-
}
|
|
4399
|
-
const sourcePackageResult = await loadSourceWidgetPackages(options.projectDir);
|
|
4400
|
-
if (!sourcePackageResult.success) return failure({
|
|
4401
|
-
code: "CONFIG_LOAD_FAILED",
|
|
4402
|
-
message: sourcePackageResult.error.message,
|
|
4403
|
-
details: sourcePackageResult.error.details
|
|
4404
|
-
});
|
|
4405
|
-
const validation = validateSingleSourceWidgetPackage(sourcePackageResult.value, { owner: options.owner });
|
|
4406
|
-
if (!validation.success || !validation.value) return failure({
|
|
4407
|
-
code: "VALIDATION_FAILED",
|
|
4408
|
-
message: validation.errors.map((error) => error.message).join("\n")
|
|
4409
|
-
});
|
|
4410
|
-
try {
|
|
4411
|
-
await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
|
|
4412
|
-
const publishDir = path.resolve(options.projectDir, publishDirInput);
|
|
4413
|
-
const widgetScriptPath = path.join(publishDir, "widget.js");
|
|
4414
|
-
await fs.emptyDir(publishDir);
|
|
4415
|
-
const tempDir = path.resolve(options.projectDir, TEMP_BUILD_DIR);
|
|
4416
|
-
await fs.emptyDir(tempDir);
|
|
4417
|
-
let buildFailure;
|
|
4418
|
-
try {
|
|
4419
|
-
await buildWidgetScriptBundle({
|
|
4420
|
-
projectDir: options.projectDir,
|
|
4421
|
-
tempDir,
|
|
4422
|
-
publishDir,
|
|
4423
|
-
validated: validation.value
|
|
4424
|
-
});
|
|
4425
|
-
} catch (error) {
|
|
4426
|
-
buildFailure = { error };
|
|
4427
|
-
}
|
|
4428
|
-
try {
|
|
4429
|
-
await fs.remove(tempDir);
|
|
4430
|
-
} catch {
|
|
4431
|
-
const cleanupFailure = /* @__PURE__ */ new Error("Failed to remove reserved temporary build directory.");
|
|
4432
|
-
if (buildFailure) {
|
|
4433
|
-
const primaryError = buildFailure.error instanceof Error ? buildFailure.error : new Error(String(buildFailure.error));
|
|
4434
|
-
throw new AggregateError([primaryError, cleanupFailure], `${primaryError.message}\n${cleanupFailure.message}`);
|
|
4435
|
-
}
|
|
4436
|
-
throw cleanupFailure;
|
|
4437
|
-
}
|
|
4438
|
-
if (buildFailure) throw buildFailure.error;
|
|
4439
|
-
const cssUrls = await collectCssUrls(publishDir);
|
|
4440
|
-
const descriptor = buildWidgetPackagePublishManifest(validation.value, { cssUrls });
|
|
4441
|
-
const manifestContent = stableStringify(descriptor);
|
|
4442
|
-
const publishPackageId = descriptor.packageId;
|
|
4443
|
-
const manifestPath = path.join(publishDir, "manifest.json");
|
|
4444
|
-
const publishManifestPath = path.join(publishDir, "publish-manifest.json");
|
|
4445
|
-
await fs.writeFile(manifestPath, manifestContent, "utf-8");
|
|
4446
|
-
const artifacts = await collectWidgetPackageArtifacts(publishDir);
|
|
4447
|
-
const publishManifest = createPublishManifestMetadata({
|
|
4448
|
-
packageId: publishPackageId,
|
|
4449
|
-
version: validation.value.version,
|
|
4450
|
-
cliVersion: options.cliVersion,
|
|
4451
|
-
runtimeVersion: DEFAULT_RUNTIME_VERSION,
|
|
4452
|
-
manifestContent,
|
|
4453
|
-
artifacts
|
|
4454
|
-
});
|
|
4455
|
-
await fs.writeFile(publishManifestPath, stableStringify(publishManifest), "utf-8");
|
|
4456
|
-
return success({
|
|
4457
|
-
publishDir,
|
|
4458
|
-
packageId: validation.value.packageId,
|
|
4459
|
-
version: validation.value.version,
|
|
4460
|
-
manifestPath,
|
|
4461
|
-
publishManifestPath,
|
|
4462
|
-
widgetScriptPath
|
|
4463
|
-
});
|
|
4464
|
-
} catch (err) {
|
|
4465
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
4466
|
-
const isPublishDirValidationError = /^(publishDir|reserved temporary build directory) /.test(error.message);
|
|
4467
|
-
return failure({
|
|
4468
|
-
code: isPublishDirValidationError ? "INVALID_PUBLISH_DIR" : "WRITE_FAILED",
|
|
4469
|
-
message: isPublishDirValidationError ? "Unsafe widget package publish directory" : "Failed to build shared widget package artifacts",
|
|
4470
|
-
details: error.message
|
|
4471
|
-
});
|
|
4472
|
-
}
|
|
4473
|
-
}
|
|
4474
|
-
function createWidgetPackageEntrySource(options) {
|
|
4475
|
-
return `import { createElement } from "react";
|
|
4476
|
-
import { startWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
|
|
4477
|
-
${options.cssImportPath ? `import ${JSON.stringify(options.cssImportPath)};` : ""}
|
|
4478
|
-
|
|
4479
|
-
const widgetConfig = await import(${JSON.stringify(options.configImportPath)});
|
|
4480
|
-
const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
|
|
4481
|
-
const descriptors = ${JSON.stringify(options.validated.widgets, null, 2)};
|
|
4482
|
-
|
|
4483
|
-
function isSourceWidgetPackage(value) {
|
|
4484
|
-
return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
function collectSourceWidgetPackages(mod) {
|
|
4488
|
-
return [
|
|
4489
|
-
mod.widgetPackage,
|
|
4490
|
-
...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
|
|
4491
|
-
mod.default,
|
|
4492
|
-
].filter(isSourceWidgetPackage);
|
|
4493
|
-
}
|
|
4494
|
-
|
|
4495
|
-
const sourcePackage = collectSourceWidgetPackages(widgetConfig)[0];
|
|
4496
|
-
if (!sourcePackage) {
|
|
4497
|
-
throw new Error("No Fluid source widget package found while loading widget package worker bundle.");
|
|
4498
|
-
}
|
|
4499
|
-
|
|
4500
|
-
const componentsByName = new Map(
|
|
4501
|
-
sourcePackage.widgets.map((widget) => [widget.name, widget.component]),
|
|
4502
|
-
);
|
|
4503
|
-
|
|
4504
|
-
const widgets = descriptors.map((descriptor) => {
|
|
4505
|
-
const component = componentsByName.get(descriptor.name);
|
|
4506
|
-
if (typeof component !== "function") {
|
|
4507
|
-
throw new Error("Widget package is missing Remote DOM component for " + descriptor.name + ".");
|
|
4508
|
-
}
|
|
4509
|
-
return {
|
|
4510
|
-
...descriptor,
|
|
4511
|
-
component: (props) => createElement(component, props),
|
|
4512
|
-
};
|
|
4513
|
-
});
|
|
4514
|
-
|
|
4515
|
-
startWidgetPackage({ widgets });
|
|
4516
|
-
`;
|
|
4517
|
-
}
|
|
4518
|
-
async function buildWidgetScriptBundle(options) {
|
|
4519
|
-
const sourceConfig = await resolvePortalWidgetSourceConfig(options.projectDir);
|
|
4520
|
-
if (!sourceConfig) throw new Error("No portal widget source config found.");
|
|
4521
|
-
const entryPath = path.join(options.tempDir, "widget-entry.ts");
|
|
4522
|
-
const viteConfigPath = path.join(options.tempDir, "vite.config.mjs");
|
|
4523
|
-
const rootStylesPath = path.join(options.projectDir, "styles.css");
|
|
4524
|
-
await fs.writeFile(entryPath, createWidgetPackageEntrySource({
|
|
4525
|
-
configImportPath: pathToFileURL(sourceConfig.path).href,
|
|
4526
|
-
cssImportPath: await fs.pathExists(rootStylesPath) ? pathToFileURL(rootStylesPath).href : void 0,
|
|
4527
|
-
validated: options.validated
|
|
4528
|
-
}), "utf-8");
|
|
4529
|
-
await fs.writeFile(viteConfigPath, createViteConfigSource({
|
|
4530
|
-
entryPath,
|
|
4531
|
-
publishDir: options.publishDir,
|
|
4532
|
-
projectConfigPath: await resolveProjectViteConfigPath(options.projectDir)
|
|
4533
|
-
}), "utf-8");
|
|
4534
|
-
const viteCliPath = resolveViteCliPath(options.projectDir);
|
|
4535
|
-
await execa(process.execPath, [
|
|
4536
|
-
viteCliPath,
|
|
4537
|
-
"build",
|
|
4538
|
-
"--config",
|
|
4539
|
-
viteConfigPath
|
|
4540
|
-
], {
|
|
4541
|
-
cwd: options.projectDir,
|
|
4542
|
-
stdio: "pipe",
|
|
4543
|
-
env: {
|
|
4544
|
-
...process.env,
|
|
4545
|
-
NODE_ENV: "production"
|
|
4546
|
-
}
|
|
4547
|
-
});
|
|
4548
|
-
}
|
|
4549
|
-
function resolveViteCliPath(projectDir) {
|
|
4550
|
-
const projectViteCliPath = resolveViteCliPathFromRequire(createRequire(path.join(projectDir, "package.json")));
|
|
4551
|
-
if (projectViteCliPath) return projectViteCliPath;
|
|
4552
|
-
const cliViteCliPath = resolveViteCliPathFromRequire(require);
|
|
4553
|
-
if (cliViteCliPath) return cliViteCliPath;
|
|
4554
|
-
throw new Error("Unable to resolve Vite for the portal project. Install the project dependencies and ensure vite is available before building widget packages.");
|
|
4555
|
-
}
|
|
4556
|
-
async function resolveProjectViteConfigPath(projectDir) {
|
|
4557
|
-
for (const fileName of PROJECT_VITE_CONFIG_CANDIDATES) {
|
|
4558
|
-
const candidate = path.join(projectDir, fileName);
|
|
4559
|
-
if (await fs.pathExists(candidate)) return candidate;
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
function createViteConfigSource(options) {
|
|
4563
|
-
const widgetConfigSource = createWidgetViteConfigObjectSource({
|
|
4564
|
-
...options,
|
|
4565
|
-
disableConfigFileLookup: !options.projectConfigPath
|
|
4566
|
-
});
|
|
4567
|
-
if (!options.projectConfigPath) return `import { defineConfig } from "vite";
|
|
4568
|
-
|
|
4569
|
-
export default defineConfig(${widgetConfigSource});
|
|
4570
|
-
`;
|
|
4571
|
-
return `import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
|
|
4572
|
-
|
|
4573
|
-
const projectConfigPath = ${JSON.stringify(options.projectConfigPath)};
|
|
4574
|
-
const widgetConfig = ${widgetConfigSource};
|
|
4575
|
-
const fluidWidgetDefineInvariants = {
|
|
4576
|
-
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
4577
|
-
};
|
|
4578
|
-
const blockedFluidPluginNames = new Set(${JSON.stringify(BLOCKED_FLUID_PROJECT_PLUGIN_NAMES)});
|
|
4579
|
-
|
|
4580
|
-
function isAllowedProjectPlugin(plugin) {
|
|
4581
|
-
return !plugin || typeof plugin !== "object" || !blockedFluidPluginNames.has(plugin.name);
|
|
4582
|
-
}
|
|
4583
|
-
|
|
4584
|
-
function sanitizeProjectPlugins(pluginOption) {
|
|
4585
|
-
if (!Array.isArray(pluginOption)) {
|
|
4586
|
-
return isAllowedProjectPlugin(pluginOption) ? pluginOption : [];
|
|
4587
|
-
}
|
|
4588
|
-
|
|
4589
|
-
return pluginOption
|
|
4590
|
-
.map((plugin) => Array.isArray(plugin) ? sanitizeProjectPlugins(plugin) : plugin)
|
|
4591
|
-
.filter(isAllowedProjectPlugin);
|
|
4592
|
-
}
|
|
4593
|
-
|
|
4594
|
-
function copyProjectConfigValue(source, target, key) {
|
|
4595
|
-
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
4596
|
-
target[key] = source[key];
|
|
4597
|
-
}
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
function sanitizeProjectConfig(config) {
|
|
4601
|
-
if (!config || typeof config !== "object") return {};
|
|
4602
|
-
|
|
4603
|
-
const sanitized = {};
|
|
4604
|
-
if (Object.prototype.hasOwnProperty.call(config, "plugins")) {
|
|
4605
|
-
sanitized.plugins = sanitizeProjectPlugins(config.plugins);
|
|
4606
|
-
}
|
|
4607
|
-
copyProjectConfigValue(config, sanitized, "resolve");
|
|
4608
|
-
copyProjectConfigValue(config, sanitized, "define");
|
|
4609
|
-
copyProjectConfigValue(config, sanitized, "css");
|
|
4610
|
-
copyProjectConfigValue(config, sanitized, "esbuild");
|
|
4611
|
-
copyProjectConfigValue(config, sanitized, "json");
|
|
4612
|
-
copyProjectConfigValue(config, sanitized, "assetsInclude");
|
|
4613
|
-
copyProjectConfigValue(config, sanitized, "envDir");
|
|
4614
|
-
copyProjectConfigValue(config, sanitized, "envPrefix");
|
|
4615
|
-
return sanitized;
|
|
4616
|
-
}
|
|
4617
|
-
|
|
4618
|
-
function copyConfigDefineValue(value) {
|
|
4619
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
4620
|
-
? { ...value }
|
|
4621
|
-
: {};
|
|
4622
|
-
}
|
|
4623
|
-
|
|
4624
|
-
function mergeFluidWidgetDefineInvariants(mergedConfig, safeProjectConfig, widgetConfig) {
|
|
4625
|
-
mergedConfig.define = {
|
|
4626
|
-
...copyConfigDefineValue(safeProjectConfig.define),
|
|
4627
|
-
...copyConfigDefineValue(mergedConfig.define),
|
|
4628
|
-
...copyConfigDefineValue(widgetConfig.define),
|
|
4629
|
-
...fluidWidgetDefineInvariants,
|
|
4630
|
-
};
|
|
4631
|
-
return mergedConfig;
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
export default defineConfig(async (configEnv) => {
|
|
4635
|
-
const projectConfig = await loadConfigFromFile(configEnv, projectConfigPath);
|
|
4636
|
-
const safeProjectConfig = sanitizeProjectConfig(projectConfig?.config ?? {});
|
|
4637
|
-
return mergeFluidWidgetDefineInvariants(mergeConfig(safeProjectConfig, widgetConfig), safeProjectConfig, widgetConfig);
|
|
4638
|
-
});
|
|
4639
|
-
`;
|
|
4640
|
-
}
|
|
4641
|
-
function createWidgetViteConfigObjectSource(options) {
|
|
4642
|
-
const configFileSource = options.disableConfigFileLookup ? " configFile: false,\n" : "";
|
|
4643
|
-
return `(() => {
|
|
4644
|
-
const fluidWidgetDefineInvariants = {
|
|
4645
|
-
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
4646
|
-
};
|
|
4647
|
-
|
|
4648
|
-
function createFluidWidgetBuildInvariants() {
|
|
4649
|
-
return {
|
|
4650
|
-
copyPublicDir: false,
|
|
4651
|
-
emptyOutDir: false,
|
|
4652
|
-
outDir: ${JSON.stringify(options.publishDir)},
|
|
4653
|
-
sourcemap: false,
|
|
4654
|
-
target: "esnext",
|
|
4655
|
-
cssCodeSplit: false,
|
|
4656
|
-
lib: {
|
|
4657
|
-
entry: ${JSON.stringify(options.entryPath)},
|
|
4658
|
-
formats: ["es"],
|
|
4659
|
-
fileName: () => "widget.js",
|
|
4660
|
-
cssFileName: "widget",
|
|
4661
|
-
},
|
|
4662
|
-
rollupOptions: {
|
|
4663
|
-
output: {
|
|
4664
|
-
inlineDynamicImports: true,
|
|
4665
|
-
hoistTransitiveImports: false,
|
|
4666
|
-
},
|
|
4667
|
-
},
|
|
4668
|
-
};
|
|
4669
|
-
}
|
|
4670
|
-
|
|
4671
|
-
function fluidWidgetBuildInvariantsPlugin() {
|
|
4672
|
-
return {
|
|
4673
|
-
name: "fluid-widget-build-invariants",
|
|
4674
|
-
enforce: "post",
|
|
4675
|
-
config() {
|
|
4676
|
-
return { build: createFluidWidgetBuildInvariants() };
|
|
4677
|
-
},
|
|
4678
|
-
configResolved(config) {
|
|
4679
|
-
config.define = {
|
|
4680
|
-
...(config.define ?? {}),
|
|
4681
|
-
...fluidWidgetDefineInvariants,
|
|
4682
|
-
};
|
|
4683
|
-
Object.assign(config.build, createFluidWidgetBuildInvariants());
|
|
4684
|
-
},
|
|
4685
|
-
};
|
|
4686
|
-
}
|
|
4687
|
-
|
|
4688
|
-
return {
|
|
4689
|
-
${configFileSource} publicDir: false,
|
|
4690
|
-
define: fluidWidgetDefineInvariants,
|
|
4691
|
-
resolve: {
|
|
4692
|
-
conditions: [
|
|
4693
|
-
"module",
|
|
4694
|
-
"browser",
|
|
4695
|
-
"development|production",
|
|
4696
|
-
],
|
|
4697
|
-
},
|
|
4698
|
-
plugins: [fluidWidgetBuildInvariantsPlugin()],
|
|
4699
|
-
build: createFluidWidgetBuildInvariants(),
|
|
4700
|
-
};
|
|
4701
|
-
})()`;
|
|
4702
|
-
}
|
|
4703
|
-
async function validateWidgetPackageOutputPaths(projectDir, publishDir) {
|
|
4704
|
-
validateSharedWidgetPackagePublishDir(projectDir, publishDir);
|
|
4705
|
-
await rejectSymlinkedPathSegments({
|
|
4706
|
-
projectDir,
|
|
4707
|
-
relativePath: publishDir,
|
|
4708
|
-
label: "publishDir"
|
|
4709
|
-
});
|
|
4710
|
-
await rejectSymlinkedPathSegments({
|
|
4711
|
-
projectDir,
|
|
4712
|
-
relativePath: TEMP_BUILD_DIR,
|
|
4713
|
-
label: "reserved temporary build directory"
|
|
4714
|
-
});
|
|
4715
|
-
}
|
|
4716
|
-
async function collectCssUrls(publishDir) {
|
|
4717
|
-
return (await fs.readdir(publishDir, { withFileTypes: true })).filter((entry) => entry.isFile() && isWidgetPackageCssArtifactPath(entry.name)).map((entry) => entry.name).sort();
|
|
4718
|
-
}
|
|
4719
|
-
async function rejectSymlinkedPathSegments(options) {
|
|
4720
|
-
const resolvedProjectDir = path.resolve(options.projectDir);
|
|
4721
|
-
const resolvedTargetPath = path.resolve(resolvedProjectDir, options.relativePath);
|
|
4722
|
-
const segments = path.relative(resolvedProjectDir, resolvedTargetPath).split(path.sep).filter(Boolean);
|
|
4723
|
-
let currentPath = resolvedProjectDir;
|
|
4724
|
-
for (const segment of segments) {
|
|
4725
|
-
currentPath = path.join(currentPath, segment);
|
|
4726
|
-
let stat;
|
|
4727
|
-
try {
|
|
4728
|
-
stat = await fs.lstat(currentPath);
|
|
4729
|
-
} catch (err) {
|
|
4730
|
-
if (isNodeError(err) && err.code === "ENOENT") return;
|
|
4731
|
-
throw err;
|
|
4732
|
-
}
|
|
4733
|
-
if (stat.isSymbolicLink()) throw new Error(`${options.label} must not include symbolic links.`);
|
|
4734
|
-
}
|
|
4735
|
-
}
|
|
4736
|
-
function normalizePublishDirForValidation(value) {
|
|
4737
|
-
return toPosixPath(path.normalize(value)).replace(/\/+$/, "") || ".";
|
|
4738
|
-
}
|
|
4739
|
-
function isPathWithinReservedDir(value, reservedDir) {
|
|
4740
|
-
return value === reservedDir || value.startsWith(`${reservedDir}/`);
|
|
4741
|
-
}
|
|
4742
|
-
function toPosixPath(value) {
|
|
4743
|
-
return value.split(path.sep).join("/");
|
|
4744
|
-
}
|
|
4745
|
-
function stableStringify(value) {
|
|
4746
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
4747
|
-
}
|
|
4748
|
-
function resolveViteCliPathFromRequire(moduleRequire) {
|
|
4749
|
-
try {
|
|
4750
|
-
const packageJsonPath = moduleRequire.resolve("vite/package.json");
|
|
4751
|
-
const binPath = getViteBinPath(moduleRequire(packageJsonPath).bin);
|
|
4752
|
-
if (!binPath) return void 0;
|
|
4753
|
-
return path.resolve(path.dirname(packageJsonPath), binPath);
|
|
4754
|
-
} catch (err) {
|
|
4755
|
-
if (isModuleResolutionError(err)) return void 0;
|
|
4756
|
-
throw err;
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
function getViteBinPath(bin) {
|
|
4760
|
-
if (typeof bin === "string") return bin;
|
|
4761
|
-
if (!isRecord$2(bin)) return void 0;
|
|
4762
|
-
const viteBin = bin.vite;
|
|
4763
|
-
return typeof viteBin === "string" ? viteBin : void 0;
|
|
4764
|
-
}
|
|
4765
|
-
function isRecord$2(value) {
|
|
4766
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4767
|
-
}
|
|
4768
|
-
function isNodeError(value) {
|
|
4769
|
-
return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string";
|
|
4770
|
-
}
|
|
4771
|
-
function isModuleResolutionError(value) {
|
|
4772
|
-
return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
|
|
4773
|
-
}
|
|
4774
|
-
//#endregion
|
|
4775
|
-
//#region ../widget-tooling/src/internal/widget-package-publish-request.ts
|
|
4776
|
-
async function createWidgetPackagePublishRequest(options) {
|
|
4777
|
-
const manifestContent = await fs.readFile(options.manifestPath);
|
|
4778
|
-
const manifest = parseJsonRecord(manifestContent.toString("utf-8"), options.manifestPath);
|
|
4779
|
-
const publishManifest = await readPublishManifest(options.publishManifestPath);
|
|
4780
|
-
assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
|
|
4781
|
-
assertMetadataMatches({
|
|
4782
|
-
label: "manifestHash",
|
|
4783
|
-
path: "manifest.json",
|
|
4784
|
-
expected: publishManifest.manifestHash,
|
|
4785
|
-
actual: sha256(manifestContent)
|
|
4786
|
-
});
|
|
4787
|
-
const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
|
|
4788
|
-
assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
|
|
4789
|
-
const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
|
|
4790
|
-
return {
|
|
4791
|
-
version: publishManifest.version,
|
|
4792
|
-
packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
|
|
4793
|
-
builderVersion: publishManifest.builderVersion,
|
|
4794
|
-
cliVersion: publishManifest.cliVersion,
|
|
4795
|
-
runtimeVersion: publishManifest.runtimeVersion,
|
|
4796
|
-
manifestHash: publishManifest.manifestHash,
|
|
4797
|
-
manifest,
|
|
4798
|
-
artifacts
|
|
4799
|
-
};
|
|
4800
|
-
}
|
|
4801
|
-
async function readJsonRecord(filePath) {
|
|
4802
|
-
return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
|
|
4803
|
-
}
|
|
4804
|
-
function parseJsonRecord(content, filePath) {
|
|
4805
|
-
const value = JSON.parse(content);
|
|
4806
|
-
if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
|
|
4807
|
-
return value;
|
|
4808
|
-
}
|
|
4809
|
-
async function readPublishManifest(filePath) {
|
|
4810
|
-
const record = await readJsonRecord(filePath);
|
|
4811
|
-
const artifactsValue = record["artifacts"];
|
|
4812
|
-
if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
|
|
4813
|
-
return {
|
|
4814
|
-
packageId: readRequiredString(record, "packageId", filePath),
|
|
4815
|
-
version: readRequiredString(record, "version", filePath),
|
|
4816
|
-
builderVersion: readRequiredString(record, "builderVersion", filePath),
|
|
4817
|
-
cliVersion: readRequiredString(record, "cliVersion", filePath),
|
|
4818
|
-
runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
|
|
4819
|
-
manifestHash: readRequiredString(record, "manifestHash", filePath),
|
|
4820
|
-
artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
|
|
4821
|
-
};
|
|
4822
|
-
}
|
|
4823
|
-
function readArtifactMetadata(value, index) {
|
|
4824
|
-
if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
|
|
4825
|
-
return {
|
|
4826
|
-
path: readRequiredString(value, "path", "publish-manifest.json"),
|
|
4827
|
-
sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
|
|
4828
|
-
bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
|
|
4829
|
-
contentType: readRequiredString(value, "contentType", "publish-manifest.json")
|
|
4830
|
-
};
|
|
4831
|
-
}
|
|
4832
|
-
async function readUploadArtifacts(publishDir, artifacts) {
|
|
4833
|
-
const resolvedPublishDir = path.resolve(publishDir);
|
|
4834
|
-
return Promise.all(artifacts.map(async (artifact) => {
|
|
4835
|
-
const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
|
|
4836
|
-
const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
|
|
4837
|
-
assertArtifactMetadataMatches(artifact, actual);
|
|
4838
|
-
return {
|
|
4839
|
-
...actual,
|
|
4840
|
-
body: await fs.readFile(artifactPath)
|
|
4841
|
-
};
|
|
4842
|
-
}));
|
|
4843
|
-
}
|
|
4844
|
-
function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
|
|
4845
|
-
assertMetadataMatches({
|
|
4846
|
-
label: "packageId",
|
|
4847
|
-
path: "manifest.json",
|
|
4848
|
-
expected: publishManifest.packageId,
|
|
4849
|
-
actual: readRequiredString(manifest, "packageId", manifestPath)
|
|
4850
|
-
});
|
|
4851
|
-
assertMetadataMatches({
|
|
4852
|
-
label: "version",
|
|
4853
|
-
path: "manifest.json",
|
|
4854
|
-
expected: publishManifest.version,
|
|
4855
|
-
actual: readRequiredString(manifest, "version", manifestPath)
|
|
4856
|
-
});
|
|
4857
|
-
}
|
|
4858
|
-
function assertArtifactInventoryMatches(expected, actual) {
|
|
4859
|
-
const expectedByPath = /* @__PURE__ */ new Map();
|
|
4860
|
-
for (const artifact of expected) {
|
|
4861
|
-
if (expectedByPath.has(artifact.path)) throw new Error(`publish-manifest.json artifacts include duplicate path ${JSON.stringify(artifact.path)}. Rebuild the widget package before publishing.`);
|
|
4862
|
-
expectedByPath.set(artifact.path, artifact);
|
|
4863
|
-
}
|
|
4864
|
-
const actualPaths = /* @__PURE__ */ new Set();
|
|
4865
|
-
for (const actualArtifact of actual) {
|
|
4866
|
-
actualPaths.add(actualArtifact.path);
|
|
4867
|
-
const expectedArtifact = expectedByPath.get(actualArtifact.path);
|
|
4868
|
-
if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
|
|
4869
|
-
assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
|
|
4870
|
-
}
|
|
4871
|
-
for (const expectedArtifact of expected) {
|
|
4872
|
-
if (actualPaths.has(expectedArtifact.path)) continue;
|
|
4873
|
-
throw new Error(`publish-manifest.json artifacts include ${JSON.stringify(expectedArtifact.path)}, but that file is not present on disk. Rebuild the widget package before publishing.`);
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
|
-
function assertArtifactMetadataMatches(expected, actual) {
|
|
4877
|
-
assertMetadataMatches({
|
|
4878
|
-
label: "path",
|
|
4879
|
-
path: expected.path,
|
|
4880
|
-
expected: expected.path,
|
|
4881
|
-
actual: actual.path
|
|
4882
|
-
});
|
|
4883
|
-
assertMetadataMatches({
|
|
4884
|
-
label: "sha256",
|
|
4885
|
-
path: expected.path,
|
|
4886
|
-
expected: expected.sha256,
|
|
4887
|
-
actual: actual.sha256
|
|
4888
|
-
});
|
|
4889
|
-
assertMetadataMatches({
|
|
4890
|
-
label: "bytes",
|
|
4891
|
-
path: expected.path,
|
|
4892
|
-
expected: expected.bytes,
|
|
4893
|
-
actual: actual.bytes
|
|
4894
|
-
});
|
|
4895
|
-
assertMetadataMatches({
|
|
4896
|
-
label: "contentType",
|
|
4897
|
-
path: expected.path,
|
|
4898
|
-
expected: expected.contentType,
|
|
4899
|
-
actual: actual.contentType
|
|
4900
|
-
});
|
|
4901
|
-
}
|
|
4902
|
-
function assertMetadataMatches(options) {
|
|
4903
|
-
if (options.expected === options.actual) return;
|
|
4904
|
-
throw new Error(`publish-manifest.json ${options.label} for ${JSON.stringify(options.path)} does not match the file on disk. Recorded ${JSON.stringify(options.expected)}, computed ${JSON.stringify(options.actual)}. Rebuild the widget package before publishing.`);
|
|
4905
|
-
}
|
|
4906
|
-
function resolveArtifactPath(publishDir, artifactPath) {
|
|
4907
|
-
const resolved = path.resolve(publishDir, artifactPath);
|
|
4908
|
-
const relative = path.relative(publishDir, resolved);
|
|
4909
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
|
|
4910
|
-
return resolved;
|
|
4911
|
-
}
|
|
4912
|
-
function stripOwnerPrefix(packageId, owner) {
|
|
4913
|
-
const prefix = `${owner}.`;
|
|
4914
|
-
if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
|
|
4915
|
-
return packageId;
|
|
4916
|
-
}
|
|
4917
|
-
function readRequiredString(record, key, filePath) {
|
|
4918
|
-
const value = record[key];
|
|
4919
|
-
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
4920
|
-
throw new Error(`${path.basename(filePath)} must include string ${key}.`);
|
|
4921
|
-
}
|
|
4922
|
-
function readRequiredNumber(record, key, filePath) {
|
|
4923
|
-
const value = record[key];
|
|
4924
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4925
|
-
throw new Error(`${path.basename(filePath)} must include number ${key}.`);
|
|
4926
|
-
}
|
|
4927
|
-
function isRecord$1(value) {
|
|
4928
|
-
return typeof value === "object" && value !== null;
|
|
4929
|
-
}
|
|
4930
|
-
//#endregion
|
|
4931
|
-
//#region ../widget-tooling/src/internal/widget-package-upload.ts
|
|
4932
|
-
var WidgetPackageUploadError = class extends Error {
|
|
4933
|
-
code;
|
|
4934
|
-
constructor(code, message) {
|
|
4935
|
-
super(message);
|
|
4936
|
-
this.name = "WidgetPackageUploadError";
|
|
4937
|
-
this.code = code;
|
|
4938
|
-
}
|
|
4939
|
-
};
|
|
4940
|
-
function buildWidgetPackageArtifactPayload(artifact) {
|
|
4941
|
-
return {
|
|
4942
|
-
path: artifact.path,
|
|
4943
|
-
sha256: artifact.sha256,
|
|
4944
|
-
bytes: artifact.bytes,
|
|
4945
|
-
contentType: artifact.contentType
|
|
4946
|
-
};
|
|
4947
|
-
}
|
|
4948
|
-
function buildWidgetPackageUploadSessionPayload(request) {
|
|
4949
|
-
return {
|
|
4950
|
-
version: request.version,
|
|
4951
|
-
package_key: request.packageKey,
|
|
4952
|
-
builder_version: request.builderVersion,
|
|
4953
|
-
cli_version: request.cliVersion,
|
|
4954
|
-
runtime_version: request.runtimeVersion,
|
|
4955
|
-
manifest_hash: request.manifestHash,
|
|
4956
|
-
manifest: request.manifest,
|
|
4957
|
-
artifacts: request.artifacts.map(buildWidgetPackageArtifactPayload)
|
|
4958
|
-
};
|
|
4959
|
-
}
|
|
4960
|
-
function buildWidgetPackageCompleteUploadPayload(packageKey, artifacts) {
|
|
4961
|
-
return {
|
|
4962
|
-
package_key: packageKey,
|
|
4963
|
-
artifacts: artifacts.map(buildWidgetPackageArtifactPayload)
|
|
4964
|
-
};
|
|
4965
|
-
}
|
|
4966
|
-
function getWidgetPackageUploadSessionRoute(owner) {
|
|
4967
|
-
switch (owner.kind) {
|
|
4968
|
-
case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions`;
|
|
4969
|
-
case "company": return "/api/company/fluid_os/widget_package_versions";
|
|
4970
|
-
}
|
|
4971
|
-
}
|
|
4972
|
-
function getWidgetPackageCompleteUploadRoute(owner, version) {
|
|
4973
|
-
switch (owner.kind) {
|
|
4974
|
-
case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
|
|
4975
|
-
case "company": return `/api/company/fluid_os/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
|
|
4976
|
-
}
|
|
4977
|
-
}
|
|
4978
|
-
async function requestWidgetPackageUploadSession(client, owner, request) {
|
|
4979
|
-
const payload = buildWidgetPackageUploadSessionPayload(request);
|
|
4980
|
-
try {
|
|
4981
|
-
return normalizeWidgetPackageUploadSession(owner.kind === "company" ? await fluid_os_v0_create_widget_package_version(client, payload) : await client.post(getWidgetPackageUploadSessionRoute(owner), payload), request.version, request.artifacts);
|
|
4982
|
-
} catch (err) {
|
|
4983
|
-
if (err instanceof WidgetPackageUploadError) throw err;
|
|
4984
|
-
throw new WidgetPackageUploadError("SESSION_REQUEST_FAILED", `Failed to create widget package upload session: ${formatUnknownError(err)}`);
|
|
4985
|
-
}
|
|
4986
|
-
}
|
|
4987
|
-
async function uploadWidgetPackageArtifacts(session, artifacts, fetchImpl = fetch) {
|
|
4988
|
-
const uploadsByPath = new Map(session.uploads.map((upload) => [upload.path, upload]));
|
|
4989
|
-
const results = [];
|
|
4990
|
-
for (const artifact of artifacts) {
|
|
4991
|
-
const upload = uploadsByPath.get(artifact.path);
|
|
4992
|
-
if (!upload) throw new WidgetPackageUploadError("MISSING_SIGNED_URL", `Upload session did not include a signed URL for artifact "${artifact.path}".`);
|
|
4993
|
-
if (upload.contentType !== artifact.contentType) throw new WidgetPackageUploadError("CONTENT_TYPE_MISMATCH", `Upload session expected artifact "${artifact.path}" to use content type "${upload.contentType}", but local artifact uses "${artifact.contentType}".`);
|
|
4994
|
-
let response;
|
|
4995
|
-
try {
|
|
4996
|
-
response = await fetchImpl(upload.url, {
|
|
4997
|
-
method: "PUT",
|
|
4998
|
-
headers: mergeUploadHeaders(upload.headers, upload.contentType),
|
|
4999
|
-
body: artifact.body
|
|
5000
|
-
});
|
|
5001
|
-
} catch (err) {
|
|
5002
|
-
throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": ${formatUnknownError(err)}`);
|
|
5003
|
-
}
|
|
5004
|
-
if (!response.ok) {
|
|
5005
|
-
const body = await response.text().catch(() => "");
|
|
5006
|
-
const details = body.trim() ? `: ${body.trim().slice(0, 500)}` : "";
|
|
5007
|
-
throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": PUT returned HTTP ${response.status}${details}`);
|
|
5008
|
-
}
|
|
5009
|
-
results.push({
|
|
5010
|
-
path: artifact.path,
|
|
5011
|
-
status: response.status,
|
|
5012
|
-
contentType: upload.contentType
|
|
5013
|
-
});
|
|
5014
|
-
}
|
|
5015
|
-
return results;
|
|
5016
|
-
}
|
|
5017
|
-
async function completeWidgetPackageUpload(client, owner, version, packageKey, artifacts) {
|
|
5018
|
-
const payload = buildWidgetPackageCompleteUploadPayload(packageKey, artifacts);
|
|
4150
|
+
/**
|
|
4151
|
+
* The whole deploy, as one function returning a result.
|
|
4152
|
+
*
|
|
4153
|
+
* A result rather than a throw because two of its outcomes — a key that belongs
|
|
4154
|
+
* to another package, a version already published — are refusals with a fix to
|
|
4155
|
+
* show, not exceptions.
|
|
4156
|
+
*/
|
|
4157
|
+
async function runPortalDeploy(options) {
|
|
5019
4158
|
try {
|
|
5020
|
-
|
|
5021
|
-
} catch (
|
|
5022
|
-
if (err instanceof WidgetPackageUploadError) throw err;
|
|
5023
|
-
throw new WidgetPackageUploadError("COMPLETE_UPLOAD_FAILED", `Failed to complete widget package upload: ${formatUnknownError(err)}`);
|
|
5024
|
-
}
|
|
5025
|
-
}
|
|
5026
|
-
async function publishWidgetPackageVersion(options) {
|
|
5027
|
-
const sessionPayload = buildWidgetPackageUploadSessionPayload(options.request);
|
|
5028
|
-
if (options.dryRun) return {
|
|
5029
|
-
dryRun: true,
|
|
5030
|
-
sessionPayload
|
|
5031
|
-
};
|
|
5032
|
-
if (!options.client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
5033
|
-
const session = await requestWidgetPackageUploadSession(options.client, options.owner, options.request);
|
|
5034
|
-
return {
|
|
5035
|
-
dryRun: false,
|
|
5036
|
-
sessionPayload,
|
|
5037
|
-
session,
|
|
5038
|
-
uploadedArtifacts: await uploadWidgetPackageArtifacts(session, options.request.artifacts, options.fetchImpl),
|
|
5039
|
-
complete: await completeWidgetPackageUpload(options.client, options.owner, session.version, options.request.packageKey, options.request.artifacts)
|
|
5040
|
-
};
|
|
5041
|
-
}
|
|
5042
|
-
function normalizeWidgetPackageUploadSession(response, requestedVersion, requestedArtifacts) {
|
|
5043
|
-
const record = requireRecord(response, "INVALID_SESSION_RESPONSE", "Upload session response must be a JSON object.");
|
|
5044
|
-
requireFiniteNumberProperty(record, "version_id", "INVALID_SESSION_RESPONSE");
|
|
5045
|
-
requireStringProperty(record, "artifact_base_url", "INVALID_SESSION_RESPONSE");
|
|
5046
|
-
const uploadValues = record["uploads"];
|
|
5047
|
-
if (!Array.isArray(uploadValues)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", "Upload session response must contain an \"uploads\" array.");
|
|
5048
|
-
const requestedArtifactsByPath = new Map(requestedArtifacts.map((artifact) => [artifact.path, artifact]));
|
|
5049
|
-
const seenUploadPaths = /* @__PURE__ */ new Set();
|
|
5050
|
-
const uploads = uploadValues.map((value, index) => {
|
|
5051
|
-
const upload = requireRecord(value, "INVALID_SESSION_RESPONSE", `Upload session uploads[${index}] must be a JSON object.`);
|
|
5052
|
-
const path = requireStringProperty(upload, "path", "INVALID_SESSION_RESPONSE");
|
|
5053
|
-
const url = requireStringProperty(upload, "upload_url", "INVALID_SESSION_RESPONSE");
|
|
5054
|
-
requireStringProperty(upload, "public_url", "INVALID_SESSION_RESPONSE");
|
|
5055
|
-
const requestedArtifact = requestedArtifactsByPath.get(path);
|
|
5056
|
-
if (!requestedArtifact) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included unexpected artifact "${path}".`);
|
|
5057
|
-
if (seenUploadPaths.has(path)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included duplicate artifact "${path}".`);
|
|
5058
|
-
seenUploadPaths.add(path);
|
|
4159
|
+
await assertStandalonePortalRemoteActionAllowed(options.projectDir, "portal deploy");
|
|
4160
|
+
} catch (error) {
|
|
5059
4161
|
return {
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
headers: {}
|
|
4162
|
+
ok: false,
|
|
4163
|
+
code: "STANDALONE_PORTAL",
|
|
4164
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5064
4165
|
};
|
|
5065
|
-
});
|
|
5066
|
-
const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !seenUploadPaths.has(artifactPath));
|
|
5067
|
-
if (missingUploadPaths.length > 0) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response is missing signed URLs for artifact(s): ${missingUploadPaths.join(", ")}.`);
|
|
5068
|
-
return {
|
|
5069
|
-
version: requestedVersion,
|
|
5070
|
-
uploads,
|
|
5071
|
-
raw: response
|
|
5072
|
-
};
|
|
5073
|
-
}
|
|
5074
|
-
function normalizeWidgetPackageCompleteResponse(response, requestedVersion) {
|
|
5075
|
-
const record = requireRecord(response, "INVALID_COMPLETE_RESPONSE", "Complete upload response must be a JSON object.");
|
|
5076
|
-
const responseVersion = requireStringProperty(record, "version", "INVALID_COMPLETE_RESPONSE");
|
|
5077
|
-
if (responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
|
|
5078
|
-
const packageKey = requireStringProperty(record, "packageId", "INVALID_COMPLETE_RESPONSE");
|
|
5079
|
-
const status = requireStringProperty(record, "status", "INVALID_COMPLETE_RESPONSE");
|
|
5080
|
-
requireStringProperty(record, "scriptUrl", "INVALID_COMPLETE_RESPONSE");
|
|
5081
|
-
requireStringProperty(record, "manifestUrl", "INVALID_COMPLETE_RESPONSE");
|
|
5082
|
-
requireStringArrayProperty(record, "cssUrls", "INVALID_COMPLETE_RESPONSE");
|
|
5083
|
-
return {
|
|
5084
|
-
version: responseVersion,
|
|
5085
|
-
status,
|
|
5086
|
-
packageKey,
|
|
5087
|
-
raw: response
|
|
5088
|
-
};
|
|
5089
|
-
}
|
|
5090
|
-
function normalizeHeaderName(name) {
|
|
5091
|
-
return name.toLowerCase();
|
|
5092
|
-
}
|
|
5093
|
-
function mergeUploadHeaders(headers, contentType) {
|
|
5094
|
-
const merged = {};
|
|
5095
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
5096
|
-
if (normalizeHeaderName(key) === "content-type") continue;
|
|
5097
|
-
merged[key] = value;
|
|
5098
4166
|
}
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
function requireRecord(value, code, message) {
|
|
5103
|
-
if (!isRecord(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
|
|
5104
|
-
return value;
|
|
5105
|
-
}
|
|
5106
|
-
function isRecord(value) {
|
|
5107
|
-
return typeof value === "object" && value !== null;
|
|
5108
|
-
}
|
|
5109
|
-
function requireStringProperty(record, key, code) {
|
|
5110
|
-
const value = record[key];
|
|
5111
|
-
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
5112
|
-
throw new WidgetPackageUploadError(code, `Response field "${key}" must be a non-empty string.`);
|
|
5113
|
-
}
|
|
5114
|
-
function requireFiniteNumberProperty(record, key, code) {
|
|
5115
|
-
const value = record[key];
|
|
5116
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
5117
|
-
throw new WidgetPackageUploadError(code, `Response field "${key}" must be a finite number.`);
|
|
5118
|
-
}
|
|
5119
|
-
function requireStringArrayProperty(record, key, code) {
|
|
5120
|
-
const value = record[key];
|
|
5121
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim().length > 0)) return value;
|
|
5122
|
-
throw new WidgetPackageUploadError(code, `Response field "${key}" must be an array of non-empty strings.`);
|
|
5123
|
-
}
|
|
5124
|
-
function formatUnknownError(err) {
|
|
5125
|
-
const base = err instanceof Error ? err.message : String(err);
|
|
5126
|
-
if (isRecord(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
|
|
5127
|
-
return base;
|
|
5128
|
-
}
|
|
5129
|
-
function safeStringify(value) {
|
|
5130
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
5131
|
-
try {
|
|
5132
|
-
return JSON.stringify(value, (_key, item) => {
|
|
5133
|
-
if (typeof item === "bigint") return `${item}n`;
|
|
5134
|
-
if (typeof item !== "object" || item === null) return item;
|
|
5135
|
-
if (seen.has(item)) return "[Circular]";
|
|
5136
|
-
seen.add(item);
|
|
5137
|
-
return item;
|
|
5138
|
-
}) ?? String(value);
|
|
5139
|
-
} catch {
|
|
5140
|
-
return "[unserializable error data]";
|
|
5141
|
-
}
|
|
5142
|
-
}
|
|
5143
|
-
//#endregion
|
|
5144
|
-
//#region ../widget-tooling/src/workflows.ts
|
|
5145
|
-
async function buildWidgetPackage(options) {
|
|
5146
|
-
const result = await buildSharedWidgetPackage({
|
|
5147
|
-
projectDir: options.projectDir,
|
|
5148
|
-
publishDir: options.outDir,
|
|
5149
|
-
owner: options.owner,
|
|
5150
|
-
cliVersion: options.cliVersion
|
|
5151
|
-
});
|
|
5152
|
-
if (result.success) return result.value;
|
|
5153
|
-
throw new WidgetToolingError(result.error.code === "CONFIG_LOAD_FAILED" ? "CONFIG_LOAD_FAILED" : result.error.code === "VALIDATION_FAILED" ? "VALIDATION_FAILED" : result.error.code === "INVALID_PUBLISH_DIR" ? "INVALID_OUTPUT_DIR" : "BUILD_FAILED", result.error.message, { details: result.error.details });
|
|
5154
|
-
}
|
|
5155
|
-
async function publishWidgetPackage(options) {
|
|
5156
|
-
if (options.owner !== options.uploadOwner.kind) throw new WidgetToolingError("OWNER_MISMATCH", `Widget package build owner "${options.owner}" must match upload owner kind "${options.uploadOwner.kind}".`);
|
|
5157
|
-
if (!options.dryRun && !options.client) throw new WidgetToolingError("CLIENT_REQUIRED", "A FetchClient is required when dryRun is false.");
|
|
5158
|
-
const build = await buildWidgetPackage(options);
|
|
5159
|
-
let request;
|
|
5160
|
-
try {
|
|
5161
|
-
request = await createWidgetPackagePublishRequest({
|
|
5162
|
-
publishDir: build.publishDir,
|
|
5163
|
-
manifestPath: build.manifestPath,
|
|
5164
|
-
publishManifestPath: build.publishManifestPath,
|
|
5165
|
-
owner: options.owner
|
|
5166
|
-
});
|
|
4167
|
+
let client;
|
|
4168
|
+
if (!options.dryRun) try {
|
|
4169
|
+
client = createAuthenticatedWidgetPackageClient();
|
|
5167
4170
|
} catch (error) {
|
|
5168
|
-
throw new WidgetToolingError("ARTIFACT_VERIFICATION_FAILED", error instanceof Error ? error.message : "Failed to verify widget package artifacts.", { cause: error });
|
|
5169
|
-
}
|
|
5170
|
-
try {
|
|
5171
|
-
const publish = options.dryRun ? await publishWidgetPackageVersion({
|
|
5172
|
-
owner: options.uploadOwner,
|
|
5173
|
-
request,
|
|
5174
|
-
dryRun: true,
|
|
5175
|
-
fetchImpl: options.fetchImpl
|
|
5176
|
-
}) : await publishWidgetPackageVersion({
|
|
5177
|
-
client: options.client,
|
|
5178
|
-
owner: options.uploadOwner,
|
|
5179
|
-
request,
|
|
5180
|
-
fetchImpl: options.fetchImpl
|
|
5181
|
-
});
|
|
5182
4171
|
return {
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
4172
|
+
ok: false,
|
|
4173
|
+
code: "NOT_AUTHENTICATED",
|
|
4174
|
+
message: "Not signed in to Fluid, so the widgets were not published.",
|
|
4175
|
+
...error instanceof Error ? { hint: error.message } : {}
|
|
5186
4176
|
};
|
|
5187
|
-
} catch (error) {
|
|
5188
|
-
if (error instanceof WidgetToolingError) throw error;
|
|
5189
|
-
if (error instanceof WidgetPackageUploadError) throw new WidgetToolingError(toPublicUploadErrorCode(error.code), error.message, { cause: error });
|
|
5190
|
-
throw new WidgetToolingError("PUBLISH_FAILED", error instanceof Error ? error.message : "Failed to publish widget package.", { cause: error });
|
|
5191
|
-
}
|
|
5192
|
-
}
|
|
5193
|
-
function toPublicUploadErrorCode(code) {
|
|
5194
|
-
switch (code) {
|
|
5195
|
-
case "SESSION_REQUEST_FAILED":
|
|
5196
|
-
case "INVALID_SESSION_RESPONSE":
|
|
5197
|
-
case "MISSING_SIGNED_URL":
|
|
5198
|
-
case "CONTENT_TYPE_MISMATCH":
|
|
5199
|
-
case "ARTIFACT_UPLOAD_FAILED":
|
|
5200
|
-
case "COMPLETE_UPLOAD_FAILED":
|
|
5201
|
-
case "INVALID_COMPLETE_RESPONSE": return code;
|
|
5202
|
-
default: return "PUBLISH_FAILED";
|
|
5203
4177
|
}
|
|
5204
|
-
}
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
const isDryRun = options.dryRun === true;
|
|
5209
|
-
const client = options.client;
|
|
5210
|
-
if (!isDryRun && !client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
5211
|
-
const cliVersion = await getPortalCliVersion();
|
|
5212
|
-
try {
|
|
5213
|
-
if (isDryRun) return await publishWidgetPackage({
|
|
5214
|
-
projectDir: options.projectDir,
|
|
5215
|
-
outDir: options.outDir,
|
|
5216
|
-
owner: options.buildOwner,
|
|
5217
|
-
uploadOwner: options.uploadOwner,
|
|
5218
|
-
cliVersion,
|
|
5219
|
-
dryRun: true
|
|
5220
|
-
});
|
|
5221
|
-
if (!client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
5222
|
-
return await publishWidgetPackage({
|
|
4178
|
+
let preflight = {};
|
|
4179
|
+
if (client) {
|
|
4180
|
+
options.onProgress?.("Checking this company's widget package...");
|
|
4181
|
+
preflight = await preflightCompanyPackage({
|
|
5223
4182
|
projectDir: options.projectDir,
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
4183
|
+
client,
|
|
4184
|
+
readConfiguredPackage: async (dir) => {
|
|
4185
|
+
const { validated } = await loadAndValidateWidgetPackage({
|
|
4186
|
+
projectDir: dir,
|
|
4187
|
+
owner: "company"
|
|
4188
|
+
});
|
|
4189
|
+
return {
|
|
4190
|
+
packageId: validated.packageId,
|
|
4191
|
+
version: validated.version
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
5229
4194
|
});
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
if (!(error instanceof Error)) throw error;
|
|
5236
|
-
if (!("details" in error) || typeof error.details !== "string" || error.details.length === 0) throw error;
|
|
5237
|
-
throw new Error(`${error.message}\n${error.details}`, { cause: error });
|
|
5238
|
-
}
|
|
5239
|
-
function createAuthenticatedWidgetPackageClient() {
|
|
5240
|
-
const { profile, profileName, token } = resolveAuthenticatedProfile();
|
|
5241
|
-
if (!token) {
|
|
5242
|
-
if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
5243
|
-
throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
5244
|
-
}
|
|
5245
|
-
return createFetchClient({
|
|
5246
|
-
baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
|
|
5247
|
-
getAuthToken: () => token
|
|
5248
|
-
});
|
|
5249
|
-
}
|
|
5250
|
-
function resolveAuthenticatedProfile() {
|
|
5251
|
-
const config = readConfig();
|
|
5252
|
-
const projectConfig = findProjectConfig(process.cwd());
|
|
5253
|
-
if (projectConfig?.profile) {
|
|
5254
|
-
const profile = config.profiles[projectConfig.profile];
|
|
5255
|
-
if (profile) return {
|
|
5256
|
-
profile,
|
|
5257
|
-
profileName: projectConfig.profile,
|
|
5258
|
-
token: profile.token
|
|
4195
|
+
if (preflight.mismatch) return {
|
|
4196
|
+
ok: false,
|
|
4197
|
+
code: "PACKAGE_KEY_MISMATCH",
|
|
4198
|
+
message: "This project publishes under a different package key.",
|
|
4199
|
+
hint: preflight.mismatch
|
|
5259
4200
|
};
|
|
5260
4201
|
}
|
|
5261
|
-
|
|
5262
|
-
const profile = config.profiles[config.activeProfile];
|
|
5263
|
-
if (!profile) return {};
|
|
5264
|
-
return {
|
|
5265
|
-
profile,
|
|
5266
|
-
profileName: config.activeProfile,
|
|
5267
|
-
token: profile.token
|
|
5268
|
-
};
|
|
5269
|
-
}
|
|
5270
|
-
function printWidgetPublishSummary(options) {
|
|
5271
|
-
console.log();
|
|
5272
|
-
console.log(chalk.green.bold(`${options.title} complete`));
|
|
5273
|
-
console.log();
|
|
5274
|
-
console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
|
|
5275
|
-
if (options.environment) console.log(chalk.gray("Environment: ") + chalk.white(options.environment));
|
|
5276
|
-
console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
|
|
5277
|
-
console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
|
|
5278
|
-
console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
|
|
5279
|
-
console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
|
|
5280
|
-
console.log();
|
|
5281
|
-
}
|
|
5282
|
-
function printRuntimeOnlyNotice() {
|
|
5283
|
-
console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
|
|
5284
|
-
console.log();
|
|
5285
|
-
}
|
|
5286
|
-
function printDryRunSessionPayload(result) {
|
|
5287
|
-
console.log(chalk.bold("Dry-run upload session payload:"));
|
|
5288
|
-
console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
|
|
5289
|
-
console.log();
|
|
5290
|
-
}
|
|
5291
|
-
//#endregion
|
|
5292
|
-
//#region src/commands/deploy.ts
|
|
5293
|
-
const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
|
|
5294
|
-
const DEFAULT_ENVIRONMENT = "production";
|
|
5295
|
-
const deployCommand = new Command("deploy").description("Publish company-owned portal widget runtime artifacts").option("-e, --environment <name>", "Target environment label for output reporting", DEFAULT_ENVIRONMENT).option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR$1).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
|
|
5296
|
-
const environment = options.environment ?? DEFAULT_ENVIRONMENT;
|
|
5297
|
-
const outDir = options.outDir ?? DEFAULT_OUT_DIR$1;
|
|
5298
|
-
const dryRun = options.dryRun === true;
|
|
5299
|
-
console.log();
|
|
5300
|
-
console.log(chalk.blue.bold("Fluid Portal Deploy"));
|
|
5301
|
-
console.log();
|
|
5302
|
-
printRuntimeOnlyNotice();
|
|
5303
|
-
console.log(chalk.gray("Environment: ") + chalk.white(environment));
|
|
5304
|
-
console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
|
|
5305
|
-
if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
|
|
5306
|
-
console.log();
|
|
5307
|
-
const spinner = ora("Building company-owned widget package...").start();
|
|
4202
|
+
options.onProgress?.("Building company-owned widget package...");
|
|
5308
4203
|
try {
|
|
5309
4204
|
const result = await publishWidgetRuntimeArtifacts({
|
|
5310
|
-
projectDir:
|
|
5311
|
-
outDir,
|
|
4205
|
+
projectDir: options.projectDir,
|
|
4206
|
+
outDir: options.outDir,
|
|
5312
4207
|
buildOwner: "company",
|
|
5313
4208
|
uploadOwner: { kind: "company" },
|
|
5314
|
-
dryRun,
|
|
5315
|
-
...
|
|
5316
|
-
|
|
5317
|
-
spinner.succeed("Built widget runtime artifacts");
|
|
5318
|
-
if (dryRun) {
|
|
5319
|
-
console.log(chalk.yellow("Dry run complete — upload session was not requested."));
|
|
5320
|
-
console.log();
|
|
5321
|
-
printDryRunSessionPayload(result);
|
|
5322
|
-
} else console.log(chalk.green("Published widget package version."));
|
|
5323
|
-
printWidgetPublishSummary({
|
|
5324
|
-
title: dryRun ? "Portal deploy dry run" : "Portal deploy",
|
|
5325
|
-
ownerLabel: "company",
|
|
5326
|
-
environment,
|
|
5327
|
-
outDir,
|
|
5328
|
-
result
|
|
4209
|
+
dryRun: options.dryRun,
|
|
4210
|
+
...preflight.versionOverride ? { version: preflight.versionOverride } : {},
|
|
4211
|
+
...client ? { client } : {}
|
|
5329
4212
|
});
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
4213
|
+
return {
|
|
4214
|
+
ok: true,
|
|
4215
|
+
packageId: result.build.packageId,
|
|
4216
|
+
version: result.build.version,
|
|
4217
|
+
artifactCount: result.request.artifacts.length,
|
|
4218
|
+
...preflight.versionOverride && preflight.configuredVersion ? { requestedVersion: preflight.configuredVersion } : {},
|
|
4219
|
+
sessionPayload: result.publish.sessionPayload
|
|
4220
|
+
};
|
|
4221
|
+
} catch (error) {
|
|
4222
|
+
return {
|
|
4223
|
+
ok: false,
|
|
4224
|
+
code: error instanceof Error && "code" in error ? String(error.code) : "PUBLISH_FAILED",
|
|
4225
|
+
message: error instanceof Error ? error.message : "Publishing the widgets failed.",
|
|
4226
|
+
...error instanceof Error && "details" in error && typeof error.details === "string" && error.details.length > 0 ? { hint: error.details } : {}
|
|
4227
|
+
};
|
|
5334
4228
|
}
|
|
5335
|
-
}
|
|
4229
|
+
}
|
|
5336
4230
|
//#endregion
|
|
5337
4231
|
//#region src/commands/doctor.ts
|
|
5338
4232
|
/** Files that are managed by the SDK and should match the canonical template. */
|
|
@@ -5507,9 +4401,9 @@ function requireToken() {
|
|
|
5507
4401
|
return token;
|
|
5508
4402
|
}
|
|
5509
4403
|
function createClient(token) {
|
|
5510
|
-
return
|
|
4404
|
+
return createFluidOsClient({
|
|
5511
4405
|
baseUrl: getApiBase(),
|
|
5512
|
-
|
|
4406
|
+
token
|
|
5513
4407
|
});
|
|
5514
4408
|
}
|
|
5515
4409
|
async function requireDefinitionId() {
|
|
@@ -5522,13 +4416,14 @@ async function requireDefinitionId() {
|
|
|
5522
4416
|
return mappings.definition.id;
|
|
5523
4417
|
}
|
|
5524
4418
|
const createVersionCommand = new Command("create").description("Create a new version (snapshot) of the portal definition").option("--activate", "Activate the version immediately after creation").action(async (options) => {
|
|
4419
|
+
await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal version create");
|
|
5525
4420
|
const client = createClient(requireToken());
|
|
5526
4421
|
const definitionId = await requireDefinitionId();
|
|
5527
4422
|
console.log();
|
|
5528
4423
|
console.log(chalk.bold("Creating version..."));
|
|
5529
4424
|
let result;
|
|
5530
4425
|
try {
|
|
5531
|
-
result = await
|
|
4426
|
+
result = await fluidOsApi.createVersion(client, definitionId);
|
|
5532
4427
|
} catch (err) {
|
|
5533
4428
|
console.error(chalk.red("Error:") + " Failed to create version — " + (err instanceof Error ? err.message : String(err)));
|
|
5534
4429
|
process.exit(1);
|
|
@@ -5546,7 +4441,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
|
|
|
5546
4441
|
if (options.activate) {
|
|
5547
4442
|
console.log("Activating version...");
|
|
5548
4443
|
try {
|
|
5549
|
-
await
|
|
4444
|
+
await fluidOsApi.updateVersion(client, definitionId, version.id, { version: { active: true } });
|
|
5550
4445
|
} catch (err) {
|
|
5551
4446
|
console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
|
|
5552
4447
|
process.exit(1);
|
|
@@ -5561,7 +4456,7 @@ const listVersionCommand = new Command("list").description("List all versions of
|
|
|
5561
4456
|
const definitionId = await requireDefinitionId();
|
|
5562
4457
|
let result;
|
|
5563
4458
|
try {
|
|
5564
|
-
result = await
|
|
4459
|
+
result = await fluidOsApi.listVersions(client, definitionId);
|
|
5565
4460
|
} catch (err) {
|
|
5566
4461
|
console.error(chalk.red("Error:") + " Failed to list versions — " + (err instanceof Error ? err.message : String(err)));
|
|
5567
4462
|
process.exit(1);
|
|
@@ -5587,6 +4482,12 @@ const listVersionCommand = new Command("list").description("List all versions of
|
|
|
5587
4482
|
console.log();
|
|
5588
4483
|
});
|
|
5589
4484
|
const activateVersionCommand = new Command("activate").description("Activate a specific version, making it the live version").argument("<version-id>", "The version ID to activate").option("-y, --yes", "Skip confirmation prompt").action(async (versionId, options) => {
|
|
4485
|
+
await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal version activate");
|
|
4486
|
+
const numericVersionId = Number(versionId);
|
|
4487
|
+
if (!Number.isSafeInteger(numericVersionId) || numericVersionId < 1) {
|
|
4488
|
+
console.error(chalk.red("Error:") + " Version ID must be a positive integer.");
|
|
4489
|
+
process.exit(1);
|
|
4490
|
+
}
|
|
5590
4491
|
const client = createClient(requireToken());
|
|
5591
4492
|
const definitionId = await requireDefinitionId();
|
|
5592
4493
|
if (!options.yes) {
|
|
@@ -5604,7 +4505,7 @@ const activateVersionCommand = new Command("activate").description("Activate a s
|
|
|
5604
4505
|
console.log();
|
|
5605
4506
|
console.log(chalk.bold("Activating version..."));
|
|
5606
4507
|
try {
|
|
5607
|
-
await
|
|
4508
|
+
await fluidOsApi.updateVersion(client, definitionId, numericVersionId, { version: { active: true } });
|
|
5608
4509
|
} catch (err) {
|
|
5609
4510
|
console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
|
|
5610
4511
|
process.exit(1);
|
|
@@ -5618,6 +4519,7 @@ const versionCommand = new Command("version").description("Manage portal definit
|
|
|
5618
4519
|
//#region src/commands/widget-publish.ts
|
|
5619
4520
|
const DEFAULT_OUT_DIR = ".fluid/widget-dist";
|
|
5620
4521
|
const publishSubcommand = new Command("publish").description("Publish droplet-owned widget runtime artifacts").requiredOption("--droplet <uuid>", "Droplet UUID that owns the widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
|
|
4522
|
+
await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal widget publish");
|
|
5621
4523
|
const outDir = options.outDir ?? DEFAULT_OUT_DIR;
|
|
5622
4524
|
const dryRun = options.dryRun === true;
|
|
5623
4525
|
console.log();
|
|
@@ -5645,17 +4547,19 @@ const publishSubcommand = new Command("publish").description("Publish droplet-ow
|
|
|
5645
4547
|
if (dryRun) {
|
|
5646
4548
|
console.log(chalk.yellow("Dry run complete — upload session was not requested."));
|
|
5647
4549
|
console.log();
|
|
5648
|
-
printDryRunSessionPayload(result);
|
|
4550
|
+
printDryRunSessionPayload(result.publish.sessionPayload);
|
|
5649
4551
|
} else console.log(chalk.green("Published widget package version."));
|
|
5650
4552
|
printWidgetPublishSummary({
|
|
5651
4553
|
title: dryRun ? "Widget publish dry run" : "Widget publish",
|
|
5652
4554
|
ownerLabel: `droplet ${options.droplet}`,
|
|
5653
4555
|
outDir,
|
|
5654
|
-
result
|
|
4556
|
+
packageId: result.build.packageId,
|
|
4557
|
+
version: result.build.version,
|
|
4558
|
+
artifactCount: result.request.artifacts.length
|
|
5655
4559
|
});
|
|
5656
4560
|
} catch (err) {
|
|
5657
4561
|
spinner.fail("Widget publish failed");
|
|
5658
|
-
|
|
4562
|
+
printWidgetPublishError(err);
|
|
5659
4563
|
process.exit(1);
|
|
5660
4564
|
}
|
|
5661
4565
|
});
|
|
@@ -5687,6 +4591,6 @@ const plugin = {
|
|
|
5687
4591
|
}
|
|
5688
4592
|
};
|
|
5689
4593
|
//#endregion
|
|
5690
|
-
export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, cloneCommand, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPortalProject, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, pathExists, portalGitCredentialPath, promptProjectConfig, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
|
|
4594
|
+
export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, cloneCommand, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPortalProject, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, formatPackageKeyMismatch, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, pathExists, portalGitCredentialPath, preflightCompanyPackage, promptProjectConfig, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readCompanyWidgetPackage, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolvePublishVersion, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
|
|
5691
4595
|
|
|
5692
4596
|
//# sourceMappingURL=index.mjs.map
|