@fluid-app/fluid-cli-portal 0.1.27 → 0.1.29

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/index.mjs CHANGED
@@ -1,17 +1,19 @@
1
- import { A as deleteFluidOSNavigation, B as updateFluidOSScreen, C as writeMappings, D as createFluidOSScreen, E as createFluidOSProfile, F as listFluidOSNavigationItems, H as updateFluidOSVersion, I as listFluidOSVersions, L as updateFluidOSNavigation, M as deleteFluidOSProfile, N as deleteFluidOSScreen, O as createFluidOSTheme, P as deleteFluidOSTheme, R as updateFluidOSNavigationItem, S as updateMapping, T as createFluidOSNavigationItem, U as createFetchClient, V as updateFluidOSTheme, _ 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 deleteFluidOSNavigationItem, k as createFluidOSVersion, 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 createFluidOSNavigation, x as resolveSlugToId, y as removeMapping, z as updateFluidOSProfile } from "./pull-hAdXOpgb.mjs";
1
+ import { A as fluid_os_v0_create_fluid_osversion, B as fluid_os_v0_update_fluid_osnavigation_item, C as writeMappings, D as fluid_os_v0_create_fluid_osprofile, E as fluid_os_v0_create_fluid_osnavigation_item, F as fluid_os_v0_delete_fluid_osscreen, G as createFetchClient, H as fluid_os_v0_update_fluid_osscreen, I as fluid_os_v0_delete_fluid_ostheme, L as fluid_os_v0_list_fluid_osnavigation_items, M as fluid_os_v0_delete_fluid_osnavigation, N as fluid_os_v0_delete_fluid_osnavigation_item, O as fluid_os_v0_create_fluid_osscreen, P as fluid_os_v0_delete_fluid_osprofile, R as fluid_os_v0_list_fluid_osversions, S as updateMapping, T as fluid_os_v0_create_fluid_osnavigation, U as fluid_os_v0_update_fluid_ostheme, V as fluid_os_v0_update_fluid_osprofile, W as fluid_os_v0_update_fluid_osversion, _ 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 fluid_os_v0_create_widget_package_version, k as fluid_os_v0_create_fluid_ostheme, 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 fluid_os_v0_complete_widget_package_version_upload, x as resolveSlugToId, y as removeMapping, z as fluid_os_v0_update_fluid_osnavigation } from "./pull-zrTaJuSb.mjs";
2
+ import { createRequire } from "node:module";
2
3
  import { Command } from "commander";
3
4
  import chalk from "chalk";
4
5
  import ora from "ora";
