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