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