5
6
  import path, { basename, dirname, join, relative, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
8
  import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
- import { failure, getActiveProfile, getAuthToken, listProfileNames, success } from "@fluid-app/fluid-cli";
9
+ import { failure, findProjectConfig, getActiveProfile, getAuthToken, listProfileNames, readConfig, success } from "@fluid-app/fluid-cli";
9
10
  import prompts from "prompts";
10
11
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
12
  import Handlebars from "handlebars";
12
13
  import { execa } from "execa";
13
14
  import fs from "fs-extra";
14
15
  import path$1 from "path";
16
+ import { createHash } from "node:crypto";
15
17
  //#region src/types.ts
16
18
  /**
17
19
  * Available project templates
@@ -239,16 +241,14 @@ async function getSdkVersion() {
239
241
  }
240
242
  }
241
243
  /**
242
- * Reads the portal-core version from the workspace package.json.
243
- * Falls back to ^0.1.0 if not found.
244
+ * Builds a pnpm link: specifier for local scaffolds.
245
+ *
246
+ * link: keeps the dependency pointed at the workspace package source instead of
247
+ * packing only package.json "files" entries like file: does. That lets Vite's
248
+ * development export resolve the SDK source and its workspace dependencies.
244
249
  */
245
- async function getCoreVersion() {
246
- try {
247
- const content = await readFile(join(join(findPackageRoot(), "..", ".."), "portal", "core", "package.json"), "utf-8");
248
- return `^${JSON.parse(content).version ?? "0.1.0"}`;
249
- } catch {
250
- return "^0.1.0";
251
- }
250
+ function getLocalPackageLinkVersion(targetPath, packagePath) {
251
+ return `link:${(relative(targetPath, packagePath) || ".").replace(/\\/g, "/")}`;
252
252
  }
253
253
  /**
254
254
  * Reads the CLI core version from the workspace package.json.
@@ -397,7 +397,7 @@ const createCommand = new Command("create").description("Create a new Fluid port
397
397
  process.exit(1);
398
398
  }
399
399
  let sdkVersion;
400
- let coreVersion;
400
+ let localCoreVersion;
401
401
  const cliVersion = await getCliVersion();
402
402
  if (!!options.local) {
403
403
  const packagesRoot = join(join(dirname(fileURLToPath(import.meta.url)), "..", ".."), "..", "..");
@@ -407,13 +407,10 @@ const createCommand = new Command("create").description("Create a new Fluid port
407
407
  console.error(chalk.red("Error: --local requires running from within the fluid-mono monorepo\n Could not find packages/portal/sdk or packages/portal/core"));
408
408
  process.exit(1);
409
409
  }
410
- sdkVersion = `file:${relative(targetPath, sdkPath)}`;
411
- coreVersion = `file:${relative(targetPath, corePath)}`;
410
+ sdkVersion = getLocalPackageLinkVersion(targetPath, sdkPath);
411
+ localCoreVersion = getLocalPackageLinkVersion(targetPath, corePath);
412
412
  console.log(chalk.cyan(" Using local packages (--local mode)"));
413
- } else {
414
- sdkVersion = await getSdkVersion();
415
- coreVersion = await getCoreVersion();
416
- }
413
+ } else sdkVersion = await getSdkVersion();
417
414
  const spinner = ora("Creating project directory...").start();
418
415
  try {
419
416
  await createDirectory(targetPath);
@@ -425,8 +422,8 @@ const createCommand = new Command("create").description("Create a new Fluid port
425
422
  const templateVariables = {
426
423
  projectName: config.name,
427
424
  sdkVersion,
425
+ localCoreVersion,
428
426
  cliVersion,
429
- coreVersion,
430
427
  selectedPages: config.selectedPages,
431
428
  hasSelectedPages: config.selectedPages.length > 0,
432
429
  profileName: config.profileName
@@ -510,7 +507,7 @@ async function autoPull(cwd) {
510
507
  console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
511
508
  console.log();
512
509
  try {
513
- const { pullCommand } = await import("./pull-hAdXOpgb.mjs").then((n) => n.n);
510
+ const { pullCommand } = await import("./pull-zrTaJuSb.mjs").then((n) => n.n);
514
511
  await pullCommand.parseAsync([], { from: "user" });
515
512
  return hasPortalContent(cwd);
516
513
  } catch (err) {
@@ -565,83 +562,417 @@ const devCommand = new Command("dev").description("Start the development server
565
562
  }
566
563
  });
567
564
  //#endregion
565
+ //#region src/utils/widget-package-config.ts
566
+ const CONFIG_CANDIDATES = [
567
+ "src/widgets.config.ts",
568
+ "src/portal.config.ts",
569
+ "portal.config.ts"
570
+ ];
571
+ const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
572
+ const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
573
+ const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
574
+ const TSX_CLI_PATH$1 = createRequire(import.meta.url).resolve("tsx/cli");
575
+ async function resolvePortalWidgetSourceConfig(projectDir) {
576
+ for (const relativePath of CONFIG_CANDIDATES) {
577
+ const candidate = path.join(projectDir, relativePath);
578
+ if (await fs.pathExists(candidate)) return {
579
+ path: candidate,
580
+ relativePath
581
+ };
582
+ }
583
+ }
584
+ async function loadSourceWidgetPackages(projectDir) {
585
+ const config = await resolvePortalWidgetSourceConfig(projectDir);
586
+ if (!config) return success([]);
587
+ let tempDir;
588
+ try {
589
+ tempDir = await createTempDirectory$1(projectDir);
590
+ const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
591
+ const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
592
+ await fs.writeFile(extractFile, createSourcePackageExtractorScript({
593
+ projectDir,
594
+ configPath: config.path
595
+ }), {
596
+ encoding: "utf-8",
597
+ flag: "wx"
598
+ });
599
+ await execa(process.execPath, [
600
+ TSX_CLI_PATH$1,
601
+ extractFile,
602
+ outputFile
603
+ ], {
604
+ cwd: projectDir,
605
+ stdio: "pipe",
606
+ env: {
607
+ ...process.env,
608
+ NODE_ENV: "production"
609
+ }
610
+ });
611
+ const outputResult = await readSourcePackageExtractorOutput(outputFile);
612
+ if (!outputResult.success) return outputResult;
613
+ const parsed = outputResult.value;
614
+ if (parsed.length === 0) return success([]);
615
+ return success(parsed);
616
+ } catch (err) {
617
+ const error = err;
618
+ return failure({
619
+ code: "CONFIG_LOAD_FAILED",
620
+ message: `Failed to load widget packages from ${config.relativePath}`,
621
+ details: error.stderr ?? error.message ?? String(err)
622
+ });
623
+ } finally {
624
+ if (tempDir) await fs.remove(tempDir).catch(() => {});
625
+ }
626
+ }
627
+ async function createTempDirectory$1(projectDir) {
628
+ const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
629
+ try {
630
+ await fs.ensureDir(projectTmpDir);
631
+ return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
632
+ } catch (err) {
633
+ const message = err instanceof Error ? err.message : String(err);
634
+ throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
635
+ }
636
+ }
637
+ async function readSourcePackageExtractorOutput(outputFile) {
638
+ let output;
639
+ try {
640
+ output = await fs.readFile(outputFile, "utf-8");
641
+ } catch (err) {
642
+ return failure({
643
+ code: "CONFIG_LOAD_FAILED",
644
+ message: "Widget package extractor did not write an output file",
645
+ details: err instanceof Error ? err.message : String(err)
646
+ });
647
+ }
648
+ let parsed;
649
+ try {
650
+ parsed = JSON.parse(output);
651
+ } catch {
652
+ return failure({
653
+ code: "INVALID_FORMAT",
654
+ message: "Failed to parse widget package output file as JSON",
655
+ details: `Output was: ${output.slice(0, 200)}`
656
+ });
657
+ }
658
+ if (!isRecord$5(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
659
+ code: "INVALID_FORMAT",
660
+ message: "Widget package extractor output file had an invalid sentinel"
661
+ });
662
+ if (!Array.isArray(parsed.data)) return failure({
663
+ code: "INVALID_FORMAT",
664
+ message: "Extracted widget package output is not an array",
665
+ details: `Expected an array, got: ${typeof parsed.data}`
666
+ });
667
+ return success(parsed.data);
668
+ }
669
+ function createSourcePackageExtractorScript(options) {
670
+ return `
671
+ import fs from "node:fs/promises";
672
+ import path from "node:path";
673
+ import { createServer, normalizePath } from "vite";
674
+
675
+ const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
676
+ const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
677
+ const projectRoot = ${JSON.stringify(options.projectDir)};
678
+ const widgetConfigPath = ${JSON.stringify(options.configPath)};
679
+ const outputPath = process.argv[2];
680
+ if (!outputPath) throw new Error("Missing widget package extractor output path.");
681
+
682
+ function toViteModuleId(modulePath) {
683
+ const relativePath = path.relative(projectRoot, modulePath);
684
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
685
+ return "/" + normalizePath(relativePath);
686
+ }
687
+ return normalizePath(modulePath);
688
+ }
689
+
690
+ const server = await createServer({
691
+ root: projectRoot,
692
+ mode: "production",
693
+ server: { middlewareMode: true },
694
+ appType: "custom",
695
+ logLevel: "error",
696
+ clearScreen: false,
697
+ resolve: {
698
+ conditions: ["fluid-widget-authoring"],
699
+ },
700
+ ssr: {
701
+ resolve: {
702
+ conditions: ["fluid-widget-authoring"],
703
+ },
704
+ },
705
+ });
706
+
707
+ try {
708
+ const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
709
+
710
+ function isSourceWidgetPackage(value) {
711
+ return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
712
+ }
713
+
714
+ function collectSourceWidgetPackages(mod) {
715
+ const candidates = [
716
+ mod.widgetPackage,
717
+ ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
718
+ mod.default,
719
+ ];
720
+ const byPackageId = new Map();
721
+ for (const candidate of candidates) {
722
+ if (!isSourceWidgetPackage(candidate)) continue;
723
+ if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
724
+ }
725
+ return Array.from(byPackageId.values());
726
+ }
727
+
728
+ function serializeWidget(widget) {
729
+ if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
730
+ return widget;
731
+ }
732
+ const { component, ...metadata } = widget;
733
+ return metadata;
734
+ }
735
+
736
+ function serializePackage(sourcePackage) {
737
+ return {
738
+ manifestVersion: sourcePackage.manifestVersion,
739
+ scope: sourcePackage.scope,
740
+ packageStableId: sourcePackage.packageStableId,
741
+ packageId: sourcePackage.packageId,
742
+ packageType: sourcePackage.packageType,
743
+ version: sourcePackage.version,
744
+ cssUrls: sourcePackage.cssUrls,
745
+ widgets: Array.isArray(sourcePackage.widgets)
746
+ ? sourcePackage.widgets.map(serializeWidget)
747
+ : sourcePackage.widgets,
748
+ };
749
+ }
750
+
751
+ await fs.writeFile(
752
+ outputPath,
753
+ JSON.stringify({
754
+ sentinel: OUTPUT_SENTINEL,
755
+ data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
756
+ }),
757
+ "utf-8",
758
+ );
759
+ } finally {
760
+ await server.close();
761
+ }
762
+ `;
763
+ }
764
+ function isRecord$5(value) {
765
+ return typeof value === "object" && value !== null && !Array.isArray(value);
766
+ }
767
+ //#endregion
568
768
  //#region src/utils/extract-manifests.ts
569
769
  /**
570
770
  * Manifest extraction utility
571
771
  *
572
772
  * Extracts serializable widget manifest metadata from portal.config.ts
573
- * by writing a minimal wrapper script that imports customWidgets and
574
- * serializes the result to stdout, then running it with tsx.
773
+ * by writing a minimal wrapper script that imports customWidgets plus
774
+ * source widget package exports and serializes the result to a temp output
775
+ * file, then running it with tsx.
575
776
  *
576
777
  * Strips the `component` field (not serializable) from each manifest.
577
778
  *
578
- * Writes a temp script, runs it with tsx, and parses JSON output. This
579
- * avoids needing Vite's module resolution — portal.config.ts must use
580
- * relative imports only.
581
- *
582
- * Used by `fluid build`. The dev server uses Vite's ssrLoadModule
583
- * instead (see manifest-plugin.ts in portal-sdk).
779
+ * Writes a temp script, runs it with tsx, and parses JSON output from the
780
+ * output file. The wrapper loads config modules through Vite SSR so project
781
+ * aliases and Vite-compatible module resolution are honored.
584
782
  */
585
- const EXTRACT_FILENAME = "__fluid_extract_manifests.ts";
783
+ const EXTRACT_FILENAME = "extract-manifests.ts";
784
+ const EXTRACT_OUTPUT_FILENAME = "manifests.json";
785
+ const EXTRACT_OUTPUT_SENTINEL = "fluid-widget-manifests:v1";
786
+ const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
586
787
  /**
587
788
  * Extract serializable widget manifests from a project's portal.config.ts.
588
789
  *
589
- * Writes a temp wrapper script, runs it with tsx, and parses JSON output.
590
- * The temp file is always cleaned up.
790
+ * Writes a temp wrapper script, runs it with tsx, and parses the temp JSON
791
+ * output file. The temp files are always cleaned up.
591
792
  *
592
- * Returns an empty array if no customWidgets export exists.
793
+ * Returns an empty array if no customWidgets or source package exports exist.
794
+ *
795
+ * Supported static export shapes are intentionally simple so the CLI can avoid
796
+ * executing unrelated portal configs: named `export const|let|var customWidgets`,
797
+ * `widgetPackage`, or `widgetPackages`; direct `export default
798
+ * defineWidgetPackage(...)`; or `export default <identifier>` where that
799
+ * identifier is initialized with `defineWidgetPackage(...)` in the same file.
800
+ * Re-export-only and computed export shapes are not detected by this layer.
593
801
  *
594
802
  * @param projectDir - The project root directory containing src/portal.config.ts
595
803
  */
596
804
  async function extractManifests(projectDir) {
597
- const configPath = path$1.join(projectDir, "src", "portal.config.ts");
598
- const extractFile = path$1.join(projectDir, EXTRACT_FILENAME);
805
+ let tempDir;
599
806
  try {
600
- if (!await fs.pathExists(configPath)) return success([]);
601
- const strippedSource = (await fs.readFile(configPath, "utf-8")).replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
602
- if (!/export\s+(const|let)\s+customWidgets[\s:=]/.test(strippedSource)) return success([]);
603
- await fs.writeFile(extractFile, `
604
- import { customWidgets } from "./src/portal.config.ts";
605
-
606
- const serializable = customWidgets.map(({ component, ...rest }) => rest);
607
- console.log(JSON.stringify(serializable));
608
- `, "utf-8");
609
- const output = (await execa("npx", ["tsx", EXTRACT_FILENAME], {
807
+ const config = await resolvePortalWidgetSourceConfig(projectDir);
808
+ if (!config) return success([]);
809
+ const configExports = readStaticWidgetExports(await fs.readFile(config.path, "utf-8"));
810
+ const legacyPortalConfigPath = path$1.join(projectDir, "src", "portal.config.ts");
811
+ const legacyCustomWidgetsConfigPath = (path$1.resolve(config.path) !== path$1.resolve(legacyPortalConfigPath) && await fs.pathExists(legacyPortalConfigPath) ? readStaticWidgetExports(await fs.readFile(legacyPortalConfigPath, "utf-8")) : void 0)?.hasCustomWidgets ? legacyPortalConfigPath : void 0;
812
+ if (!configExports.hasCustomWidgets && !configExports.hasSourceWidgetPackages && !legacyCustomWidgetsConfigPath) return success([]);
813
+ tempDir = await createTempDirectory(projectDir);
814
+ const extractFile = path$1.join(tempDir, EXTRACT_FILENAME);
815
+ const outputFile = path$1.join(tempDir, EXTRACT_OUTPUT_FILENAME);
816
+ const wrapperScript = createManifestExtractorScript({
817
+ projectDir,
818
+ widgetConfigPath: config.path,
819
+ legacyCustomWidgetsConfigPath
820
+ });
821
+ await fs.writeFile(extractFile, wrapperScript, {
822
+ encoding: "utf-8",
823
+ flag: "wx"
824
+ });
825
+ await execa(process.execPath, [
826
+ TSX_CLI_PATH,
827
+ extractFile,
828
+ outputFile
829
+ ], {
610
830
  cwd: projectDir,
611
831
  stdio: "pipe",
612
832
  env: {
613
833
  ...process.env,
614
834
  NODE_ENV: "production"
615
835
  }
616
- })).stdout.trim();
617
- if (!output || output === "null" || output === "[]") return success([]);
618
- let parsed;
619
- try {
620
- parsed = JSON.parse(output);
621
- } catch {
622
- return failure({
623
- code: "INVALID_FORMAT",
624
- message: "Failed to parse manifest output as JSON",
625
- details: `Output was: ${output.slice(0, 200)}`
626
- });
627
- }
628
- if (!Array.isArray(parsed)) return failure({
629
- code: "INVALID_FORMAT",
630
- message: "customWidgets export is not an array",
631
- details: `Expected an array, got: ${typeof parsed}`
632
836
  });
837
+ const outputResult = await readManifestExtractorOutput(outputFile);
838
+ if (!outputResult.success) return outputResult;
839
+ const parsed = outputResult.value;
840
+ if (parsed.length === 0) return success([]);
633
841
  return success(parsed.filter((m) => typeof m === "object" && m !== null && typeof m.type === "string" && typeof m.displayName === "string"));
634
842
  } catch (err) {
635
843
  const error = err;
636
844
  return failure({
637
845
  code: "EXTRACTION_FAILED",
638
- message: "Failed to extract widget manifests from portal.config.ts",
846
+ message: "Failed to extract widget manifests from portal widget source config",
639
847
  details: error.stderr ?? error.message ?? String(err)
640
848
  });
641
849
  } finally {
642
- await fs.remove(extractFile).catch(() => {});
850
+ if (tempDir) await fs.remove(tempDir).catch(() => {});
851
+ }
852
+ }
853
+ function readStaticWidgetExports(source) {
854
+ const strippedSource = source.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
855
+ const defaultExportMatch = /export\s+default\s+([A-Za-z_$][\w$]*)\b/.exec(strippedSource);
856
+ const widgetPackageDefinitionNames = new Set(Array.from(strippedSource.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)(?:\s*:[^=]+)?\s*=\s*defineWidgetPackage\s*\(/g), (match) => match[1]));
857
+ const hasDefaultWidgetPackageExport = /export\s+default\s+defineWidgetPackage\s*\(/.test(strippedSource) || defaultExportMatch !== null && widgetPackageDefinitionNames.has(defaultExportMatch[1]);
858
+ return {
859
+ hasCustomWidgets: /export\s+(?:const|let|var)\s+customWidgets\b/.test(strippedSource),
860
+ hasSourceWidgetPackages: /export\s+(?:const|let|var)\s+widgetPackage\b/.test(strippedSource) || /export\s+(?:const|let|var)\s+widgetPackages\b/.test(strippedSource) || hasDefaultWidgetPackageExport
861
+ };
862
+ }
863
+ async function createTempDirectory(projectDir) {
864
+ const projectTmpDir = path$1.join(projectDir, ".fluid", "tmp");
865
+ try {
866
+ await fs.ensureDir(projectTmpDir);
867
+ return await fs.mkdtemp(path$1.join(projectTmpDir, "manifests-"));
868
+ } catch (err) {
869
+ const message = err instanceof Error ? err.message : String(err);
870
+ throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
643
871
  }
644
872
  }
873
+ async function readManifestExtractorOutput(outputFile) {
874
+ let output;
875
+ try {
876
+ output = await fs.readFile(outputFile, "utf-8");
877
+ } catch (err) {
878
+ return failure({
879
+ code: "EXTRACTION_FAILED",
880
+ message: "Manifest extractor did not write an output file",
881
+ details: err instanceof Error ? err.message : String(err)
882
+ });
883
+ }
884
+ let parsed;
885
+ try {
886
+ parsed = JSON.parse(output);
887
+ } catch {
888
+ return failure({
889
+ code: "INVALID_FORMAT",
890
+ message: "Failed to parse manifest output file as JSON",
891
+ details: `Output was: ${output.slice(0, 200)}`
892
+ });
893
+ }
894
+ if (!isRecord$4(parsed) || parsed.sentinel !== EXTRACT_OUTPUT_SENTINEL) return failure({
895
+ code: "INVALID_FORMAT",
896
+ message: "Manifest extractor output file had an invalid sentinel"
897
+ });
898
+ if (!Array.isArray(parsed.data)) return failure({
899
+ code: "INVALID_FORMAT",
900
+ message: "extracted widget manifests output is not an array",
901
+ details: `Expected an array, got: ${typeof parsed.data}`
902
+ });
903
+ return success(parsed.data);
904
+ }
905
+ function isRecord$4(value) {
906
+ return typeof value === "object" && value !== null && !Array.isArray(value);
907
+ }
908
+ function createManifestExtractorScript(options) {
909
+ return `
910
+ import fs from "node:fs/promises";
911
+ import path from "node:path";
912
+ import { createServer, normalizePath } from "vite";
913
+ import {
914
+ isSourceWidgetPackage,
915
+ sourceWidgetPackagesToManifests,
916
+ } from "@fluid-app/portal-sdk";
917
+
918
+ const OUTPUT_SENTINEL = ${JSON.stringify(EXTRACT_OUTPUT_SENTINEL)};
919
+ const projectRoot = ${JSON.stringify(options.projectDir)};
920
+ const widgetConfigPath = ${JSON.stringify(options.widgetConfigPath)};
921
+ const legacyCustomWidgetsConfigPath = ${JSON.stringify(options.legacyCustomWidgetsConfigPath)};
922
+ const outputPath = process.argv[2];
923
+ if (!outputPath) throw new Error("Missing manifest extractor output path.");
924
+
925
+ function toViteModuleId(modulePath) {
926
+ const relativePath = path.relative(projectRoot, modulePath);
927
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
928
+ return "/" + normalizePath(relativePath);
929
+ }
930
+ return normalizePath(modulePath);
931
+ }
932
+
933
+ const server = await createServer({
934
+ root: projectRoot,
935
+ mode: "production",
936
+ server: { middlewareMode: true },
937
+ appType: "custom",
938
+ logLevel: "error",
939
+ clearScreen: false,
940
+ });
941
+
942
+ try {
943
+ const portalConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
944
+ const legacyPortalConfig = legacyCustomWidgetsConfigPath
945
+ ? await server.ssrLoadModule(toViteModuleId(legacyCustomWidgetsConfigPath))
946
+ : {};
947
+
948
+ const sourcePackages = [];
949
+ const seenSourcePackageIds = new Set();
950
+ for (const candidate of [
951
+ portalConfig.widgetPackage,
952
+ ...(Array.isArray(portalConfig.widgetPackages) ? portalConfig.widgetPackages : []),
953
+ portalConfig.default,
954
+ ]) {
955
+ if (!isSourceWidgetPackage(candidate)) continue;
956
+ if (seenSourcePackageIds.has(candidate.packageId)) continue;
957
+ seenSourcePackageIds.add(candidate.packageId);
958
+ sourcePackages.push(candidate);
959
+ }
960
+ const manifests = [
961
+ ...(Array.isArray(portalConfig.customWidgets) ? portalConfig.customWidgets : []),
962
+ ...(Array.isArray(legacyPortalConfig.customWidgets) ? legacyPortalConfig.customWidgets : []),
963
+ ...sourceWidgetPackagesToManifests(sourcePackages),
964
+ ];
965
+ const serializable = manifests.map(({ component, ...rest }) => rest);
966
+ await fs.writeFile(
967
+ outputPath,
968
+ JSON.stringify({ sentinel: OUTPUT_SENTINEL, data: serializable }),
969
+ "utf-8",
970
+ );
971
+ } finally {
972
+ await server.close();
973
+ }
974
+ `;
975
+ }
645
976
  //#endregion
646
977
  //#region src/commands/build.ts
647
978
  const buildCommand = new Command("build").description("Build the application for production").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
@@ -895,7 +1226,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
895
1226
  const slug = slugFromPath(file);
896
1227
  try {
897
1228
  const local = await readPortalFile(portalDir, file);
898
- const newId = (await createFluidOSScreen(client, defId, { screen: {
1229
+ const newId = (await fluid_os_v0_create_fluid_osscreen(client, defId, { screen: {
899
1230
  name: local.name,
900
1231
  slug,
901
1232
  component_tree: toApiComponentTree(local.component_tree)
@@ -936,7 +1267,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
936
1267
  }
937
1268
  try {
938
1269
  const local = await readPortalFile(portalDir, file);
939
- await updateFluidOSScreen(client, defId, screenId, { screen: {
1270
+ await fluid_os_v0_update_fluid_osscreen(client, defId, screenId, { screen: {
940
1271
  name: local.name,
941
1272
  slug,
942
1273
  component_tree: toApiComponentTree(local.component_tree)
@@ -975,7 +1306,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
975
1306
  };
976
1307
  }
977
1308
  try {
978
- await deleteFluidOSScreen(client, defId, screenId);
1309
+ await fluid_os_v0_delete_fluid_osscreen(client, defId, screenId);
979
1310
  currentMappings = removeMapping(currentMappings, "screens", slug);
980
1311
  results.push({
981
1312
  file,
@@ -1010,7 +1341,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
1010
1341
  const slug = slugFromPath(file);
1011
1342
  try {
1012
1343
  const local = await readPortalFile(portalDir, file);
1013
- const newId = (await createFluidOSTheme(client, defId, { theme: {
1344
+ const newId = (await fluid_os_v0_create_fluid_ostheme(client, defId, { theme: {
1014
1345
  name: local.name,
1015
1346
  active: local.active,
1016
1347
  config: local.config
@@ -1051,7 +1382,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
1051
1382
  }
1052
1383
  try {
1053
1384
  const local = await readPortalFile(portalDir, file);
1054
- await updateFluidOSTheme(client, defId, themeId, { theme: {
1385
+ await fluid_os_v0_update_fluid_ostheme(client, defId, themeId, { theme: {
1055
1386
  name: local.name,
1056
1387
  active: local.active,
1057
1388
  config: local.config
@@ -1090,7 +1421,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
1090
1421
  };
1091
1422
  }
1092
1423
  try {
1093
- await deleteFluidOSTheme(client, defId, themeId);
1424
+ await fluid_os_v0_delete_fluid_ostheme(client, defId, themeId);
1094
1425
  currentMappings = removeMapping(currentMappings, "themes", slug);
1095
1426
  results.push({
1096
1427
  file,
@@ -1160,7 +1491,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
1160
1491
  const slug = slugFromPath(file);
1161
1492
  try {
1162
1493
  const local = await readPortalFile(portalDir, file);
1163
- const newId = (await createFluidOSNavigation(client, defId, { navigation: {
1494
+ const newId = (await fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
1164
1495
  name: local.name,
1165
1496
  platform: local.platform
1166
1497
  } })).navigation?.id;
@@ -1170,7 +1501,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
1170
1501
  const localToServerId = /* @__PURE__ */ new Map();
1171
1502
  for (const item of resolvedItems) {
1172
1503
  const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
1173
- const created = await createFluidOSNavigationItem(client, defId, newId, { navigation_item: {
1504
+ const created = await fluid_os_v0_create_fluid_osnavigation_item(client, defId, newId, { navigation_item: {
1174
1505
  label: item.label ?? "",
1175
1506
  position: item.position ?? 0,
1176
1507
  icon: item.icon ?? void 0,
@@ -1217,15 +1548,15 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
1217
1548
  }
1218
1549
  try {
1219
1550
  const local = await readPortalFile(portalDir, file);
1220
- await updateFluidOSNavigation(client, defId, navId, { navigation: {
1551
+ await fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
1221
1552
  name: local.name,
1222
1553
  platform: local.platform
1223
1554
  } });
1224
1555
  const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
1225
- const serverItems = (await listFluidOSNavigationItems(client, defId, navId)).navigation_items ?? [];
1556
+ const serverItems = (await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? [];
1226
1557
  const serverById = new Map(serverItems.map((s) => [s.id, s]));
1227
1558
  const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
1228
- for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await deleteFluidOSNavigationItem(client, defId, navId, serverItem.id);
1559
+ for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navId, serverItem.id);
1229
1560
  const localToServerId = /* @__PURE__ */ new Map();
1230
1561
  for (const item of resolvedItems) {
1231
1562
  const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
@@ -1238,9 +1569,9 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
1238
1569
  source: item.source ?? void 0,
1239
1570
  parent_id: resolvedParentId
1240
1571
  };
1241
- if (item.id && serverById.has(item.id)) await updateFluidOSNavigationItem(client, defId, navId, item.id, { navigation_item: body });
1572
+ if (item.id && serverById.has(item.id)) await fluid_os_v0_update_fluid_osnavigation_item(client, defId, navId, item.id, { navigation_item: body });
1242
1573
  else {
1243
- const created = await createFluidOSNavigationItem(client, defId, navId, { navigation_item: {
1574
+ const created = await fluid_os_v0_create_fluid_osnavigation_item(client, defId, navId, { navigation_item: {
1244
1575
  ...body,
1245
1576
  label: body.label ?? "",
1246
1577
  position: body.position ?? 0
@@ -1282,7 +1613,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
1282
1613
  };
1283
1614
  }
1284
1615
  try {
1285
- await deleteFluidOSNavigation(client, defId, navId);
1616
+ await fluid_os_v0_delete_fluid_osnavigation(client, defId, navId);
1286
1617
  currentMappings = removeMapping(currentMappings, "navigations", slug);
1287
1618
  results.push({
1288
1619
  file,
@@ -1316,7 +1647,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
1316
1647
  for (const file of changes.new) {
1317
1648
  const slug = slugFromPath(file);
1318
1649
  try {
1319
- const newId = (await createFluidOSProfile(client, defId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) })).profile?.id;
1650
+ const newId = (await fluid_os_v0_create_fluid_osprofile(client, defId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) })).profile?.id;
1320
1651
  if (newId != null) currentMappings = updateMapping(currentMappings, "profiles", slug, newId);
1321
1652
  results.push({
1322
1653
  file,
@@ -1352,7 +1683,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
1352
1683
  };
1353
1684
  }
1354
1685
  try {
1355
- await updateFluidOSProfile(client, defId, profileId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) });
1686
+ await fluid_os_v0_update_fluid_osprofile(client, defId, profileId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) });
1356
1687
  results.push({
1357
1688
  file,
1358
1689
  action: "updated",
@@ -1387,7 +1718,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
1387
1718
  };
1388
1719
  }
1389
1720
  try {
1390
- await deleteFluidOSProfile(client, defId, profileId);
1721
+ await fluid_os_v0_delete_fluid_osprofile(client, defId, profileId);
1391
1722
  currentMappings = removeMapping(currentMappings, "profiles", slug);
1392
1723
  results.push({
1393
1724
  file,
@@ -1746,7 +2077,7 @@ export function ${componentName}({ title = "${displayName}" }: ${widgetType}Prop
1746
2077
  );
1747
2078
  }
1748
2079
  `);
1749
- await fs.writeFile(path.join(widgetDir, "manifest.ts"), `import type { WidgetManifest } from "@fluid-app/portal-core/registries";
2080
+ await fs.writeFile(path.join(widgetDir, "manifest.ts"), `import type { WidgetManifest } from "@fluid-app/portal-sdk";
1750
2081
  import { ${componentName} } from "./component";
1751
2082
 
1752
2083
  export const manifest: WidgetManifest = {
@@ -1810,6 +2141,1529 @@ export { manifest } from "./manifest";
1810
2141
  });
1811
2142
  const widgetCommand = new Command("widget").description("Manage custom portal widgets").addCommand(createSubcommand);
1812
2143
  //#endregion
2144
+ //#region src/utils/widget-package-artifacts.ts
2145
+ const WIDGET_PACKAGE_BUILDER_VERSION = "1";
2146
+ const CONTENT_TYPES = {
2147
+ ".css": "text/css",
2148
+ ".js": "application/javascript",
2149
+ ".json": "application/json",
2150
+ ".map": "application/json"
2151
+ };
2152
+ const CSS_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.css$/;
2153
+ const JS_MAP_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.js\.map$/;
2154
+ async function computeArtifactMetadata(filePath, baseDir) {
2155
+ const relativePath = toPosixPath$1(path.relative(baseDir, filePath));
2156
+ assertAllowedArtifactPath(relativePath);
2157
+ await assertRegularArtifactFile(filePath, relativePath);
2158
+ const buffer = await fs.readFile(filePath);
2159
+ return {
2160
+ path: relativePath,
2161
+ sha256: sha256(buffer),
2162
+ bytes: buffer.byteLength,
2163
+ contentType: getArtifactContentType(filePath)
2164
+ };
2165
+ }
2166
+ async function collectWidgetPackageArtifacts(publishDir) {
2167
+ const artifactPaths = await findArtifactPaths(publishDir);
2168
+ const artifacts = await Promise.all(artifactPaths.map((artifactPath) => computeArtifactMetadata(artifactPath, publishDir)));
2169
+ assertRequiredUploadArtifacts(artifacts);
2170
+ return artifacts.sort((a, b) => a.path.localeCompare(b.path));
2171
+ }
2172
+ function createPublishManifestMetadata(options) {
2173
+ return {
2174
+ packageId: options.packageId,
2175
+ version: options.version,
2176
+ builderVersion: options.builderVersion ?? "1",
2177
+ cliVersion: options.cliVersion,
2178
+ runtimeVersion: options.runtimeVersion,
2179
+ manifestHash: sha256(options.manifestContent),
2180
+ artifacts: options.artifacts
2181
+ };
2182
+ }
2183
+ function getArtifactContentType(filePath) {
2184
+ if (path.basename(filePath).endsWith(".js.map")) return "application/json";
2185
+ return CONTENT_TYPES[path.extname(filePath)] ?? "application/octet-stream";
2186
+ }
2187
+ function sha256(content) {
2188
+ return createHash("sha256").update(content).digest("hex");
2189
+ }
2190
+ function isWidgetPackageCssArtifactPath(relativePath) {
2191
+ return CSS_ARTIFACT_PATH_PATTERN.test(relativePath);
2192
+ }
2193
+ function isWidgetPackageJsMapArtifactPath(relativePath) {
2194
+ return JS_MAP_ARTIFACT_PATH_PATTERN.test(relativePath);
2195
+ }
2196
+ async function assertRegularArtifactFile(filePath, relativePath) {
2197
+ const stat = await fs.lstat(filePath);
2198
+ if (stat.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
2199
+ if (!stat.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
2200
+ }
2201
+ async function findArtifactPaths(dir, baseDir = dir) {
2202
+ if (!await fs.pathExists(dir)) return [];
2203
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2204
+ return (await Promise.all(entries.map(async (entry) => {
2205
+ const entryPath = path.join(dir, entry.name);
2206
+ const relativePath = toPosixPath$1(path.relative(baseDir, entryPath));
2207
+ if (entry.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
2208
+ if (isUploadArtifact(relativePath)) {
2209
+ if (!entry.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
2210
+ return [entryPath];
2211
+ }
2212
+ if (entry.isDirectory()) {
2213
+ const nestedPaths = await findArtifactPaths(entryPath, baseDir);
2214
+ if (nestedPaths.length === 0) throwUnsupportedArtifactPath(`${relativePath}/`);
2215
+ return nestedPaths;
2216
+ }
2217
+ if (isAllowedGeneratedFile(relativePath)) return [];
2218
+ throwUnsupportedArtifactPath(relativePath);
2219
+ }))).flat();
2220
+ }
2221
+ function assertRequiredUploadArtifacts(artifacts) {
2222
+ const artifactPaths = new Set(artifacts.map((artifact) => artifact.path));
2223
+ for (const requiredPath of ["widget.js", "manifest.json"]) {
2224
+ if (artifactPaths.has(requiredPath)) continue;
2225
+ throw new Error(`Widget package publish output must include regular file ${JSON.stringify(requiredPath)}.`);
2226
+ }
2227
+ }
2228
+ function assertAllowedArtifactPath(relativePath) {
2229
+ if (isUploadArtifact(relativePath) || isAllowedGeneratedFile(relativePath)) return;
2230
+ throwUnsupportedArtifactPath(relativePath);
2231
+ }
2232
+ function isUploadArtifact(relativePath) {
2233
+ return relativePath === "widget.js" || relativePath === "manifest.json" || isWidgetPackageCssArtifactPath(relativePath) || isWidgetPackageJsMapArtifactPath(relativePath);
2234
+ }
2235
+ function isAllowedGeneratedFile(relativePath) {
2236
+ return isFlatArtifactPath(relativePath) && relativePath === "publish-manifest.json";
2237
+ }
2238
+ function isFlatArtifactPath(relativePath) {
2239
+ return relativePath.length > 0 && !relativePath.includes("/") && !relativePath.includes("\\");
2240
+ }
2241
+ function throwUnsupportedArtifactPath(relativePath) {
2242
+ throw new Error(`Unsupported widget publish artifact ${JSON.stringify(relativePath)}. Allowed files are widget.js, manifest.json, top-level [A-Za-z0-9._~-]+.css, top-level [A-Za-z0-9._~-]+.js.map, and publish-manifest.json.`);
2243
+ }
2244
+ function toPosixPath$1(value) {
2245
+ return value.split(path.sep).join("/");
2246
+ }
2247
+ //#endregion
2248
+ //#region src/utils/widget-package-validation.ts
2249
+ const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
2250
+ const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
2251
+ const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
2252
+ const SEMVER_CORE_PATTERN = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)";
2253
+ const SEMVER_PRERELEASE_IDENTIFIER_PATTERN = "(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)";
2254
+ const SEMVER_URL_SAFE_PATTERN = new RegExp(`^${SEMVER_CORE_PATTERN}(?:-${SEMVER_PRERELEASE_IDENTIFIER_PATTERN}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER_PATTERN})*)?$`);
2255
+ const WIDGET_PACKAGE_MANIFEST_VERSION = 1;
2256
+ const DEFAULT_WIDGET_ICON = "box";
2257
+ const DEFAULT_WIDGET_CATEGORY = "components";
2258
+ const DEFAULT_WIDGET_CONTAINER = "block";
2259
+ const DEFAULT_MIN_SDK_VERSION = "0.0.0";
2260
+ function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
2261
+ if (sourcePackages.length === 0) return validationFailure({
2262
+ code: "NO_SOURCE_PACKAGE",
2263
+ message: "No defineWidgetPackage source package was found. Export widgetPackage, widgetPackages, or a default widget package from src/widgets.config.ts, src/portal.config.ts, or portal.config.ts."
2264
+ });
2265
+ if (sourcePackages.length > 1) return validationFailure({
2266
+ code: "MULTIPLE_SOURCE_PACKAGES",
2267
+ message: "Publish/deploy supports exactly one defineWidgetPackage source package. Export one package or split packages into separate builds."
2268
+ });
2269
+ const sourcePackageValue = sourcePackages[0];
2270
+ if (!sourcePackageValue) return validationFailure({
2271
+ code: "NO_SOURCE_PACKAGE",
2272
+ message: "No defineWidgetPackage source package was found."
2273
+ });
2274
+ const shapeErrors = validateSourcePackageShape(sourcePackageValue);
2275
+ if (shapeErrors.length > 0) return {
2276
+ success: false,
2277
+ errors: shapeErrors
2278
+ };
2279
+ const sourcePackage = sourcePackageValue;
2280
+ const errors = [];
2281
+ const owner = options.owner ?? sourcePackage.packageType;
2282
+ if (owner !== "company" && owner !== "droplet") errors.push({
2283
+ code: "UNSUPPORTED_OWNER",
2284
+ path: "packageType",
2285
+ message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
2286
+ });
2287
+ const resolvedOwner = owner === "droplet" ? "droplet" : "company";
2288
+ const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
2289
+ const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
2290
+ if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
2291
+ code: "INVALID_PACKAGE_KEY",
2292
+ path: packageKeyPath,
2293
+ message: "Widget package key must be URL-safe dot-separated text (letters, numbers, '.', '_', '~', and '-' only)."
2294
+ });
2295
+ if (!isUrlSafeSemver(sourcePackage.version)) errors.push({
2296
+ code: "INVALID_VERSION",
2297
+ path: "version",
2298
+ 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)."
2299
+ });
2300
+ if (sourcePackage.widgets.length === 0) errors.push({
2301
+ code: "NO_WIDGETS",
2302
+ path: "widgets",
2303
+ message: "Widget package must include at least one widget."
2304
+ });
2305
+ const packageId = `${resolvedOwner}.${packageKey}`;
2306
+ const seenNames = /* @__PURE__ */ new Set();
2307
+ const seenTypes = /* @__PURE__ */ new Set();
2308
+ const widgets = [];
2309
+ sourcePackage.widgets.forEach((widget, index) => {
2310
+ const path = `widgets[${index}]`;
2311
+ if (!isUrlSafeWidgetName(widget.name)) {
2312
+ errors.push({
2313
+ code: "INVALID_WIDGET_NAME",
2314
+ path: `${path}.name`,
2315
+ message: "Widget name must be URL-safe text (letters, numbers, '_', '~', and '-' only) and cannot be empty."
2316
+ });
2317
+ return;
2318
+ }
2319
+ if (seenNames.has(widget.name)) {
2320
+ errors.push({
2321
+ code: "DUPLICATE_WIDGET",
2322
+ path: `${path}.name`,
2323
+ message: `Duplicate widget name "${widget.name}" found in package ${packageKey}.`
2324
+ });
2325
+ return;
2326
+ }
2327
+ seenNames.add(widget.name);
2328
+ const type = `${packageId}.${widget.name}`;
2329
+ if (!isUrlSafeDottedIdentifier(type)) {
2330
+ errors.push({
2331
+ code: "INVALID_WIDGET_TYPE",
2332
+ path: `${path}.type`,
2333
+ message: `Generated widget type "${type}" is not URL-safe.`
2334
+ });
2335
+ return;
2336
+ }
2337
+ if (seenTypes.has(type)) {
2338
+ errors.push({
2339
+ code: "DUPLICATE_WIDGET",
2340
+ path: `${path}.type`,
2341
+ message: `Duplicate generated widget type "${type}" found.`
2342
+ });
2343
+ return;
2344
+ }
2345
+ seenTypes.add(type);
2346
+ widgets.push(sourceWidgetToDescriptor(widget, type));
2347
+ });
2348
+ if (errors.length > 0) return {
2349
+ success: false,
2350
+ errors
2351
+ };
2352
+ return {
2353
+ success: true,
2354
+ value: {
2355
+ sourcePackage,
2356
+ packageKey,
2357
+ packageId,
2358
+ owner: resolvedOwner,
2359
+ version: sourcePackage.version,
2360
+ widgets
2361
+ },
2362
+ errors: []
2363
+ };
2364
+ }
2365
+ function buildWidgetPackageDescriptor(validated, options = {}) {
2366
+ const remoteEntryUrl = options.remoteEntryUrl ?? "widget.js";
2367
+ validateRuntimeDescriptorUrl(remoteEntryUrl, "remoteEntryUrl");
2368
+ return {
2369
+ manifestVersion: WIDGET_PACKAGE_MANIFEST_VERSION,
2370
+ packageId: getWidgetPackageDescriptorPackageId(validated),
2371
+ packageType: validated.owner,
2372
+ version: validated.version,
2373
+ remoteEntryUrl,
2374
+ cssUrls: mergeCssUrls(validated.sourcePackage.cssUrls, options.cssUrls),
2375
+ widgets: validated.widgets
2376
+ };
2377
+ }
2378
+ function getWidgetPackageDescriptorPackageId(validated) {
2379
+ return validated.owner === "droplet" ? validated.packageKey : validated.packageId;
2380
+ }
2381
+ function sourceWidgetToDescriptor(widget, type) {
2382
+ const displayName = widget.displayName ?? widget.name;
2383
+ return {
2384
+ type,
2385
+ name: widget.name,
2386
+ displayName,
2387
+ description: widget.description ?? `Custom widget ${displayName}`,
2388
+ icon: widget.icon ?? DEFAULT_WIDGET_ICON,
2389
+ category: widget.category ?? DEFAULT_WIDGET_CATEGORY,
2390
+ propertySchema: normalizePropertySchema(widget.propertySchema, type),
2391
+ defaultProps: widget.defaultProps ?? {},
2392
+ container: normalizeContainer(widget.container),
2393
+ minSdkVersion: widget.minSdkVersion ?? DEFAULT_MIN_SDK_VERSION,
2394
+ resizable: normalizeResizable(widget.resizable)
2395
+ };
2396
+ }
2397
+ function validationFailure(error) {
2398
+ return {
2399
+ success: false,
2400
+ errors: [error]
2401
+ };
2402
+ }
2403
+ function validateSourcePackageShape(value) {
2404
+ if (!isRecord$3(value)) return [{
2405
+ code: "INVALID_SOURCE_PACKAGE",
2406
+ message: "Widget source package must be a JSON object."
2407
+ }];
2408
+ const errors = [];
2409
+ requireStringField(value, "packageId", "packageId", errors);
2410
+ requireStringField(value, "version", "version", errors);
2411
+ validateOptionalStringField(value, "scope", "scope", errors);
2412
+ validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
2413
+ validateOptionalStringField(value, "packageType", "packageType", errors);
2414
+ if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
2415
+ code: "INVALID_SOURCE_PACKAGE",
2416
+ path: "cssUrls",
2417
+ message: "Widget package cssUrls must be an array of strings when present."
2418
+ });
2419
+ else value.cssUrls.forEach((cssUrl, index) => {
2420
+ validateCssUrl(cssUrl, `cssUrls[${index}]`, errors);
2421
+ });
2422
+ if (!Array.isArray(value.widgets)) {
2423
+ errors.push({
2424
+ code: "INVALID_SOURCE_PACKAGE",
2425
+ path: "widgets",
2426
+ message: "Widget package widgets must be an array."
2427
+ });
2428
+ return errors;
2429
+ }
2430
+ value.widgets.forEach((widget, index) => {
2431
+ const widgetPath = `widgets[${index}]`;
2432
+ if (!isRecord$3(widget)) {
2433
+ errors.push({
2434
+ code: "INVALID_WIDGET_METADATA",
2435
+ path: widgetPath,
2436
+ message: "Widget metadata must be a JSON object."
2437
+ });
2438
+ return;
2439
+ }
2440
+ requireStringField(widget, "name", `${widgetPath}.name`, errors, {
2441
+ code: "INVALID_WIDGET_NAME",
2442
+ message: "Widget name must be a string and cannot be empty."
2443
+ });
2444
+ validateOptionalStringField(widget, "displayName", `${widgetPath}.displayName`, errors, { requireNonEmpty: true });
2445
+ validateOptionalStringField(widget, "description", `${widgetPath}.description`, errors, { requireNonEmpty: true });
2446
+ validateOptionalStringField(widget, "icon", `${widgetPath}.icon`, errors, { requireNonEmpty: true });
2447
+ validateOptionalStringField(widget, "category", `${widgetPath}.category`, errors, { requireNonEmpty: true });
2448
+ validateOptionalStringField(widget, "minSdkVersion", `${widgetPath}.minSdkVersion`, errors, { requireNonEmpty: true });
2449
+ validateOptionalRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
2450
+ validateOptionalRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
2451
+ });
2452
+ return errors;
2453
+ }
2454
+ function requireStringField(record, key, path, errors, override) {
2455
+ const value = record[key];
2456
+ if (typeof value === "string" && value.trim().length > 0) return;
2457
+ errors.push({
2458
+ code: override?.code ?? "INVALID_SOURCE_PACKAGE",
2459
+ path,
2460
+ message: override?.message ?? `${path} must be a string and cannot be empty.`
2461
+ });
2462
+ }
2463
+ function validateOptionalStringField(record, key, path, errors, options = {}) {
2464
+ const value = record[key];
2465
+ if (value === void 0) return;
2466
+ if (typeof value === "string") {
2467
+ if (!options.requireNonEmpty || value.trim().length > 0) return;
2468
+ errors.push({
2469
+ code: "INVALID_SOURCE_PACKAGE",
2470
+ path,
2471
+ message: `${path} must be a non-empty string when present.`
2472
+ });
2473
+ return;
2474
+ }
2475
+ errors.push({
2476
+ code: "INVALID_SOURCE_PACKAGE",
2477
+ path,
2478
+ message: `${path} must be a string when present.`
2479
+ });
2480
+ }
2481
+ function validateCssUrl(value, cssUrlPath, errors) {
2482
+ if (typeof value !== "string" || value.trim().length === 0) {
2483
+ errors.push({
2484
+ code: "INVALID_SOURCE_PACKAGE",
2485
+ path: cssUrlPath,
2486
+ message: "Widget package cssUrls entries must be non-empty strings."
2487
+ });
2488
+ return;
2489
+ }
2490
+ if (!isValidRuntimeDescriptorUrl(value)) {
2491
+ errors.push({
2492
+ code: "INVALID_SOURCE_PACKAGE",
2493
+ path: cssUrlPath,
2494
+ message: "Widget package cssUrls entries must be https URLs, trusted local http URLs, or relative URLs."
2495
+ });
2496
+ return;
2497
+ }
2498
+ if (!isAbsoluteRuntimeUrl(value) && !isWidgetPackageCssArtifactPath(value)) errors.push({
2499
+ code: "INVALID_SOURCE_PACKAGE",
2500
+ path: cssUrlPath,
2501
+ message: "Widget package relative cssUrls entries must be top-level CSS artifact filenames matching [A-Za-z0-9._~-]+.css."
2502
+ });
2503
+ }
2504
+ function validateRuntimeDescriptorUrl(value, urlPath) {
2505
+ if (value.trim().length === 0) throw new Error(`${urlPath} must be a non-empty string.`);
2506
+ if (!isValidRuntimeDescriptorUrl(value)) throw new Error(`${urlPath} must be an https URL, trusted local http URL, or relative URL.`);
2507
+ }
2508
+ function isValidRuntimeDescriptorUrl(value) {
2509
+ if (value.trim() !== value || containsUnsafeUrlCharacter(value)) return false;
2510
+ if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.exec(value)) {
2511
+ let parsedUrl;
2512
+ try {
2513
+ parsedUrl = new URL(value);
2514
+ } catch {
2515
+ return false;
2516
+ }
2517
+ return parsedUrl.protocol === "https:" || isTrustedLocalHttpUrl(parsedUrl, value);
2518
+ }
2519
+ if (value.slice(0, 2) === "//") return false;
2520
+ try {
2521
+ new URL(value, "http://localhost");
2522
+ return true;
2523
+ } catch {
2524
+ return false;
2525
+ }
2526
+ }
2527
+ function isTrustedLocalHttpUrl(url, rawUrl) {
2528
+ if (url.protocol !== "http:") return false;
2529
+ const hostname = extractRawHostname(rawUrl).toLowerCase();
2530
+ return hostname === "localhost" || hostname === "[::1]" || hostname === "::1" || isLoopbackIpv4Hostname(hostname);
2531
+ }
2532
+ function extractRawHostname(rawUrl) {
2533
+ const authorityStart = rawUrl.indexOf("://") + 3;
2534
+ const authorityEnd = findAuthorityEnd(rawUrl, authorityStart);
2535
+ const authority = rawUrl.slice(authorityStart, authorityEnd);
2536
+ const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
2537
+ if (hostAndPort.charAt(0) === "[") {
2538
+ const bracketEnd = hostAndPort.indexOf("]");
2539
+ return bracketEnd === -1 ? hostAndPort : hostAndPort.slice(0, bracketEnd + 1);
2540
+ }
2541
+ const portStart = hostAndPort.indexOf(":");
2542
+ return portStart === -1 ? hostAndPort : hostAndPort.slice(0, portStart);
2543
+ }
2544
+ function findAuthorityEnd(rawUrl, authorityStart) {
2545
+ let authorityEnd = rawUrl.length;
2546
+ for (const delimiter of [
2547
+ "/",
2548
+ "?",
2549
+ "#"
2550
+ ]) {
2551
+ const delimiterIndex = rawUrl.indexOf(delimiter, authorityStart);
2552
+ if (delimiterIndex !== -1 && delimiterIndex < authorityEnd) authorityEnd = delimiterIndex;
2553
+ }
2554
+ return authorityEnd;
2555
+ }
2556
+ function isLoopbackIpv4Hostname(hostname) {
2557
+ const octets = hostname.split(".");
2558
+ if (octets.length !== 4 || octets[0] !== "127") return false;
2559
+ return octets.every(isBoundedIpv4Octet);
2560
+ }
2561
+ function isBoundedIpv4Octet(octet) {
2562
+ if (!/^\d{1,3}$/.test(octet)) return false;
2563
+ if (octet.length > 1 && octet.charAt(0) === "0") return false;
2564
+ const value = Number(octet);
2565
+ return value >= 0 && value <= 255;
2566
+ }
2567
+ function containsUnsafeUrlCharacter(value) {
2568
+ for (let index = 0; index < value.length; index += 1) {
2569
+ const charCode = value.charCodeAt(index);
2570
+ if (charCode <= 31 || charCode === 127 || charCode === 92) return true;
2571
+ }
2572
+ return false;
2573
+ }
2574
+ function validateOptionalRecordField(record, key, path, errors) {
2575
+ const value = record[key];
2576
+ if (value === void 0 || isRecord$3(value)) return;
2577
+ errors.push({
2578
+ code: "INVALID_WIDGET_METADATA",
2579
+ path,
2580
+ message: `${path} must be a JSON object when present.`
2581
+ });
2582
+ }
2583
+ function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
2584
+ const generatedCssUrlSet = new Set(generatedCssUrls ?? []);
2585
+ sourceCssUrls?.forEach((cssUrl, index) => {
2586
+ validateRuntimeDescriptorUrl(cssUrl, `cssUrls[${index}]`);
2587
+ if (isAbsoluteRuntimeUrl(cssUrl)) return;
2588
+ if (!isWidgetPackageCssArtifactPath(cssUrl)) throw new Error(`cssUrls[${index}] relative URL ${JSON.stringify(cssUrl)} must be a top-level CSS artifact filename matching [A-Za-z0-9._~-]+.css.`);
2589
+ if (generatedCssUrlSet.has(cssUrl)) return;
2590
+ throw new Error(`cssUrls[${index}] relative 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.`);
2591
+ });
2592
+ const cssUrls = Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []]));
2593
+ cssUrls.forEach((cssUrl, index) => {
2594
+ validateRuntimeDescriptorUrl(cssUrl, `cssUrls[${index}]`);
2595
+ });
2596
+ return cssUrls;
2597
+ }
2598
+ function isAbsoluteRuntimeUrl(value) {
2599
+ return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
2600
+ }
2601
+ function normalizePropertySchema(value, widgetType) {
2602
+ return {
2603
+ ...value ?? {},
2604
+ widgetType
2605
+ };
2606
+ }
2607
+ function normalizeContainer(value) {
2608
+ if (value === "inline" || value === "block" || value === "card" || value === "fullscreen") return value;
2609
+ return DEFAULT_WIDGET_CONTAINER;
2610
+ }
2611
+ function normalizeResizable(value) {
2612
+ if (typeof value === "boolean") return value;
2613
+ if (value === "horizontal" || value === "vertical" || value === "both") return value;
2614
+ if (!isRecord$3(value)) return false;
2615
+ const horizontal = value.horizontal === true;
2616
+ const vertical = value.vertical === true;
2617
+ if (horizontal && vertical) return "both";
2618
+ if (horizontal) return "horizontal";
2619
+ if (vertical) return "vertical";
2620
+ return false;
2621
+ }
2622
+ function resolvePackageKey(sourcePackage, owner) {
2623
+ const packageStableId = readNonEmptyTrimmedString(sourcePackage.packageStableId);
2624
+ if (packageStableId) return prefixPackageStableIdWithNonOwnerScope(packageStableId, sourcePackage.scope);
2625
+ return stripOwnerScopeFromPackageId(sourcePackage.packageId, sourcePackage.scope, owner);
2626
+ }
2627
+ function prefixPackageStableIdWithNonOwnerScope(packageStableId, sourceScope) {
2628
+ const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
2629
+ if (!sourceScopeText || isWidgetPackageOwnerKind(sourceScopeText)) return packageStableId;
2630
+ const sourceScopePrefix = `${sourceScopeText}.`;
2631
+ if (packageStableId.startsWith(sourceScopePrefix)) return packageStableId;
2632
+ return `${sourceScopeText}.${packageStableId}`;
2633
+ }
2634
+ function stripOwnerScopeFromPackageId(packageId, sourceScope, owner) {
2635
+ const ownerPrefix = `${owner}.`;
2636
+ if (packageId.startsWith(ownerPrefix) && packageId.length > ownerPrefix.length) return packageId.slice(ownerPrefix.length);
2637
+ const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
2638
+ if (isWidgetPackageOwnerKind(sourceScopeText) && packageId.startsWith(`${sourceScopeText}.`) && packageId.length > sourceScopeText.length + 1) return packageId.slice(sourceScopeText.length + 1);
2639
+ return packageId;
2640
+ }
2641
+ function readNonEmptyTrimmedString(value) {
2642
+ const trimmed = value?.trim();
2643
+ return trimmed ? trimmed : void 0;
2644
+ }
2645
+ function isWidgetPackageOwnerKind(value) {
2646
+ return value === "company" || value === "droplet";
2647
+ }
2648
+ function isUrlSafeDottedIdentifier(value) {
2649
+ return URL_SAFE_DOTTED_IDENTIFIER_PATTERN.test(value);
2650
+ }
2651
+ function isUrlSafeWidgetName(value) {
2652
+ return URL_SAFE_WIDGET_NAME_PATTERN.test(value);
2653
+ }
2654
+ function isUrlSafeSemver(value) {
2655
+ return SEMVER_URL_SAFE_PATTERN.test(value);
2656
+ }
2657
+ function isRecord$3(value) {
2658
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2659
+ }
2660
+ //#endregion
2661
+ //#region src/utils/widget-package-builder.ts
2662
+ const DEFAULT_PUBLISH_DIR = ".fluid/widget-dist";
2663
+ const DEFAULT_RUNTIME_VERSION = "1";
2664
+ const TEMP_BUILD_DIR = ".fluid/widget-build";
2665
+ const TEMP_CLI_DIR = ".fluid/tmp";
2666
+ const PROJECT_VITE_CONFIG_CANDIDATES = [
2667
+ "vite.config.ts",
2668
+ "vite.config.mts",
2669
+ "vite.config.js",
2670
+ "vite.config.mjs"
2671
+ ];
2672
+ const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = [
2673
+ "fluid-manifest-plugin",
2674
+ "fluid-preview-plugin",
2675
+ "fluid-builder-preview",
2676
+ "fluid-builder-preview-standalone",
2677
+ "fluid-portal-dev",
2678
+ "fluid-backend-dev"
2679
+ ];
2680
+ const require = createRequire(import.meta.url);
2681
+ function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
2682
+ const label = options.optionName ?? "publishDir";
2683
+ if (path.isAbsolute(publishDir)) throw new Error(`${label} must be relative to the project directory.`);
2684
+ const normalizedPublishDir = normalizePublishDirForValidation(publishDir);
2685
+ if (normalizedPublishDir === ".fluid" || !normalizedPublishDir.startsWith(".fluid/")) throw new Error(`${label} must be a relative path under .fluid/.`);
2686
+ if (isPathWithinReservedDir(normalizedPublishDir, TEMP_BUILD_DIR)) throw new Error(`${label} must not be ${TEMP_BUILD_DIR} because it is reserved for temporary builder output.`);
2687
+ 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.`);
2688
+ const resolvedProjectDir = path.resolve(projectDir);
2689
+ const resolvedPublishDir = path.resolve(resolvedProjectDir, publishDir);
2690
+ const relative = path.relative(resolvedProjectDir, resolvedPublishDir);
2691
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${label} must stay inside the project directory.`);
2692
+ }
2693
+ async function buildSharedWidgetPackage(options) {
2694
+ const publishDirInput = options.publishDir ?? DEFAULT_PUBLISH_DIR;
2695
+ try {
2696
+ await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
2697
+ } catch (err) {
2698
+ return failure({
2699
+ code: "INVALID_PUBLISH_DIR",
2700
+ message: "Unsafe widget package publish directory",
2701
+ details: (err instanceof Error ? err : new Error(String(err))).message
2702
+ });
2703
+ }
2704
+ const sourcePackageResult = await loadSourceWidgetPackages(options.projectDir);
2705
+ if (!sourcePackageResult.success) return failure({
2706
+ code: "CONFIG_LOAD_FAILED",
2707
+ message: sourcePackageResult.error.message,
2708
+ details: sourcePackageResult.error.details
2709
+ });
2710
+ const validation = validateSingleSourceWidgetPackage(sourcePackageResult.value, { owner: options.owner });
2711
+ if (!validation.success || !validation.value) return failure({
2712
+ code: "VALIDATION_FAILED",
2713
+ message: validation.errors.map((error) => error.message).join("\n")
2714
+ });
2715
+ try {
2716
+ await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
2717
+ const publishDir = path.resolve(options.projectDir, publishDirInput);
2718
+ await fs.emptyDir(publishDir);
2719
+ const tempDir = path.resolve(options.projectDir, TEMP_BUILD_DIR);
2720
+ await fs.emptyDir(tempDir);
2721
+ try {
2722
+ await buildWidgetScriptBundle({
2723
+ projectDir: options.projectDir,
2724
+ tempDir,
2725
+ publishDir,
2726
+ validated: validation.value
2727
+ });
2728
+ } finally {
2729
+ await fs.remove(tempDir).catch(() => {});
2730
+ }
2731
+ const cssUrls = await collectCssUrls(publishDir);
2732
+ const manifestContent = stableStringify(buildWidgetPackageDescriptor(validation.value, { cssUrls }));
2733
+ const manifestPath = path.join(publishDir, "manifest.json");
2734
+ const widgetScriptPath = path.join(publishDir, "widget.js");
2735
+ const publishManifestPath = path.join(publishDir, "publish-manifest.json");
2736
+ await fs.writeFile(manifestPath, manifestContent, "utf-8");
2737
+ const artifacts = await collectWidgetPackageArtifacts(publishDir);
2738
+ const publishManifest = createPublishManifestMetadata({
2739
+ packageId: getWidgetPackageDescriptorPackageId(validation.value),
2740
+ version: validation.value.version,
2741
+ cliVersion: readCliVersion(),
2742
+ runtimeVersion: DEFAULT_RUNTIME_VERSION,
2743
+ manifestContent,
2744
+ artifacts
2745
+ });
2746
+ await fs.writeFile(publishManifestPath, stableStringify(publishManifest), "utf-8");
2747
+ return success({
2748
+ publishDir,
2749
+ packageId: getWidgetPackageDescriptorPackageId(validation.value),
2750
+ version: validation.value.version,
2751
+ manifestPath,
2752
+ publishManifestPath,
2753
+ widgetScriptPath
2754
+ });
2755
+ } catch (err) {
2756
+ const error = err instanceof Error ? err : new Error(String(err));
2757
+ const isPublishDirValidationError = /^(publishDir|reserved temporary build directory) /.test(error.message);
2758
+ return failure({
2759
+ code: isPublishDirValidationError ? "INVALID_PUBLISH_DIR" : "WRITE_FAILED",
2760
+ message: isPublishDirValidationError ? "Unsafe widget package publish directory" : "Failed to build shared widget package artifacts",
2761
+ details: error.message
2762
+ });
2763
+ }
2764
+ }
2765
+ function createWidgetPackageEntrySource(options) {
2766
+ return `import * as widgetConfig from ${JSON.stringify(options.configImportPath)};
2767
+
2768
+ const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
2769
+ const descriptors = ${JSON.stringify(options.validated.widgets, null, 2)};
2770
+
2771
+ function isSourceWidgetPackage(value) {
2772
+ return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
2773
+ }
2774
+
2775
+ function collectSourceWidgetPackages(mod) {
2776
+ return [
2777
+ mod.widgetPackage,
2778
+ ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
2779
+ mod.default,
2780
+ ].filter(isSourceWidgetPackage);
2781
+ }
2782
+
2783
+ const sourcePackage = collectSourceWidgetPackages(widgetConfig)[0];
2784
+ if (!sourcePackage) {
2785
+ throw new Error("No Fluid source widget package found while loading widget package bundle.");
2786
+ }
2787
+
2788
+ const componentsByName = new Map(
2789
+ sourcePackage.widgets.map((widget) => [widget.name, widget.component]),
2790
+ );
2791
+
2792
+ const widgets = descriptors.map((descriptor) => {
2793
+ const component = componentsByName.get(descriptor.name);
2794
+ if (typeof component !== "function" && typeof component !== "object") {
2795
+ throw new Error("Widget package is missing component for " + descriptor.name + ".");
2796
+ }
2797
+ return { ...descriptor, component };
2798
+ });
2799
+
2800
+ if (!window.FluidWidgets || typeof window.FluidWidgets.registerPackage !== "function") {
2801
+ throw new Error("FluidWidgets registry is not installed.");
2802
+ }
2803
+
2804
+ window.FluidWidgets.registerPackage({
2805
+ packageId: ${JSON.stringify(options.validated.packageId)},
2806
+ version: ${JSON.stringify(options.validated.version)},
2807
+ widgets,
2808
+ });
2809
+ `;
2810
+ }
2811
+ async function buildWidgetScriptBundle(options) {
2812
+ const sourceConfig = await resolvePortalWidgetSourceConfig(options.projectDir);
2813
+ if (!sourceConfig) throw new Error("No portal widget source config found.");
2814
+ const entryPath = path.join(options.tempDir, "widget-entry.ts");
2815
+ const viteConfigPath = path.join(options.tempDir, "vite.config.mjs");
2816
+ await fs.writeFile(entryPath, createWidgetPackageEntrySource({
2817
+ configImportPath: pathToFileURL(sourceConfig.path).href,
2818
+ validated: options.validated
2819
+ }), "utf-8");
2820
+ await fs.writeFile(viteConfigPath, createViteConfigSource({
2821
+ entryPath,
2822
+ publishDir: options.publishDir,
2823
+ projectConfigPath: await resolveProjectViteConfigPath(options.projectDir)
2824
+ }), "utf-8");
2825
+ const viteCliPath = resolveViteCliPath(options.projectDir);
2826
+ await execa(process.execPath, [
2827
+ viteCliPath,
2828
+ "build",
2829
+ "--config",
2830
+ viteConfigPath
2831
+ ], {
2832
+ cwd: options.projectDir,
2833
+ stdio: "pipe",
2834
+ env: {
2835
+ ...process.env,
2836
+ NODE_ENV: "production"
2837
+ }
2838
+ });
2839
+ }
2840
+ function resolveViteCliPath(projectDir) {
2841
+ const projectViteCliPath = resolveViteCliPathFromRequire(createRequire(path.join(projectDir, "package.json")));
2842
+ if (projectViteCliPath) return projectViteCliPath;
2843
+ const cliViteCliPath = resolveViteCliPathFromRequire(require);
2844
+ if (cliViteCliPath) return cliViteCliPath;
2845
+ throw new Error("Unable to resolve Vite for the portal project. Install the project dependencies and ensure vite is available before building widget packages.");
2846
+ }
2847
+ async function resolveProjectViteConfigPath(projectDir) {
2848
+ for (const fileName of PROJECT_VITE_CONFIG_CANDIDATES) {
2849
+ const candidate = path.join(projectDir, fileName);
2850
+ if (await fs.pathExists(candidate)) return candidate;
2851
+ }
2852
+ }
2853
+ function createViteConfigSource(options) {
2854
+ const widgetConfigSource = createWidgetViteConfigObjectSource({
2855
+ ...options,
2856
+ disableConfigFileLookup: !options.projectConfigPath
2857
+ });
2858
+ if (!options.projectConfigPath) return `import { defineConfig } from "vite";
2859
+
2860
+ export default defineConfig(${widgetConfigSource});
2861
+ `;
2862
+ return `import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
2863
+
2864
+ const projectConfigPath = ${JSON.stringify(options.projectConfigPath)};
2865
+ const widgetConfig = ${widgetConfigSource};
2866
+ const blockedFluidPluginNames = new Set(${JSON.stringify(BLOCKED_FLUID_PROJECT_PLUGIN_NAMES)});
2867
+
2868
+ function isAllowedProjectPlugin(plugin) {
2869
+ return !plugin || typeof plugin !== "object" || !blockedFluidPluginNames.has(plugin.name);
2870
+ }
2871
+
2872
+ function sanitizeProjectPlugins(pluginOption) {
2873
+ if (!Array.isArray(pluginOption)) {
2874
+ return isAllowedProjectPlugin(pluginOption) ? pluginOption : [];
2875
+ }
2876
+
2877
+ return pluginOption
2878
+ .map((plugin) => Array.isArray(plugin) ? sanitizeProjectPlugins(plugin) : plugin)
2879
+ .filter(isAllowedProjectPlugin);
2880
+ }
2881
+
2882
+ function copyProjectConfigValue(source, target, key) {
2883
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
2884
+ target[key] = source[key];
2885
+ }
2886
+ }
2887
+
2888
+ function sanitizeProjectConfig(config) {
2889
+ if (!config || typeof config !== "object") return {};
2890
+
2891
+ const sanitized = {};
2892
+ if (Object.prototype.hasOwnProperty.call(config, "plugins")) {
2893
+ sanitized.plugins = sanitizeProjectPlugins(config.plugins);
2894
+ }
2895
+ copyProjectConfigValue(config, sanitized, "resolve");
2896
+ copyProjectConfigValue(config, sanitized, "define");
2897
+ copyProjectConfigValue(config, sanitized, "css");
2898
+ copyProjectConfigValue(config, sanitized, "esbuild");
2899
+ copyProjectConfigValue(config, sanitized, "json");
2900
+ copyProjectConfigValue(config, sanitized, "assetsInclude");
2901
+ copyProjectConfigValue(config, sanitized, "envDir");
2902
+ copyProjectConfigValue(config, sanitized, "envPrefix");
2903
+ return sanitized;
2904
+ }
2905
+
2906
+ export default defineConfig(async (configEnv) => {
2907
+ const projectConfig = await loadConfigFromFile(configEnv, projectConfigPath);
2908
+ const safeProjectConfig = sanitizeProjectConfig(projectConfig?.config ?? {});
2909
+ return mergeConfig(safeProjectConfig, widgetConfig);
2910
+ });
2911
+ `;
2912
+ }
2913
+ function createWidgetViteConfigObjectSource(options) {
2914
+ const configFileSource = options.disableConfigFileLookup ? " configFile: false,\n" : "";
2915
+ return `(() => {
2916
+ function createFluidWidgetBuildInvariants() {
2917
+ return {
2918
+ copyPublicDir: false,
2919
+ emptyOutDir: false,
2920
+ outDir: ${JSON.stringify(options.publishDir)},
2921
+ sourcemap: false,
2922
+ lib: {
2923
+ entry: ${JSON.stringify(options.entryPath)},
2924
+ formats: ["iife"],
2925
+ name: "FluidWidgetPackage",
2926
+ fileName: () => "widget.js",
2927
+ },
2928
+ rollupOptions: {
2929
+ external: ["react", "react-dom", "react/jsx-runtime"],
2930
+ output: {
2931
+ globals: {
2932
+ react: "FluidShared.React",
2933
+ "react-dom": "FluidShared.ReactDOM",
2934
+ "react/jsx-runtime": "FluidShared.ReactJsxRuntime",
2935
+ },
2936
+ },
2937
+ },
2938
+ };
2939
+ }
2940
+
2941
+ function fluidWidgetBuildInvariantsPlugin() {
2942
+ return {
2943
+ name: "fluid-widget-build-invariants",
2944
+ enforce: "post",
2945
+ config() {
2946
+ return { build: createFluidWidgetBuildInvariants() };
2947
+ },
2948
+ configResolved(config) {
2949
+ Object.assign(config.build, createFluidWidgetBuildInvariants());
2950
+ },
2951
+ };
2952
+ }
2953
+
2954
+ return {
2955
+ ${configFileSource} publicDir: false,
2956
+ resolve: {
2957
+ conditions: ["fluid-widget-authoring"],
2958
+ },
2959
+ plugins: [fluidWidgetBuildInvariantsPlugin()],
2960
+ build: createFluidWidgetBuildInvariants(),
2961
+ };
2962
+ })()`;
2963
+ }
2964
+ async function validateWidgetPackageOutputPaths(projectDir, publishDir) {
2965
+ validateSharedWidgetPackagePublishDir(projectDir, publishDir);
2966
+ await rejectSymlinkedPathSegments({
2967
+ projectDir,
2968
+ relativePath: publishDir,
2969
+ label: "publishDir"
2970
+ });
2971
+ await rejectSymlinkedPathSegments({
2972
+ projectDir,
2973
+ relativePath: TEMP_BUILD_DIR,
2974
+ label: "reserved temporary build directory"
2975
+ });
2976
+ }
2977
+ async function collectCssUrls(publishDir) {
2978
+ return (await fs.readdir(publishDir, { withFileTypes: true })).filter((entry) => entry.isFile() && isWidgetPackageCssArtifactPath(entry.name)).map((entry) => entry.name).sort();
2979
+ }
2980
+ async function rejectSymlinkedPathSegments(options) {
2981
+ const resolvedProjectDir = path.resolve(options.projectDir);
2982
+ const resolvedTargetPath = path.resolve(resolvedProjectDir, options.relativePath);
2983
+ const segments = path.relative(resolvedProjectDir, resolvedTargetPath).split(path.sep).filter(Boolean);
2984
+ let currentPath = resolvedProjectDir;
2985
+ for (const segment of segments) {
2986
+ currentPath = path.join(currentPath, segment);
2987
+ let stat;
2988
+ try {
2989
+ stat = await fs.lstat(currentPath);
2990
+ } catch (err) {
2991
+ if (isNodeError(err) && err.code === "ENOENT") return;
2992
+ throw err;
2993
+ }
2994
+ if (stat.isSymbolicLink()) throw new Error(`${options.label} must not include symbolic links.`);
2995
+ }
2996
+ }
2997
+ function normalizePublishDirForValidation(value) {
2998
+ return toPosixPath(path.normalize(value)).replace(/\/+$/, "") || ".";
2999
+ }
3000
+ function isPathWithinReservedDir(value, reservedDir) {
3001
+ return value === reservedDir || value.startsWith(`${reservedDir}/`);
3002
+ }
3003
+ function toPosixPath(value) {
3004
+ return value.split(path.sep).join("/");
3005
+ }
3006
+ function stableStringify(value) {
3007
+ return `${JSON.stringify(value, null, 2)}\n`;
3008
+ }
3009
+ function readCliVersion() {
3010
+ const startDir = path.dirname(fileURLToPath(import.meta.url));
3011
+ for (const packageJsonPath of candidatePackageJsonPaths(startDir)) try {
3012
+ const packageJson = require(packageJsonPath);
3013
+ if (packageJson.name === "@fluid-app/fluid-cli-portal" && typeof packageJson.version === "string") return packageJson.version;
3014
+ } catch {}
3015
+ return "0.0.0";
3016
+ }
3017
+ function resolveViteCliPathFromRequire(moduleRequire) {
3018
+ try {
3019
+ const packageJsonPath = moduleRequire.resolve("vite/package.json");
3020
+ const binPath = getViteBinPath(moduleRequire(packageJsonPath).bin);
3021
+ if (!binPath) return void 0;
3022
+ return path.resolve(path.dirname(packageJsonPath), binPath);
3023
+ } catch (err) {
3024
+ if (isModuleResolutionError(err)) return void 0;
3025
+ throw err;
3026
+ }
3027
+ }
3028
+ function getViteBinPath(bin) {
3029
+ if (typeof bin === "string") return bin;
3030
+ if (!isRecord$2(bin)) return void 0;
3031
+ const viteBin = bin.vite;
3032
+ return typeof viteBin === "string" ? viteBin : void 0;
3033
+ }
3034
+ function isRecord$2(value) {
3035
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3036
+ }
3037
+ function isNodeError(value) {
3038
+ return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string";
3039
+ }
3040
+ function isModuleResolutionError(value) {
3041
+ return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
3042
+ }
3043
+ function candidatePackageJsonPaths(startDir) {
3044
+ const paths = [];
3045
+ let current = startDir;
3046
+ while (true) {
3047
+ paths.push(path.join(current, "package.json"));
3048
+ const parent = path.dirname(current);
3049
+ if (parent === current) return paths;
3050
+ current = parent;
3051
+ }
3052
+ }
3053
+ //#endregion
3054
+ //#region src/utils/widget-package-upload.ts
3055
+ var WidgetPackageUploadError = class extends Error {
3056
+ code;
3057
+ constructor(code, message) {
3058
+ super(message);
3059
+ this.name = "WidgetPackageUploadError";
3060
+ this.code = code;
3061
+ }
3062
+ };
3063
+ const SESSION_ARRAY_KEYS = [
3064
+ "uploads",
3065
+ "artifacts",
3066
+ "signedUploads",
3067
+ "signed_uploads",
3068
+ "signedUrls",
3069
+ "signed_urls",
3070
+ "uploadUrls",
3071
+ "upload_urls"
3072
+ ];
3073
+ const SESSION_RECORD_KEYS = [
3074
+ "signedUrls",
3075
+ "signed_urls",
3076
+ "uploadUrls",
3077
+ "upload_urls"
3078
+ ];
3079
+ function buildWidgetPackageArtifactPayload(artifact) {
3080
+ return {
3081
+ path: artifact.path,
3082
+ sha256: artifact.sha256,
3083
+ bytes: artifact.bytes,
3084
+ contentType: artifact.contentType
3085
+ };
3086
+ }
3087
+ function buildWidgetPackageUploadSessionPayload(request) {
3088
+ return {
3089
+ version: request.version,
3090
+ package_key: request.packageKey,
3091
+ builder_version: request.builderVersion,
3092
+ cli_version: request.cliVersion,
3093
+ runtime_version: request.runtimeVersion,
3094
+ manifest_hash: request.manifestHash,
3095
+ manifest: request.manifest,
3096
+ artifacts: request.artifacts.map(buildWidgetPackageArtifactPayload)
3097
+ };
3098
+ }
3099
+ function buildWidgetPackageCompleteUploadPayload(packageKey, artifacts) {
3100
+ return {
3101
+ package_key: packageKey,
3102
+ artifacts: artifacts.map(buildWidgetPackageArtifactPayload)
3103
+ };
3104
+ }
3105
+ function getWidgetPackageUploadSessionRoute(owner) {
3106
+ switch (owner.kind) {
3107
+ case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions`;
3108
+ case "company": return "/api/company/fluid_os/widget_package_versions";
3109
+ }
3110
+ }
3111
+ function getWidgetPackageCompleteUploadRoute(owner, version) {
3112
+ switch (owner.kind) {
3113
+ case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
3114
+ case "company": return `/api/company/fluid_os/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
3115
+ }
3116
+ }
3117
+ async function requestWidgetPackageUploadSession(client, owner, request) {
3118
+ const payload = buildWidgetPackageUploadSessionPayload(request);
3119
+ try {
3120
+ 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);
3121
+ } catch (err) {
3122
+ if (err instanceof WidgetPackageUploadError) throw err;
3123
+ throw new WidgetPackageUploadError("SESSION_REQUEST_FAILED", `Failed to create widget package upload session: ${formatUnknownError(err)}`);
3124
+ }
3125
+ }
3126
+ async function uploadWidgetPackageArtifacts(session, artifacts, fetchImpl = fetch) {
3127
+ const uploadsByPath = new Map(session.uploads.map((upload) => [upload.path, upload]));
3128
+ const results = [];
3129
+ for (const artifact of artifacts) {
3130
+ const upload = uploadsByPath.get(artifact.path);
3131
+ if (!upload) throw new WidgetPackageUploadError("MISSING_SIGNED_URL", `Upload session did not include a signed URL for artifact "${artifact.path}".`);
3132
+ 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}".`);
3133
+ let response;
3134
+ try {
3135
+ response = await fetchImpl(upload.url, {
3136
+ method: "PUT",
3137
+ headers: mergeUploadHeaders(upload.headers, upload.contentType),
3138
+ body: artifact.body
3139
+ });
3140
+ } catch (err) {
3141
+ throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": ${formatUnknownError(err)}`);
3142
+ }
3143
+ if (!response.ok) {
3144
+ const body = await response.text().catch(() => "");
3145
+ const details = body.trim() ? `: ${body.trim().slice(0, 500)}` : "";
3146
+ throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": PUT returned HTTP ${response.status}${details}`);
3147
+ }
3148
+ results.push({
3149
+ path: artifact.path,
3150
+ status: response.status,
3151
+ contentType: upload.contentType
3152
+ });
3153
+ }
3154
+ return results;
3155
+ }
3156
+ async function completeWidgetPackageUpload(client, owner, version, packageKey, artifacts) {
3157
+ const payload = buildWidgetPackageCompleteUploadPayload(packageKey, artifacts);
3158
+ try {
3159
+ return normalizeWidgetPackageCompleteResponse(owner.kind === "company" ? await fluid_os_v0_complete_widget_package_version_upload(client, version, payload) : await client.post(getWidgetPackageCompleteUploadRoute(owner, version), payload), version);
3160
+ } catch (err) {
3161
+ if (err instanceof WidgetPackageUploadError) throw err;
3162
+ throw new WidgetPackageUploadError("COMPLETE_UPLOAD_FAILED", `Failed to complete widget package upload: ${formatUnknownError(err)}`);
3163
+ }
3164
+ }
3165
+ async function publishWidgetPackageVersion(options) {
3166
+ const sessionPayload = buildWidgetPackageUploadSessionPayload(options.request);
3167
+ if (options.dryRun) return {
3168
+ dryRun: true,
3169
+ sessionPayload
3170
+ };
3171
+ if (!options.client) throw new Error("A FetchClient is required when dryRun is false.");
3172
+ const session = await requestWidgetPackageUploadSession(options.client, options.owner, options.request);
3173
+ return {
3174
+ dryRun: false,
3175
+ sessionPayload,
3176
+ session,
3177
+ uploadedArtifacts: await uploadWidgetPackageArtifacts(session, options.request.artifacts, options.fetchImpl),
3178
+ complete: await completeWidgetPackageUpload(options.client, options.owner, session.version, options.request.packageKey, options.request.artifacts)
3179
+ };
3180
+ }
3181
+ function normalizeWidgetPackageUploadSession(response, requestedVersion, requestedArtifacts) {
3182
+ const record = requireRecord(response, "Upload session response must be a JSON object.");
3183
+ const responseVersion = getResponseVersion(record);
3184
+ if (responseVersion && responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
3185
+ const version = responseVersion ?? requestedVersion;
3186
+ const nestedRecord = getNestedVersionRecord(record);
3187
+ const uploads = getSignedUploads(nestedRecord ? [record, nestedRecord] : [record], requestedArtifacts);
3188
+ const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !uploads.some((upload) => upload.path === artifactPath));
3189
+ if (missingUploadPaths.length > 0) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response is missing signed URLs for artifact(s): ${missingUploadPaths.join(", ")}.`);
3190
+ return {
3191
+ version,
3192
+ uploads,
3193
+ raw: response
3194
+ };
3195
+ }
3196
+ function normalizeWidgetPackageCompleteResponse(response, requestedVersion) {
3197
+ if (response == null) return {
3198
+ version: requestedVersion,
3199
+ raw: response
3200
+ };
3201
+ const record = requireRecord(response, "Complete upload response must be a JSON object or empty response.");
3202
+ const nested = getNestedVersionRecord(record) ?? record;
3203
+ const responseVersion = getResponseVersion(nested);
3204
+ if (responseVersion && responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
3205
+ const packageKey = getStringProperty(nested, "package_key") ?? getStringProperty(nested, "packageKey") ?? getStringProperty(nested, "packageId");
3206
+ const status = getStringProperty(nested, "status") ?? getStringProperty(nested, "state");
3207
+ return {
3208
+ version: responseVersion ?? requestedVersion,
3209
+ ...status ? { status } : {},
3210
+ ...packageKey ? { packageKey } : {},
3211
+ raw: response
3212
+ };
3213
+ }
3214
+ function getSignedUploads(records, requestedArtifacts) {
3215
+ const uploads = [];
3216
+ for (const record of records) {
3217
+ for (const key of SESSION_ARRAY_KEYS) {
3218
+ const value = record[key];
3219
+ if (!Array.isArray(value)) continue;
3220
+ for (const item of value) {
3221
+ const upload = signedUploadFromRecord(item, requestedArtifacts);
3222
+ if (upload) uploads.push(upload);
3223
+ }
3224
+ }
3225
+ for (const key of SESSION_RECORD_KEYS) {
3226
+ const value = record[key];
3227
+ if (!isRecord$1(value) || Array.isArray(value)) continue;
3228
+ for (const [artifactPath, uploadValue] of Object.entries(value)) {
3229
+ const upload = signedUploadFromKeyedValue(artifactPath, uploadValue, requestedArtifacts);
3230
+ if (upload) uploads.push(upload);
3231
+ }
3232
+ }
3233
+ }
3234
+ return dedupeUploads(uploads);
3235
+ }
3236
+ function signedUploadFromRecord(value, requestedArtifacts) {
3237
+ if (!isRecord$1(value)) return null;
3238
+ const artifactPath = readFirstString(value, [
3239
+ "path",
3240
+ "key",
3241
+ "artifactPath",
3242
+ "artifact_path",
3243
+ "artifactKey",
3244
+ "artifact_key",
3245
+ "name"
3246
+ ]);
3247
+ const url = readFirstString(value, [
3248
+ "url",
3249
+ "signedUrl",
3250
+ "signed_url",
3251
+ "uploadUrl",
3252
+ "upload_url"
3253
+ ]);
3254
+ if (!artifactPath || !url) return null;
3255
+ const requestedArtifact = findRequestedArtifact(requestedArtifacts, artifactPath);
3256
+ const contentType = readFirstString(value, [
3257
+ "contentType",
3258
+ "content_type",
3259
+ "expectedContentType",
3260
+ "expected_content_type"
3261
+ ]) ?? requestedArtifact?.contentType;
3262
+ if (!contentType) return null;
3263
+ return {
3264
+ path: artifactPath,
3265
+ url,
3266
+ contentType,
3267
+ headers: getHeaders(value["headers"])
3268
+ };
3269
+ }
3270
+ function signedUploadFromKeyedValue(artifactPath, value, requestedArtifacts) {
3271
+ const requestedArtifact = findRequestedArtifact(requestedArtifacts, artifactPath);
3272
+ if (typeof value === "string") {
3273
+ if (!requestedArtifact) return null;
3274
+ return {
3275
+ path: artifactPath,
3276
+ url: value,
3277
+ contentType: requestedArtifact.contentType,
3278
+ headers: {}
3279
+ };
3280
+ }
3281
+ if (!isRecord$1(value)) return null;
3282
+ const url = readFirstString(value, [
3283
+ "url",
3284
+ "signedUrl",
3285
+ "signed_url",
3286
+ "uploadUrl",
3287
+ "upload_url"
3288
+ ]);
3289
+ if (!url) return null;
3290
+ const contentType = readFirstString(value, [
3291
+ "contentType",
3292
+ "content_type",
3293
+ "expectedContentType",
3294
+ "expected_content_type"
3295
+ ]) ?? requestedArtifact?.contentType;
3296
+ if (!contentType) return null;
3297
+ return {
3298
+ path: artifactPath,
3299
+ url,
3300
+ contentType,
3301
+ headers: getHeaders(value["headers"])
3302
+ };
3303
+ }
3304
+ function normalizeHeaderName(name) {
3305
+ return name.toLowerCase();
3306
+ }
3307
+ function mergeUploadHeaders(headers, contentType) {
3308
+ const merged = {};
3309
+ for (const [key, value] of Object.entries(headers)) {
3310
+ if (normalizeHeaderName(key) === "content-type") continue;
3311
+ merged[key] = value;
3312
+ }
3313
+ merged["Content-Type"] = contentType;
3314
+ return merged;
3315
+ }
3316
+ function dedupeUploads(uploads) {
3317
+ const deduped = /* @__PURE__ */ new Map();
3318
+ for (const upload of uploads) deduped.set(upload.path, upload);
3319
+ return Array.from(deduped.values());
3320
+ }
3321
+ function findRequestedArtifact(requestedArtifacts, artifactPath) {
3322
+ return requestedArtifacts.find((artifact) => artifact.path === artifactPath);
3323
+ }
3324
+ function requireRecord(value, message) {
3325
+ if (!isRecord$1(value) || Array.isArray(value)) throw new WidgetPackageUploadError("INVALID_RESPONSE", message);
3326
+ return value;
3327
+ }
3328
+ function isRecord$1(value) {
3329
+ return typeof value === "object" && value !== null;
3330
+ }
3331
+ function getStringProperty(record, key) {
3332
+ const value = record[key];
3333
+ if (typeof value === "string" && value.trim().length > 0) return value;
3334
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
3335
+ }
3336
+ function readFirstString(record, keys) {
3337
+ for (const key of keys) {
3338
+ const value = getStringProperty(record, key);
3339
+ if (value) return value;
3340
+ }
3341
+ }
3342
+ function getHeaders(value) {
3343
+ if (!isRecord$1(value) || Array.isArray(value)) return {};
3344
+ const headers = {};
3345
+ for (const [key, headerValue] of Object.entries(value)) if (typeof headerValue === "string") headers[key] = headerValue;
3346
+ return headers;
3347
+ }
3348
+ function getResponseVersion(record) {
3349
+ const directVersion = getStringProperty(record, "version");
3350
+ if (directVersion) return directVersion;
3351
+ const nested = getNestedVersionRecord(record);
3352
+ return nested ? getStringProperty(nested, "version") : void 0;
3353
+ }
3354
+ function getNestedVersionRecord(record) {
3355
+ const candidates = [
3356
+ record["portal_widget_package_version"],
3357
+ record["widget_package_version"],
3358
+ record["package_version"]
3359
+ ];
3360
+ for (const candidate of candidates) if (isRecord$1(candidate) && !Array.isArray(candidate)) return candidate;
3361
+ }
3362
+ function formatUnknownError(err) {
3363
+ const base = err instanceof Error ? err.message : String(err);
3364
+ if (isRecord$1(err) && "data" in err) return `${base} — ${JSON.stringify(err.data)}`;
3365
+ return base;
3366
+ }
3367
+ //#endregion
3368
+ //#region src/commands/widget-package-publish.ts
3369
+ async function publishWidgetRuntimeArtifacts(options, dependencies = {}) {
3370
+ const buildPackage = dependencies.buildSharedWidgetPackage ?? buildSharedWidgetPackage;
3371
+ const publishVersion = dependencies.publishWidgetPackageVersion ?? publishWidgetPackageVersion;
3372
+ const isDryRun = options.dryRun === true;
3373
+ const client = options.client;
3374
+ if (!isDryRun && !client) throw new Error("A FetchClient is required when dryRun is false.");
3375
+ const safeOutDir = validateWidgetPublishOutDir(options.projectDir, options.outDir);
3376
+ const buildResult = await buildPackage({
3377
+ projectDir: options.projectDir,
3378
+ publishDir: safeOutDir,
3379
+ owner: options.buildOwner
3380
+ });
3381
+ if (!buildResult.success) {
3382
+ const details = buildResult.error.details ? `\n${buildResult.error.details}` : "";
3383
+ throw new Error(`${buildResult.error.message}${details}`);
3384
+ }
3385
+ const request = await createWidgetPackagePublishRequest({
3386
+ publishDir: buildResult.value.publishDir,
3387
+ manifestPath: buildResult.value.manifestPath,
3388
+ publishManifestPath: buildResult.value.publishManifestPath,
3389
+ owner: options.buildOwner
3390
+ });
3391
+ let publish;
3392
+ if (isDryRun) publish = await publishVersion({
3393
+ owner: options.uploadOwner,
3394
+ request,
3395
+ dryRun: true
3396
+ });
3397
+ else {
3398
+ if (!client) throw new Error("A FetchClient is required when dryRun is false.");
3399
+ publish = await publishVersion({
3400
+ client,
3401
+ owner: options.uploadOwner,
3402
+ request
3403
+ });
3404
+ }
3405
+ return {
3406
+ build: buildResult.value,
3407
+ request,
3408
+ publish
3409
+ };
3410
+ }
3411
+ async function createWidgetPackagePublishRequest(options) {
3412
+ const manifestContent = await fs.readFile(options.manifestPath);
3413
+ const manifest = parseJsonRecord(manifestContent.toString("utf-8"), options.manifestPath);
3414
+ const publishManifest = await readPublishManifest(options.publishManifestPath);
3415
+ assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
3416
+ assertMetadataMatches({
3417
+ label: "manifestHash",
3418
+ path: "manifest.json",
3419
+ expected: publishManifest.manifestHash,
3420
+ actual: sha256(manifestContent)
3421
+ });
3422
+ const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
3423
+ assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
3424
+ const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
3425
+ return {
3426
+ version: publishManifest.version,
3427
+ packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
3428
+ builderVersion: publishManifest.builderVersion,
3429
+ cliVersion: publishManifest.cliVersion,
3430
+ runtimeVersion: publishManifest.runtimeVersion,
3431
+ manifestHash: publishManifest.manifestHash,
3432
+ manifest,
3433
+ artifacts
3434
+ };
3435
+ }
3436
+ function createAuthenticatedWidgetPackageClient() {
3437
+ const { profile, profileName, token } = resolveAuthenticatedProfile();
3438
+ if (!token) {
3439
+ if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
3440
+ throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
3441
+ }
3442
+ return createFetchClient({
3443
+ baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
3444
+ getAuthToken: () => token
3445
+ });
3446
+ }
3447
+ function resolveAuthenticatedProfile() {
3448
+ const config = readConfig();
3449
+ const projectConfig = findProjectConfig(process.cwd());
3450
+ if (projectConfig?.profile) {
3451
+ const profile = config.profiles[projectConfig.profile];
3452
+ if (profile) return {
3453
+ profile,
3454
+ profileName: projectConfig.profile,
3455
+ token: profile.token
3456
+ };
3457
+ }
3458
+ if (!config.activeProfile) return {};
3459
+ const profile = config.profiles[config.activeProfile];
3460
+ if (!profile) return {};
3461
+ return {
3462
+ profile,
3463
+ profileName: config.activeProfile,
3464
+ token: profile.token
3465
+ };
3466
+ }
3467
+ function printWidgetPublishSummary(options) {
3468
+ console.log();
3469
+ console.log(chalk.green.bold(`${options.title} complete`));
3470
+ console.log();
3471
+ console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
3472
+ if (options.environment) console.log(chalk.gray("Environment: ") + chalk.white(options.environment));
3473
+ console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
3474
+ console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
3475
+ console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
3476
+ console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
3477
+ console.log();
3478
+ }
3479
+ function printRuntimeOnlyNotice() {
3480
+ console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
3481
+ console.log();
3482
+ }
3483
+ function printDryRunSessionPayload(result) {
3484
+ console.log(chalk.bold("Dry-run upload session payload:"));
3485
+ console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
3486
+ console.log();
3487
+ }
3488
+ function validateWidgetPublishOutDir(projectDir, outDir) {
3489
+ validateSharedWidgetPackagePublishDir(projectDir, outDir, { optionName: "--out-dir" });
3490
+ return outDir;
3491
+ }
3492
+ async function readJsonRecord(filePath) {
3493
+ return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
3494
+ }
3495
+ function parseJsonRecord(content, filePath) {
3496
+ const value = JSON.parse(content);
3497
+ if (!isRecord(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
3498
+ return value;
3499
+ }
3500
+ async function readPublishManifest(filePath) {
3501
+ const record = await readJsonRecord(filePath);
3502
+ const artifactsValue = record["artifacts"];
3503
+ if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
3504
+ return {
3505
+ packageId: readRequiredString(record, "packageId", filePath),
3506
+ version: readRequiredString(record, "version", filePath),
3507
+ builderVersion: readRequiredString(record, "builderVersion", filePath),
3508
+ cliVersion: readRequiredString(record, "cliVersion", filePath),
3509
+ runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
3510
+ manifestHash: readRequiredString(record, "manifestHash", filePath),
3511
+ artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
3512
+ };
3513
+ }
3514
+ function readArtifactMetadata(value, index) {
3515
+ if (!isRecord(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
3516
+ return {
3517
+ path: readRequiredString(value, "path", "publish-manifest.json"),
3518
+ sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
3519
+ bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
3520
+ contentType: readRequiredString(value, "contentType", "publish-manifest.json")
3521
+ };
3522
+ }
3523
+ async function readUploadArtifacts(publishDir, artifacts) {
3524
+ const resolvedPublishDir = path.resolve(publishDir);
3525
+ return Promise.all(artifacts.map(async (artifact) => {
3526
+ const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
3527
+ const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
3528
+ assertArtifactMetadataMatches(artifact, actual);
3529
+ return {
3530
+ ...actual,
3531
+ body: await fs.readFile(artifactPath)
3532
+ };
3533
+ }));
3534
+ }
3535
+ function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
3536
+ assertMetadataMatches({
3537
+ label: "packageId",
3538
+ path: "manifest.json",
3539
+ expected: publishManifest.packageId,
3540
+ actual: readRequiredString(manifest, "packageId", manifestPath)
3541
+ });
3542
+ assertMetadataMatches({
3543
+ label: "version",
3544
+ path: "manifest.json",
3545
+ expected: publishManifest.version,
3546
+ actual: readRequiredString(manifest, "version", manifestPath)
3547
+ });
3548
+ }
3549
+ function assertArtifactInventoryMatches(expected, actual) {
3550
+ const expectedByPath = /* @__PURE__ */ new Map();
3551
+ for (const artifact of expected) {
3552
+ 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.`);
3553
+ expectedByPath.set(artifact.path, artifact);
3554
+ }
3555
+ const actualPaths = /* @__PURE__ */ new Set();
3556
+ for (const actualArtifact of actual) {
3557
+ actualPaths.add(actualArtifact.path);
3558
+ const expectedArtifact = expectedByPath.get(actualArtifact.path);
3559
+ if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
3560
+ assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
3561
+ }
3562
+ for (const expectedArtifact of expected) {
3563
+ if (actualPaths.has(expectedArtifact.path)) continue;
3564
+ 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.`);
3565
+ }
3566
+ }
3567
+ function assertArtifactMetadataMatches(expected, actual) {
3568
+ assertMetadataMatches({
3569
+ label: "path",
3570
+ path: expected.path,
3571
+ expected: expected.path,
3572
+ actual: actual.path
3573
+ });
3574
+ assertMetadataMatches({
3575
+ label: "sha256",
3576
+ path: expected.path,
3577
+ expected: expected.sha256,
3578
+ actual: actual.sha256
3579
+ });
3580
+ assertMetadataMatches({
3581
+ label: "bytes",
3582
+ path: expected.path,
3583
+ expected: expected.bytes,
3584
+ actual: actual.bytes
3585
+ });
3586
+ assertMetadataMatches({
3587
+ label: "contentType",
3588
+ path: expected.path,
3589
+ expected: expected.contentType,
3590
+ actual: actual.contentType
3591
+ });
3592
+ }
3593
+ function assertMetadataMatches(options) {
3594
+ if (options.expected === options.actual) return;
3595
+ 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.`);
3596
+ }
3597
+ function resolveArtifactPath(publishDir, artifactPath) {
3598
+ const resolved = path.resolve(publishDir, artifactPath);
3599
+ const relative = path.relative(publishDir, resolved);
3600
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
3601
+ return resolved;
3602
+ }
3603
+ function stripOwnerPrefix(packageId, owner) {
3604
+ const prefix = `${owner}.`;
3605
+ if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
3606
+ return packageId;
3607
+ }
3608
+ function readRequiredString(record, key, filePath) {
3609
+ const value = record[key];
3610
+ if (typeof value === "string" && value.trim().length > 0) return value;
3611
+ throw new Error(`${path.basename(filePath)} must include string ${key}.`);
3612
+ }
3613
+ function readRequiredNumber(record, key, filePath) {
3614
+ const value = record[key];
3615
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3616
+ throw new Error(`${path.basename(filePath)} must include number ${key}.`);
3617
+ }
3618
+ function isRecord(value) {
3619
+ return typeof value === "object" && value !== null;
3620
+ }
3621
+ //#endregion
3622
+ //#region src/commands/deploy.ts
3623
+ const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
3624
+ const DEFAULT_ENVIRONMENT = "production";
3625
+ 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) => {
3626
+ const environment = options.environment ?? DEFAULT_ENVIRONMENT;
3627
+ const outDir = options.outDir ?? DEFAULT_OUT_DIR$1;
3628
+ const dryRun = options.dryRun === true;
3629
+ console.log();
3630
+ console.log(chalk.blue.bold("Fluid Portal Deploy"));
3631
+ console.log();
3632
+ printRuntimeOnlyNotice();
3633
+ console.log(chalk.gray("Environment: ") + chalk.white(environment));
3634
+ console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
3635
+ if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
3636
+ console.log();
3637
+ const spinner = ora("Building company-owned widget package...").start();
3638
+ try {
3639
+ const result = await publishWidgetRuntimeArtifacts({
3640
+ projectDir: process.cwd(),
3641
+ outDir,
3642
+ buildOwner: "company",
3643
+ uploadOwner: { kind: "company" },
3644
+ dryRun,
3645
+ ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
3646
+ });
3647
+ spinner.succeed("Built widget runtime artifacts");
3648
+ if (dryRun) {
3649
+ console.log(chalk.yellow("Dry run complete — upload session was not requested."));
3650
+ console.log();
3651
+ printDryRunSessionPayload(result);
3652
+ } else console.log(chalk.green("Published widget package version."));
3653
+ printWidgetPublishSummary({
3654
+ title: dryRun ? "Portal deploy dry run" : "Portal deploy",
3655
+ ownerLabel: "company",
3656
+ environment,
3657
+ outDir,
3658
+ result
3659
+ });
3660
+ } catch (err) {
3661
+ spinner.fail("Portal deploy failed");
3662
+ console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
3663
+ process.exit(1);
3664
+ }
3665
+ });
3666
+ //#endregion
1813
3667
  //#region src/commands/doctor.ts
1814
3668
  /** Files that are managed by the SDK and should match the canonical template. */
1815
3669
  const INFRASTRUCTURE_FILES = [
@@ -2004,7 +3858,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
2004
3858
  console.log(chalk.bold("Creating version..."));
2005
3859
  let result;
2006
3860
  try {
2007
- result = await createFluidOSVersion(client, definitionId);
3861
+ result = await fluid_os_v0_create_fluid_osversion(client, definitionId);
2008
3862
  } catch (err) {
2009
3863
  console.error(chalk.red("Error:") + " Failed to create version — " + (err instanceof Error ? err.message : String(err)));
2010
3864
  process.exit(1);
@@ -2022,7 +3876,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
2022
3876
  if (options.activate) {
2023
3877
  console.log("Activating version...");
2024
3878
  try {
2025
- await updateFluidOSVersion(client, definitionId, version.id, { version: { active: true } });
3879
+ await fluid_os_v0_update_fluid_osversion(client, definitionId, version.id, { version: { active: true } });
2026
3880
  } catch (err) {
2027
3881
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
2028
3882
  process.exit(1);
@@ -2037,7 +3891,7 @@ const listVersionCommand = new Command("list").description("List all versions of
2037
3891
  const definitionId = await requireDefinitionId();
2038
3892
  let result;
2039
3893
  try {
2040
- result = await listFluidOSVersions(client, definitionId);
3894
+ result = await fluid_os_v0_list_fluid_osversions(client, definitionId);
2041
3895
  } catch (err) {
2042
3896
  console.error(chalk.red("Error:") + " Failed to list versions — " + (err instanceof Error ? err.message : String(err)));
2043
3897
  process.exit(1);
@@ -2080,7 +3934,7 @@ const activateVersionCommand = new Command("activate").description("Activate a s
2080
3934
  console.log();
2081
3935
  console.log(chalk.bold("Activating version..."));
2082
3936
  try {
2083
- await updateFluidOSVersion(client, definitionId, versionId, { version: { active: true } });
3937
+ await fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
2084
3938
  } catch (err) {
2085
3939
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
2086
3940
  process.exit(1);
@@ -2091,6 +3945,52 @@ const activateVersionCommand = new Command("activate").description("Activate a s
2091
3945
  });
2092
3946
  const versionCommand = new Command("version").description("Manage portal definition versions").addCommand(createVersionCommand).addCommand(listVersionCommand).addCommand(activateVersionCommand);
2093
3947
  //#endregion
3948
+ //#region src/commands/widget-publish.ts
3949
+ const DEFAULT_OUT_DIR = ".fluid/widget-dist";
3950
+ 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) => {
3951
+ const outDir = options.outDir ?? DEFAULT_OUT_DIR;
3952
+ const dryRun = options.dryRun === true;
3953
+ console.log();
3954
+ console.log(chalk.blue.bold("Fluid Widget Publish"));
3955
+ console.log();
3956
+ printRuntimeOnlyNotice();
3957
+ console.log(chalk.gray("Droplet: ") + chalk.white(options.droplet));
3958
+ console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
3959
+ if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
3960
+ console.log();
3961
+ const spinner = ora("Building droplet-owned widget package...").start();
3962
+ try {
3963
+ const result = await publishWidgetRuntimeArtifacts({
3964
+ projectDir: process.cwd(),
3965
+ outDir,
3966
+ buildOwner: "droplet",
3967
+ uploadOwner: {
3968
+ kind: "droplet",
3969
+ uuid: options.droplet
3970
+ },
3971
+ dryRun,
3972
+ ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
3973
+ });
3974
+ spinner.succeed("Built widget runtime artifacts");
3975
+ if (dryRun) {
3976
+ console.log(chalk.yellow("Dry run complete — upload session was not requested."));
3977
+ console.log();
3978
+ printDryRunSessionPayload(result);
3979
+ } else console.log(chalk.green("Published widget package version."));
3980
+ printWidgetPublishSummary({
3981
+ title: dryRun ? "Widget publish dry run" : "Widget publish",
3982
+ ownerLabel: `droplet ${options.droplet}`,
3983
+ outDir,
3984
+ result
3985
+ });
3986
+ } catch (err) {
3987
+ spinner.fail("Widget publish failed");
3988
+ console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
3989
+ process.exit(1);
3990
+ }
3991
+ });
3992
+ const topLevelWidgetCommand = new Command("widget").description("Publish Fluid widget packages").addCommand(publishSubcommand);
3993
+ //#endregion
2094
3994
  //#region src/index.ts
2095
3995
  /**
2096
3996
  * @fluid-app/fluid-cli-portal
@@ -2108,6 +4008,7 @@ const plugin = {
2108
4008
  portal.addCommand(buildCommand);
2109
4009
  portal.addCommand(pullCommand);
2110
4010
  portal.addCommand(pushCommand);
4011
+ portal.addCommand(deployCommand);
2111
4012
  portal.addCommand(widgetCommand);
2112
4013
  portal.addCommand(doctorCommand);
2113
4014
  portal.addCommand(versionCommand);
@@ -2115,6 +4016,6 @@ const plugin = {
2115
4016
  }
2116
4017
  };
2117
4018
  //#endregion
2118
- export { FILE_SYSTEM_ERRORS, TEMPLATES, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, plugin as default, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, fileExists, getCoreVersion, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, installDependencies, pathExists, promptProjectConfig, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolveSlugToId, runPackageManager, slugFromPath, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeMappings, writeSnapshot };
4019
+ export { FILE_SYSTEM_ERRORS, TEMPLATES, WIDGET_PACKAGE_BUILDER_VERSION, WidgetPackageUploadError, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSharedWidgetPackage, buildSnapshot, buildThemeIdToSlugMap, buildWidgetPackageCompleteUploadPayload, buildWidgetPackageDescriptor, buildWidgetPackageUploadSessionPayload, categorizeChanges, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPublishManifestMetadata, createWidgetPackageEntrySource, createWidgetPackagePublishRequest, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, fileExists, getArtifactContentType, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, getWidgetPackageCompleteUploadRoute, getWidgetPackageUploadSessionRoute, installDependencies, loadSourceWidgetPackages, normalizeWidgetPackageCompleteResponse, normalizeWidgetPackageUploadSession, pathExists, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, promptProjectConfig, publishWidgetPackageVersion, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, requestWidgetPackageUploadSession, resolveIdToSlug, resolvePortalWidgetSourceConfig, resolveSlugToId, runPackageManager, sha256, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, uploadWidgetPackageArtifacts, validateCrossReferences, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir, versionCommand, widgetCommand, writeFileSafe, writeMappings, writeSnapshot };
2119
4020
 
2120
4021
  //# sourceMappingURL=index.mjs.map