@fluid-app/fluid-cli-portal 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as fluid_os_v0_create_fluid_osversion, B as fluid_os_v0_update_fluid_osnavigation, 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 fluid_os_v0_update_fluid_osversion, H as fluid_os_v0_update_fluid_osprofile, I as fluid_os_v0_delete_fluid_ostheme, K as createFetchClient, L as fluid_os_v0_list_fluid_osdefinitions, 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_osnavigation_items, S as updateMapping, T as fluid_os_v0_create_fluid_osnavigation, U as fluid_os_v0_update_fluid_osscreen, V as fluid_os_v0_update_fluid_osnavigation_item, W as fluid_os_v0_update_fluid_ostheme, _ 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_list_fluid_osversions } from "./pull-CG5dELOX.mjs";
1
+ import { C as writeMappings, S as updateMapping, _ 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, 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, x as resolveSlugToId, y as removeMapping } from "./pull-Cl7omGLf.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "commander";
4
4
  import chalk from "chalk";
@@ -12,9 +12,10 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileS
12
12
  import Handlebars from "handlebars";
13
13
  import { execa } from "execa";
14
14
  import os from "node:os";
15
- import fs from "fs-extra";
16
- import path$1 from "path";
15
+ import { createFetchClient, fluidOs } from "@fluid-app/fluidos-api-client";
17
16
  import { createHash } from "node:crypto";
17
+ import fs from "fs-extra";
18
+ import { isJsonValue } from "@fluid-app/portal-core/remote-dom-widget-package";
18
19
  //#region src/types.ts
19
20
  /**
20
21
  * Available project templates
@@ -699,11 +700,14 @@ async function gitRemoteBranchExists(cwd, remoteUrl, branch = "main") {
699
700
  /**
700
701
  * Adopt `FETCH_HEAD` as the current (unborn) branch's base: point the
701
702
  * branch ref at it and load its tree into the index, leaving the working
702
- * tree untouched. The next auto-commit then records the local project
703
- * tree as a child of the remote history — local files win, remote-only
704
- * files are recorded as deletions — so the push fast-forwards instead of
705
- * being rejected for unrelated history. Only call when `hasCommits` is
706
- * false; with local commits it would silently rewrite the branch ref.
703
+ * tree untouched. The next auto-commit (run with `ignoreRemovals`) then
704
+ * records the local project tree on top of the remote history — local
705
+ * additions/edits win, and remote-only files survive because a
706
+ * content-only local dir (e.g. populated by `fluid portal pull`) is no
707
+ * statement about what the full repo tracks — so the push fast-forwards
708
+ * instead of being rejected for unrelated history. Only call when
709
+ * `hasCommits` is false; with local commits it would silently rewrite
710
+ * the branch ref.
707
711
  * Mirrors the widget CLI's first-push adoption (the backend provisions
708
712
  * repos with an auto-init/template commit, so a fresh root commit is
709
713
  * always unrelated history).
@@ -780,7 +784,9 @@ async function gitMergeFastForward(cwd) {
780
784
  }
781
785
  }
782
786
  /**
783
- * `git add -A && git commit -m <message>`.
787
+ * `git add -A && git commit -m <message>` (or, with
788
+ * `{ ignoreRemovals: true }`, `git add --ignore-removal .` — see
789
+ * GitAutoCommitOptions).
784
790
  *
785
791
  * `git add -A` intentionally stages the whole work tree: a portal's
786
792
  * provisioned repo tracks the entire project (it's seeded from the
@@ -794,9 +800,14 @@ async function gitMergeFastForward(cwd) {
794
800
  * identity so the commit isn't rejected. "nothing to commit" is treated
795
801
  * as a no-op, not an error.
796
802
  */
797
- async function gitAutoCommit(cwd, message) {
803
+ async function gitAutoCommit(cwd, message, options = {}) {
804
+ const addArgs = options.ignoreRemovals ? [
805
+ "add",
806
+ "--ignore-removal",
807
+ "."
808
+ ] : ["add", "-A"];
798
809
  try {
799
- await execa("git", ["add", "-A"], { cwd });
810
+ await execa("git", addArgs, { cwd });
800
811
  } catch (err) {
801
812
  throw toGitError(err, "add");
802
813
  }
@@ -819,7 +830,7 @@ async function gitAutoCommit(cwd, message) {
819
830
  cwd,
820
831
  stdio: [
821
832
  "ignore",
822
- "inherit",
833
+ "pipe",
823
834
  "pipe"
824
835
  ]
825
836
  });
@@ -828,7 +839,7 @@ async function gitAutoCommit(cwd, message) {
828
839
  if (missing) throw missing;
829
840
  const e = err;
830
841
  const streams = `${typeof e.stdout === "string" ? e.stdout : ""}\n${typeof e.stderr === "string" ? e.stderr : ""}`;
831
- if (/nothing to commit/i.test(streams)) return;
842
+ if (/nothing to commit|no changes added to commit/i.test(streams)) return;
832
843
  throw toGitError(err, "commit");
833
844
  }
834
845
  }
@@ -1149,7 +1160,7 @@ async function autoPull(cwd) {
1149
1160
  console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
1150
1161
  console.log();
1151
1162
  try {
1152
- const { pullCommand } = await import("./pull-CG5dELOX.mjs").then((n) => n.n);
1163
+ const { pullCommand } = await import("./pull-Cl7omGLf.mjs").then((n) => n.n);
1153
1164
  await pullCommand.parseAsync([], { from: "user" });
1154
1165
  return hasPortalContent(cwd);
1155
1166
  } catch (err) {
@@ -1204,615 +1215,173 @@ const devCommand = new Command("dev").description("Start the development server
1204
1215
  }
1205
1216
  });
1206
1217
  //#endregion
1207
- //#region src/utils/widget-package-config.ts
1208
- const CONFIG_CANDIDATES = [
1209
- "src/widgets.config.ts",
1210
- "src/portal.config.ts",
1211
- "portal.config.ts"
1212
- ];
1213
- const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
1214
- const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
1215
- const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
1216
- const FLUID_WIDGET_AUTHORING_NODE_CONDITIONS = ["--conditions=fluid-widget-authoring", "--conditions=production"];
1217
- const TSX_CLI_PATH$1 = createRequire(import.meta.url).resolve("tsx/cli");
1218
- function withFluidWidgetAuthoringNodeCondition(nodeOptions) {
1219
- const existingOptions = nodeOptions?.trim();
1220
- const options = existingOptions ? existingOptions.split(/\s+/) : [];
1221
- const optionSet = new Set(options);
1222
- for (const condition of FLUID_WIDGET_AUTHORING_NODE_CONDITIONS) if (!optionSet.has(condition)) options.push(condition);
1223
- return options.join(" ");
1224
- }
1225
- async function resolvePortalWidgetSourceConfig(projectDir) {
1226
- for (const relativePath of CONFIG_CANDIDATES) {
1227
- const candidate = path.join(projectDir, relativePath);
1228
- if (await fs.pathExists(candidate)) return {
1229
- path: candidate,
1230
- relativePath
1231
- };
1218
+ //#region src/commands/build.ts
1219
+ const buildCommand = new Command("build").description("Build the application for production").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
1220
+ const cwd = process.cwd();
1221
+ if (!existsSync(join(cwd, "package.json"))) {
1222
+ console.error(chalk.red("Error: No package.json found in current directory"));
1223
+ console.error(chalk.yellow("Make sure you're in a Fluid project directory"));
1224
+ process.exit(1);
1232
1225
  }
1233
- }
1234
- async function loadSourceWidgetPackages(projectDir) {
1235
- const config = await resolvePortalWidgetSourceConfig(projectDir);
1236
- if (!config) return success([]);
1237
- let tempDir;
1226
+ if (!existsSync(join(cwd, "vite.config.ts"))) {
1227
+ console.error(chalk.red("Error: No vite.config.ts found"));
1228
+ console.error(chalk.yellow("This command must be run from a Fluid project directory"));
1229
+ process.exit(1);
1230
+ }
1231
+ console.log();
1232
+ console.log(chalk.bold("Building for production..."));
1233
+ console.log();
1234
+ const spinner = ora("Building...").start();
1238
1235
  try {
1239
- tempDir = await createTempDirectory$1(projectDir);
1240
- const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
1241
- const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
1242
- await fs.writeFile(extractFile, createSourcePackageExtractorScript({
1243
- projectDir,
1244
- configPath: config.path
1245
- }), {
1246
- encoding: "utf-8",
1247
- flag: "wx"
1248
- });
1249
- await execa(process.execPath, [
1250
- TSX_CLI_PATH$1,
1251
- extractFile,
1252
- outputFile
1253
- ], {
1254
- cwd: projectDir,
1255
- stdio: "pipe",
1256
- env: {
1257
- ...process.env,
1258
- NODE_ENV: "production",
1259
- NODE_OPTIONS: withFluidWidgetAuthoringNodeCondition(process.env.NODE_OPTIONS)
1260
- }
1261
- });
1262
- const outputResult = await readSourcePackageExtractorOutput(outputFile);
1263
- if (!outputResult.success) return outputResult;
1264
- const parsed = outputResult.value;
1265
- if (parsed.length === 0) return success([]);
1266
- return success(parsed);
1267
- } catch (err) {
1268
- const error = err;
1269
- return failure({
1270
- code: "CONFIG_LOAD_FAILED",
1271
- message: `Failed to load widget packages from ${config.relativePath}`,
1272
- details: error.stderr ?? error.message ?? String(err)
1236
+ await execa("pnpm", ["run", "build"], {
1237
+ cwd,
1238
+ stdio: "pipe"
1273
1239
  });
1274
- } finally {
1275
- if (tempDir) await fs.remove(tempDir).catch(() => {});
1240
+ spinner.succeed("Build completed");
1241
+ const outDir = options.outDir ?? "dist";
1242
+ console.log();
1243
+ console.log(`Output written to ${chalk.cyan(outDir)}/`);
1244
+ console.log();
1245
+ console.log("To preview the build locally:");
1246
+ console.log(chalk.cyan(" pnpm vite preview"));
1247
+ console.log();
1248
+ } catch (error) {
1249
+ spinner.fail("Build failed");
1250
+ const execaError = error;
1251
+ if (execaError.stderr) console.error(execaError.stderr);
1252
+ process.exit(1);
1276
1253
  }
1254
+ });
1255
+ //#endregion
1256
+ //#region src/utils/push-validation.ts
1257
+ /**
1258
+ * Cross-reference validation and change categorization utilities for the push command.
1259
+ *
1260
+ * Extracted into a standalone utility so that pure logic can be tested
1261
+ * without pulling in CLI dependencies (ora, chalk, prompts, etc.).
1262
+ */
1263
+ /**
1264
+ * Extract the slug from a file path (e.g., "screens/home.json" -> "home").
1265
+ */
1266
+ function slugFromPath(filePath) {
1267
+ return basename(filePath, ".json");
1277
1268
  }
1278
- async function createTempDirectory$1(projectDir) {
1279
- const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
1280
- try {
1281
- await fs.ensureDir(projectTmpDir);
1282
- return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
1283
- } catch (err) {
1284
- const message = err instanceof Error ? err.message : String(err);
1285
- throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
1269
+ /**
1270
+ * Extract the resource type directory from a file path (e.g., "screens/home.json" -> "screens").
1271
+ */
1272
+ function resourceTypeFromPath(filePath) {
1273
+ const dir = filePath.split("/")[0];
1274
+ if (dir === "screens" || dir === "themes" || dir === "navigations" || dir === "profiles") return dir;
1275
+ return null;
1276
+ }
1277
+ /**
1278
+ * Read and parse a JSON file from the portal directory.
1279
+ */
1280
+ async function readPortalFile$1(portalDir, relativePath) {
1281
+ const content = await readFile(join(portalDir, relativePath), "utf-8");
1282
+ return JSON.parse(content);
1283
+ }
1284
+ /**
1285
+ * Categorize a snapshot diff into resource-type-specific change lists.
1286
+ */
1287
+ function categorizeChanges(diff) {
1288
+ const result = {
1289
+ screens: {
1290
+ new: [],
1291
+ changed: [],
1292
+ deleted: []
1293
+ },
1294
+ themes: {
1295
+ new: [],
1296
+ changed: [],
1297
+ deleted: []
1298
+ },
1299
+ navigations: {
1300
+ new: [],
1301
+ changed: [],
1302
+ deleted: []
1303
+ },
1304
+ profiles: {
1305
+ new: [],
1306
+ changed: [],
1307
+ deleted: []
1308
+ }
1309
+ };
1310
+ for (const file of diff.new) {
1311
+ const type = resourceTypeFromPath(file);
1312
+ if (type) result[type].new.push(file);
1313
+ }
1314
+ for (const file of diff.changed) {
1315
+ const type = resourceTypeFromPath(file);
1316
+ if (type) result[type].changed.push(file);
1317
+ }
1318
+ for (const file of diff.deleted) {
1319
+ const type = resourceTypeFromPath(file);
1320
+ if (type) result[type].deleted.push(file);
1286
1321
  }
1322
+ return result;
1287
1323
  }
1288
- async function readSourcePackageExtractorOutput(outputFile) {
1289
- let output;
1290
- try {
1291
- output = await fs.readFile(outputFile, "utf-8");
1292
- } catch (err) {
1293
- return failure({
1294
- code: "CONFIG_LOAD_FAILED",
1295
- message: "Widget package extractor did not write an output file",
1296
- details: err instanceof Error ? err.message : String(err)
1324
+ /**
1325
+ * Validate that all cross-references between local portal files are valid.
1326
+ *
1327
+ * Checks:
1328
+ * - Navigation items' "screen" slugs reference existing screen files or mappings
1329
+ * - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
1330
+ * - Profile "themes" slugs reference existing theme files or mappings
1331
+ */
1332
+ async function validateCrossReferences(portalDir, mappings, changes) {
1333
+ const errors = [];
1334
+ const validScreenSlugs = buildValidSlugsSet(portalDir, "screens", mappings);
1335
+ const validNavSlugs = buildValidSlugsSet(portalDir, "navigations", mappings);
1336
+ const validThemeSlugs = buildValidSlugsSet(portalDir, "themes", mappings);
1337
+ for (const file of changes.screens.deleted) validScreenSlugs.delete(slugFromPath(file));
1338
+ for (const file of changes.navigations.deleted) validNavSlugs.delete(slugFromPath(file));
1339
+ for (const file of changes.themes.deleted) validThemeSlugs.delete(slugFromPath(file));
1340
+ const navFilesToCheck = [...changes.navigations.new, ...changes.navigations.changed];
1341
+ for (const file of navFilesToCheck) try {
1342
+ validateNavigationItems((await readPortalFile$1(portalDir, file)).navigation_items, file, validScreenSlugs, errors);
1343
+ } catch {
1344
+ errors.push({
1345
+ file,
1346
+ message: "Failed to read navigation file"
1297
1347
  });
1298
1348
  }
1299
- let parsed;
1300
- try {
1301
- parsed = JSON.parse(output);
1349
+ const profileFilesToCheck = [...changes.profiles.new, ...changes.profiles.changed];
1350
+ for (const file of profileFilesToCheck) try {
1351
+ const profile = await readPortalFile$1(portalDir, file);
1352
+ if (profile.navigation && !validNavSlugs.has(profile.navigation)) errors.push({
1353
+ file,
1354
+ message: `References navigation "${profile.navigation}" which does not exist`
1355
+ });
1356
+ if (profile.mobile_navigation && !validNavSlugs.has(profile.mobile_navigation)) errors.push({
1357
+ file,
1358
+ message: `References mobile_navigation "${profile.mobile_navigation}" which does not exist`
1359
+ });
1360
+ for (const themeSlug of profile.themes) if (!validThemeSlugs.has(themeSlug)) errors.push({
1361
+ file,
1362
+ message: `References theme "${themeSlug}" which does not exist`
1363
+ });
1302
1364
  } catch {
1303
- return failure({
1304
- code: "INVALID_FORMAT",
1305
- message: "Failed to parse widget package output file as JSON",
1306
- details: `Output was: ${output.slice(0, 200)}`
1365
+ errors.push({
1366
+ file,
1367
+ message: "Failed to read profile file"
1307
1368
  });
1308
1369
  }
1309
- if (!isRecord$5(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
1310
- code: "INVALID_FORMAT",
1311
- message: "Widget package extractor output file had an invalid sentinel"
1312
- });
1313
- if (!Array.isArray(parsed.data)) return failure({
1314
- code: "INVALID_FORMAT",
1315
- message: "Extracted widget package output is not an array",
1316
- details: `Expected an array, got: ${typeof parsed.data}`
1317
- });
1318
- return success(parsed.data);
1370
+ return errors;
1319
1371
  }
1320
- function createSourcePackageExtractorScript(options) {
1321
- return `
1322
- import fs from "node:fs/promises";
1323
- import path from "node:path";
1324
- import { createServer, normalizePath } from "vite";
1325
-
1326
- const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
1327
- const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
1328
- const projectRoot = ${JSON.stringify(options.projectDir)};
1329
- const widgetConfigPath = ${JSON.stringify(options.configPath)};
1330
- const outputPath = process.argv[2];
1331
- if (!outputPath) throw new Error("Missing widget package extractor output path.");
1332
-
1333
- function toViteModuleId(modulePath) {
1334
- const relativePath = path.relative(projectRoot, modulePath);
1335
- if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
1336
- return "/" + normalizePath(relativePath);
1337
- }
1338
- return normalizePath(modulePath);
1339
- }
1340
-
1341
- const server = await createServer({
1342
- root: projectRoot,
1343
- mode: "production",
1344
- server: { middlewareMode: true },
1345
- appType: "custom",
1346
- logLevel: "error",
1347
- clearScreen: false,
1348
- resolve: {
1349
- conditions: ["fluid-widget-authoring", "module", "browser", "development|production"],
1350
- },
1351
- ssr: {
1352
- noExternal: ["@fluid-app/portal-sdk"],
1353
- resolve: {
1354
- conditions: ["fluid-widget-authoring", "module", "node", "development|production"],
1355
- },
1356
- },
1357
- });
1358
-
1359
- try {
1360
- const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
1361
-
1362
- function isSourceWidgetPackage(value) {
1363
- return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
1364
- }
1365
-
1366
- function collectSourceWidgetPackages(mod) {
1367
- const candidates = [
1368
- mod.widgetPackage,
1369
- ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
1370
- mod.default,
1371
- ];
1372
- const byPackageId = new Map();
1373
- for (const candidate of candidates) {
1374
- if (!isSourceWidgetPackage(candidate)) continue;
1375
- if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
1376
- }
1377
- return Array.from(byPackageId.values());
1378
- }
1379
-
1380
- function serializeWidget(widget) {
1381
- if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
1382
- return widget;
1383
- }
1384
- const { component, ...metadata } = widget;
1385
- return metadata;
1386
- }
1387
-
1388
- function serializePackage(sourcePackage) {
1389
- return {
1390
- manifestVersion: sourcePackage.manifestVersion,
1391
- scope: sourcePackage.scope,
1392
- packageStableId: sourcePackage.packageStableId,
1393
- packageId: sourcePackage.packageId,
1394
- packageType: sourcePackage.packageType,
1395
- version: sourcePackage.version,
1396
- cssUrls: sourcePackage.cssUrls,
1397
- widgets: Array.isArray(sourcePackage.widgets)
1398
- ? sourcePackage.widgets.map(serializeWidget)
1399
- : sourcePackage.widgets,
1400
- };
1401
- }
1402
-
1403
- await fs.writeFile(
1404
- outputPath,
1405
- JSON.stringify({
1406
- sentinel: OUTPUT_SENTINEL,
1407
- data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
1408
- }),
1409
- "utf-8",
1410
- );
1411
- } finally {
1412
- await server.close();
1413
- }
1414
- `;
1415
- }
1416
- function isRecord$5(value) {
1417
- return typeof value === "object" && value !== null && !Array.isArray(value);
1418
- }
1419
- //#endregion
1420
- //#region src/utils/extract-manifests.ts
1421
- /**
1422
- * Manifest extraction utility
1423
- *
1424
- * Extracts serializable widget manifest metadata from portal.config.ts
1425
- * by writing a minimal wrapper script that imports customWidgets plus
1426
- * source widget package exports and serializes the result to a temp output
1427
- * file, then running it with tsx.
1428
- *
1429
- * Strips the `component` field (not serializable) from each manifest.
1430
- *
1431
- * Writes a temp script, runs it with tsx, and parses JSON output from the
1432
- * output file. The wrapper loads config modules through Vite SSR so project
1433
- * aliases and Vite-compatible module resolution are honored.
1434
- */
1435
- const EXTRACT_FILENAME = "extract-manifests.ts";
1436
- const EXTRACT_OUTPUT_FILENAME = "manifests.json";
1437
- const EXTRACT_OUTPUT_SENTINEL = "fluid-widget-manifests:v1";
1438
- const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
1439
- /**
1440
- * Extract serializable widget manifests from a project's portal.config.ts.
1441
- *
1442
- * Writes a temp wrapper script, runs it with tsx, and parses the temp JSON
1443
- * output file. The temp files are always cleaned up.
1444
- *
1445
- * Returns an empty array if no customWidgets or source package exports exist.
1446
- *
1447
- * Supported static export shapes are intentionally simple so the CLI can avoid
1448
- * executing unrelated portal configs: named `export const|let|var customWidgets`,
1449
- * `widgetPackage`, or `widgetPackages`; direct `export default
1450
- * defineWidgetPackage(...)`; or `export default <identifier>` where that
1451
- * identifier is initialized with `defineWidgetPackage(...)` in the same file.
1452
- * Re-export-only and computed export shapes are not detected by this layer.
1453
- *
1454
- * @param projectDir - The project root directory containing src/portal.config.ts
1455
- */
1456
- async function extractManifests(projectDir) {
1457
- let tempDir;
1458
- try {
1459
- const config = await resolvePortalWidgetSourceConfig(projectDir);
1460
- if (!config) return success([]);
1461
- const configExports = readStaticWidgetExports(await fs.readFile(config.path, "utf-8"));
1462
- const legacyPortalConfigPath = path$1.join(projectDir, "src", "portal.config.ts");
1463
- 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;
1464
- if (!configExports.hasCustomWidgets && !configExports.hasSourceWidgetPackages && !legacyCustomWidgetsConfigPath) return success([]);
1465
- tempDir = await createTempDirectory(projectDir);
1466
- const extractFile = path$1.join(tempDir, EXTRACT_FILENAME);
1467
- const outputFile = path$1.join(tempDir, EXTRACT_OUTPUT_FILENAME);
1468
- const wrapperScript = createManifestExtractorScript({
1469
- projectDir,
1470
- widgetConfigPath: config.path,
1471
- legacyCustomWidgetsConfigPath
1472
- });
1473
- await fs.writeFile(extractFile, wrapperScript, {
1474
- encoding: "utf-8",
1475
- flag: "wx"
1476
- });
1477
- await execa(process.execPath, [
1478
- TSX_CLI_PATH,
1479
- extractFile,
1480
- outputFile
1481
- ], {
1482
- cwd: projectDir,
1483
- stdio: "pipe",
1484
- env: {
1485
- ...process.env,
1486
- NODE_ENV: "production",
1487
- NODE_OPTIONS: withFluidWidgetAuthoringNodeCondition(process.env.NODE_OPTIONS)
1488
- }
1489
- });
1490
- const outputResult = await readManifestExtractorOutput(outputFile);
1491
- if (!outputResult.success) return outputResult;
1492
- const parsed = outputResult.value;
1493
- if (parsed.length === 0) return success([]);
1494
- return success(parsed.filter((m) => typeof m === "object" && m !== null && typeof m.type === "string" && typeof m.displayName === "string"));
1495
- } catch (err) {
1496
- const error = err;
1497
- return failure({
1498
- code: "EXTRACTION_FAILED",
1499
- message: "Failed to extract widget manifests from portal widget source config",
1500
- details: error.stderr ?? error.message ?? String(err)
1501
- });
1502
- } finally {
1503
- if (tempDir) await fs.remove(tempDir).catch(() => {});
1504
- }
1505
- }
1506
- function readStaticWidgetExports(source) {
1507
- const strippedSource = source.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1508
- const defaultExportMatch = /export\s+default\s+([A-Za-z_$][\w$]*)\b/.exec(strippedSource);
1509
- 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]));
1510
- const hasDefaultWidgetPackageExport = /export\s+default\s+defineWidgetPackage\s*\(/.test(strippedSource) || defaultExportMatch !== null && widgetPackageDefinitionNames.has(defaultExportMatch[1]);
1511
- return {
1512
- hasCustomWidgets: /export\s+(?:const|let|var)\s+customWidgets\b/.test(strippedSource),
1513
- hasSourceWidgetPackages: /export\s+(?:const|let|var)\s+widgetPackage\b/.test(strippedSource) || /export\s+(?:const|let|var)\s+widgetPackages\b/.test(strippedSource) || hasDefaultWidgetPackageExport
1514
- };
1515
- }
1516
- async function createTempDirectory(projectDir) {
1517
- const projectTmpDir = path$1.join(projectDir, ".fluid", "tmp");
1518
- try {
1519
- await fs.ensureDir(projectTmpDir);
1520
- return await fs.mkdtemp(path$1.join(projectTmpDir, "manifests-"));
1521
- } catch (err) {
1522
- const message = err instanceof Error ? err.message : String(err);
1523
- throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
1524
- }
1525
- }
1526
- async function readManifestExtractorOutput(outputFile) {
1527
- let output;
1528
- try {
1529
- output = await fs.readFile(outputFile, "utf-8");
1530
- } catch (err) {
1531
- return failure({
1532
- code: "EXTRACTION_FAILED",
1533
- message: "Manifest extractor did not write an output file",
1534
- details: err instanceof Error ? err.message : String(err)
1535
- });
1536
- }
1537
- let parsed;
1538
- try {
1539
- parsed = JSON.parse(output);
1540
- } catch {
1541
- return failure({
1542
- code: "INVALID_FORMAT",
1543
- message: "Failed to parse manifest output file as JSON",
1544
- details: `Output was: ${output.slice(0, 200)}`
1545
- });
1546
- }
1547
- if (!isRecord$4(parsed) || parsed.sentinel !== EXTRACT_OUTPUT_SENTINEL) return failure({
1548
- code: "INVALID_FORMAT",
1549
- message: "Manifest extractor output file had an invalid sentinel"
1550
- });
1551
- if (!Array.isArray(parsed.data)) return failure({
1552
- code: "INVALID_FORMAT",
1553
- message: "extracted widget manifests output is not an array",
1554
- details: `Expected an array, got: ${typeof parsed.data}`
1555
- });
1556
- return success(parsed.data);
1557
- }
1558
- function isRecord$4(value) {
1559
- return typeof value === "object" && value !== null && !Array.isArray(value);
1560
- }
1561
- function createManifestExtractorScript(options) {
1562
- return `
1563
- import fs from "node:fs/promises";
1564
- import path from "node:path";
1565
- import { createServer, normalizePath } from "vite";
1566
- import {
1567
- isSourceWidgetPackage,
1568
- sourceWidgetPackagesToManifests,
1569
- } from "@fluid-app/portal-sdk";
1570
-
1571
- const OUTPUT_SENTINEL = ${JSON.stringify(EXTRACT_OUTPUT_SENTINEL)};
1572
- const projectRoot = ${JSON.stringify(options.projectDir)};
1573
- const widgetConfigPath = ${JSON.stringify(options.widgetConfigPath)};
1574
- const legacyCustomWidgetsConfigPath = ${JSON.stringify(options.legacyCustomWidgetsConfigPath)};
1575
- const outputPath = process.argv[2];
1576
- if (!outputPath) throw new Error("Missing manifest extractor output path.");
1577
-
1578
- function toViteModuleId(modulePath) {
1579
- const relativePath = path.relative(projectRoot, modulePath);
1580
- if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
1581
- return "/" + normalizePath(relativePath);
1582
- }
1583
- return normalizePath(modulePath);
1584
- }
1585
-
1586
- const server = await createServer({
1587
- root: projectRoot,
1588
- mode: "production",
1589
- server: { middlewareMode: true },
1590
- appType: "custom",
1591
- logLevel: "error",
1592
- clearScreen: false,
1593
- resolve: {
1594
- conditions: ["fluid-widget-authoring", "module", "browser", "development|production"],
1595
- },
1596
- ssr: {
1597
- noExternal: ["@fluid-app/portal-sdk"],
1598
- resolve: {
1599
- conditions: ["fluid-widget-authoring", "module", "node", "development|production"],
1600
- },
1601
- },
1602
- });
1603
-
1604
- try {
1605
- const portalConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
1606
- const legacyPortalConfig = legacyCustomWidgetsConfigPath
1607
- ? await server.ssrLoadModule(toViteModuleId(legacyCustomWidgetsConfigPath))
1608
- : {};
1609
-
1610
- const sourcePackages = [];
1611
- const seenSourcePackageIds = new Set();
1612
- for (const candidate of [
1613
- portalConfig.widgetPackage,
1614
- ...(Array.isArray(portalConfig.widgetPackages) ? portalConfig.widgetPackages : []),
1615
- portalConfig.default,
1616
- ]) {
1617
- if (!isSourceWidgetPackage(candidate)) continue;
1618
- if (seenSourcePackageIds.has(candidate.packageId)) continue;
1619
- seenSourcePackageIds.add(candidate.packageId);
1620
- sourcePackages.push(candidate);
1621
- }
1622
- const manifests = [
1623
- ...(Array.isArray(portalConfig.customWidgets) ? portalConfig.customWidgets : []),
1624
- ...(Array.isArray(legacyPortalConfig.customWidgets) ? legacyPortalConfig.customWidgets : []),
1625
- ...sourceWidgetPackagesToManifests(sourcePackages),
1626
- ];
1627
- const serializable = manifests.map(({ component, ...rest }) => rest);
1628
- await fs.writeFile(
1629
- outputPath,
1630
- JSON.stringify({ sentinel: OUTPUT_SENTINEL, data: serializable }),
1631
- "utf-8",
1632
- );
1633
- } finally {
1634
- await server.close();
1635
- }
1636
- `;
1637
- }
1638
- //#endregion
1639
- //#region src/commands/build.ts
1640
- const buildCommand = new Command("build").description("Build the application for production").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
1641
- const cwd = process.cwd();
1642
- if (!existsSync(join(cwd, "package.json"))) {
1643
- console.error(chalk.red("Error: No package.json found in current directory"));
1644
- console.error(chalk.yellow("Make sure you're in a Fluid project directory"));
1645
- process.exit(1);
1646
- }
1647
- if (!existsSync(join(cwd, "vite.config.ts"))) {
1648
- console.error(chalk.red("Error: No vite.config.ts found"));
1649
- console.error(chalk.yellow("This command must be run from a Fluid project directory"));
1650
- process.exit(1);
1651
- }
1652
- console.log();
1653
- console.log(chalk.bold("Building for production..."));
1654
- console.log();
1655
- const spinner = ora("Building...").start();
1656
- try {
1657
- await execa("pnpm", ["run", "build"], {
1658
- cwd,
1659
- stdio: "pipe"
1660
- });
1661
- spinner.succeed("Build completed");
1662
- const manifestSpinner = ora("Extracting widget manifests...").start();
1663
- const outDir = options.outDir ?? "dist";
1664
- const manifestResult = await extractManifests(cwd);
1665
- const manifestPath = [join(cwd, outDir, "__manifests__.json"), join(cwd, outDir, "public", "__manifests__.json")].find((p) => existsSync(p));
1666
- if (manifestResult.success) if (manifestResult.value.length === 0) manifestSpinner.info("No custom widgets found");
1667
- else if (!manifestPath) manifestSpinner.warn(`__manifests__.json not found in build output — skipping manifest write`);
1668
- else {
1669
- writeFileSync(manifestPath, JSON.stringify(manifestResult.value));
1670
- manifestSpinner.succeed(`Extracted ${manifestResult.value.length} widget manifest(s)`);
1671
- }
1672
- else manifestSpinner.warn(`Manifest extraction failed: ${manifestResult.error.message}`);
1673
- console.log();
1674
- console.log(`Output written to ${chalk.cyan(outDir)}/`);
1675
- console.log();
1676
- console.log("To preview the build locally:");
1677
- console.log(chalk.cyan(" pnpm vite preview"));
1678
- console.log();
1679
- } catch (error) {
1680
- spinner.fail("Build failed");
1681
- const execaError = error;
1682
- if (execaError.stderr) console.error(execaError.stderr);
1683
- process.exit(1);
1684
- }
1685
- });
1686
- //#endregion
1687
- //#region src/utils/push-validation.ts
1688
- /**
1689
- * Cross-reference validation and change categorization utilities for the push command.
1690
- *
1691
- * Extracted into a standalone utility so that pure logic can be tested
1692
- * without pulling in CLI dependencies (ora, chalk, prompts, etc.).
1693
- */
1694
- /**
1695
- * Extract the slug from a file path (e.g., "screens/home.json" -> "home").
1696
- */
1697
- function slugFromPath(filePath) {
1698
- return basename(filePath, ".json");
1699
- }
1700
- /**
1701
- * Extract the resource type directory from a file path (e.g., "screens/home.json" -> "screens").
1702
- */
1703
- function resourceTypeFromPath(filePath) {
1704
- const dir = filePath.split("/")[0];
1705
- if (dir === "screens" || dir === "themes" || dir === "navigations" || dir === "profiles") return dir;
1706
- return null;
1707
- }
1708
- /**
1709
- * Read and parse a JSON file from the portal directory.
1710
- */
1711
- async function readPortalFile$1(portalDir, relativePath) {
1712
- const content = await readFile(join(portalDir, relativePath), "utf-8");
1713
- return JSON.parse(content);
1714
- }
1715
- /**
1716
- * Categorize a snapshot diff into resource-type-specific change lists.
1717
- */
1718
- function categorizeChanges(diff) {
1719
- const result = {
1720
- screens: {
1721
- new: [],
1722
- changed: [],
1723
- deleted: []
1724
- },
1725
- themes: {
1726
- new: [],
1727
- changed: [],
1728
- deleted: []
1729
- },
1730
- navigations: {
1731
- new: [],
1732
- changed: [],
1733
- deleted: []
1734
- },
1735
- profiles: {
1736
- new: [],
1737
- changed: [],
1738
- deleted: []
1739
- }
1740
- };
1741
- for (const file of diff.new) {
1742
- const type = resourceTypeFromPath(file);
1743
- if (type) result[type].new.push(file);
1744
- }
1745
- for (const file of diff.changed) {
1746
- const type = resourceTypeFromPath(file);
1747
- if (type) result[type].changed.push(file);
1748
- }
1749
- for (const file of diff.deleted) {
1750
- const type = resourceTypeFromPath(file);
1751
- if (type) result[type].deleted.push(file);
1752
- }
1753
- return result;
1754
- }
1755
- /**
1756
- * Validate that all cross-references between local portal files are valid.
1757
- *
1758
- * Checks:
1759
- * - Navigation items' "screen" slugs reference existing screen files or mappings
1760
- * - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
1761
- * - Profile "themes" slugs reference existing theme files or mappings
1762
- */
1763
- async function validateCrossReferences(portalDir, mappings, changes) {
1764
- const errors = [];
1765
- const validScreenSlugs = buildValidSlugsSet(portalDir, "screens", mappings);
1766
- const validNavSlugs = buildValidSlugsSet(portalDir, "navigations", mappings);
1767
- const validThemeSlugs = buildValidSlugsSet(portalDir, "themes", mappings);
1768
- for (const file of changes.screens.deleted) validScreenSlugs.delete(slugFromPath(file));
1769
- for (const file of changes.navigations.deleted) validNavSlugs.delete(slugFromPath(file));
1770
- for (const file of changes.themes.deleted) validThemeSlugs.delete(slugFromPath(file));
1771
- const navFilesToCheck = [...changes.navigations.new, ...changes.navigations.changed];
1772
- for (const file of navFilesToCheck) try {
1773
- validateNavigationItems((await readPortalFile$1(portalDir, file)).navigation_items, file, validScreenSlugs, errors);
1774
- } catch {
1775
- errors.push({
1776
- file,
1777
- message: "Failed to read navigation file"
1778
- });
1779
- }
1780
- const profileFilesToCheck = [...changes.profiles.new, ...changes.profiles.changed];
1781
- for (const file of profileFilesToCheck) try {
1782
- const profile = await readPortalFile$1(portalDir, file);
1783
- if (profile.navigation && !validNavSlugs.has(profile.navigation)) errors.push({
1784
- file,
1785
- message: `References navigation "${profile.navigation}" which does not exist`
1786
- });
1787
- if (profile.mobile_navigation && !validNavSlugs.has(profile.mobile_navigation)) errors.push({
1788
- file,
1789
- message: `References mobile_navigation "${profile.mobile_navigation}" which does not exist`
1790
- });
1791
- for (const themeSlug of profile.themes) if (!validThemeSlugs.has(themeSlug)) errors.push({
1792
- file,
1793
- message: `References theme "${themeSlug}" which does not exist`
1794
- });
1795
- } catch {
1796
- errors.push({
1797
- file,
1798
- message: "Failed to read profile file"
1799
- });
1800
- }
1801
- return errors;
1802
- }
1803
- /**
1804
- * Build a set of valid slugs for a resource type by combining
1805
- * existing mapping slugs with local file slugs on disk.
1806
- */
1807
- function buildValidSlugsSet(portalDir, resourceType, mappings) {
1808
- const slugs = /* @__PURE__ */ new Set();
1809
- for (const slug of Object.keys(mappings[resourceType])) slugs.add(slug);
1810
- const dir = join(portalDir, resourceType);
1811
- if (existsSync(dir)) try {
1812
- const entries = readdirSync(dir);
1813
- for (const entry of entries) if (entry.endsWith(".json")) slugs.add(basename(entry, ".json"));
1814
- } catch {}
1815
- return slugs;
1372
+ /**
1373
+ * Build a set of valid slugs for a resource type by combining
1374
+ * existing mapping slugs with local file slugs on disk.
1375
+ */
1376
+ function buildValidSlugsSet(portalDir, resourceType, mappings) {
1377
+ const slugs = /* @__PURE__ */ new Set();
1378
+ for (const slug of Object.keys(mappings[resourceType])) slugs.add(slug);
1379
+ const dir = join(portalDir, resourceType);
1380
+ if (existsSync(dir)) try {
1381
+ const entries = readdirSync(dir);
1382
+ for (const entry of entries) if (entry.endsWith(".json")) slugs.add(basename(entry, ".json"));
1383
+ } catch {}
1384
+ return slugs;
1816
1385
  }
1817
1386
  /**
1818
1387
  * Recursively validate navigation item screen references.
@@ -1949,7 +1518,7 @@ async function runPortalGitSync(options) {
1949
1518
  await gitFetch(cwd, remoteUrl, branch);
1950
1519
  await gitAdoptFetchHead(cwd);
1951
1520
  }
1952
- if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage);
1521
+ if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage, { ignoreRemovals: true });
1953
1522
  return await pushCommitted(cwd, remoteUrl, branch);
1954
1523
  }
1955
1524
  await gitFetch(cwd, remoteUrl, branch);
@@ -2094,7 +1663,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2094
1663
  const slug = slugFromPath(file);
2095
1664
  try {
2096
1665
  const local = await readPortalFile(portalDir, file);
2097
- const newId = (await fluid_os_v0_create_fluid_osscreen(client, defId, { screen: {
1666
+ const newId = (await fluidOs.fluid_os_v0_create_fluid_osscreen(client, defId, { screen: {
2098
1667
  name: local.name,
2099
1668
  slug,
2100
1669
  component_tree: toApiComponentTree(local.component_tree)
@@ -2135,7 +1704,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2135
1704
  }
2136
1705
  try {
2137
1706
  const local = await readPortalFile(portalDir, file);
2138
- await fluid_os_v0_update_fluid_osscreen(client, defId, screenId, { screen: {
1707
+ await fluidOs.fluid_os_v0_update_fluid_osscreen(client, defId, screenId, { screen: {
2139
1708
  name: local.name,
2140
1709
  slug,
2141
1710
  component_tree: toApiComponentTree(local.component_tree)
@@ -2174,7 +1743,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2174
1743
  };
2175
1744
  }
2176
1745
  try {
2177
- await fluid_os_v0_delete_fluid_osscreen(client, defId, screenId);
1746
+ await fluidOs.fluid_os_v0_delete_fluid_osscreen(client, defId, screenId);
2178
1747
  currentMappings = removeMapping(currentMappings, "screens", slug);
2179
1748
  results.push({
2180
1749
  file,
@@ -2209,7 +1778,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2209
1778
  const slug = slugFromPath(file);
2210
1779
  try {
2211
1780
  const local = await readPortalFile(portalDir, file);
2212
- const newId = (await fluid_os_v0_create_fluid_ostheme(client, defId, { theme: {
1781
+ const newId = (await fluidOs.fluid_os_v0_create_fluid_ostheme(client, defId, { theme: {
2213
1782
  name: local.name,
2214
1783
  active: local.active,
2215
1784
  config: local.config
@@ -2250,7 +1819,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2250
1819
  }
2251
1820
  try {
2252
1821
  const local = await readPortalFile(portalDir, file);
2253
- await fluid_os_v0_update_fluid_ostheme(client, defId, themeId, { theme: {
1822
+ await fluidOs.fluid_os_v0_update_fluid_ostheme(client, defId, themeId, { theme: {
2254
1823
  name: local.name,
2255
1824
  active: local.active,
2256
1825
  config: local.config
@@ -2289,7 +1858,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2289
1858
  };
2290
1859
  }
2291
1860
  try {
2292
- await fluid_os_v0_delete_fluid_ostheme(client, defId, themeId);
1861
+ await fluidOs.fluid_os_v0_delete_fluid_ostheme(client, defId, themeId);
2293
1862
  currentMappings = removeMapping(currentMappings, "themes", slug);
2294
1863
  results.push({
2295
1864
  file,
@@ -2359,7 +1928,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2359
1928
  const slug = slugFromPath(file);
2360
1929
  try {
2361
1930
  const local = await readPortalFile(portalDir, file);
2362
- const newId = (await fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
1931
+ const newId = (await fluidOs.fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
2363
1932
  name: local.name,
2364
1933
  platform: local.platform
2365
1934
  } })).navigation?.id;
@@ -2369,7 +1938,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2369
1938
  const localToServerId = /* @__PURE__ */ new Map();
2370
1939
  for (const item of resolvedItems) {
2371
1940
  const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
2372
- const created = await fluid_os_v0_create_fluid_osnavigation_item(client, defId, newId, { navigation_item: {
1941
+ const created = await fluidOs.fluid_os_v0_create_fluid_osnavigation_item(client, defId, newId, { navigation_item: {
2373
1942
  label: item.label ?? "",
2374
1943
  position: item.position ?? 0,
2375
1944
  icon: item.icon ?? void 0,
@@ -2416,15 +1985,15 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2416
1985
  }
2417
1986
  try {
2418
1987
  const local = await readPortalFile(portalDir, file);
2419
- await fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
1988
+ await fluidOs.fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
2420
1989
  name: local.name,
2421
1990
  platform: local.platform
2422
1991
  } });
2423
1992
  const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
2424
- const serverItems = (await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? [];
1993
+ const serverItems = (await fluidOs.fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? [];
2425
1994
  const serverById = new Map(serverItems.map((s) => [s.id, s]));
2426
1995
  const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
2427
- for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navId, serverItem.id);
1996
+ for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluidOs.fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navId, serverItem.id);
2428
1997
  const localToServerId = /* @__PURE__ */ new Map();
2429
1998
  for (const item of resolvedItems) {
2430
1999
  const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
@@ -2437,9 +2006,9 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2437
2006
  source: item.source ?? void 0,
2438
2007
  parent_id: resolvedParentId
2439
2008
  };
2440
- if (item.id && serverById.has(item.id)) await fluid_os_v0_update_fluid_osnavigation_item(client, defId, navId, item.id, { navigation_item: body });
2009
+ if (item.id && serverById.has(item.id)) await fluidOs.fluid_os_v0_update_fluid_osnavigation_item(client, defId, navId, item.id, { navigation_item: body });
2441
2010
  else {
2442
- const created = await fluid_os_v0_create_fluid_osnavigation_item(client, defId, navId, { navigation_item: {
2011
+ const created = await fluidOs.fluid_os_v0_create_fluid_osnavigation_item(client, defId, navId, { navigation_item: {
2443
2012
  ...body,
2444
2013
  label: body.label ?? "",
2445
2014
  position: body.position ?? 0
@@ -2481,7 +2050,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2481
2050
  };
2482
2051
  }
2483
2052
  try {
2484
- await fluid_os_v0_delete_fluid_osnavigation(client, defId, navId);
2053
+ await fluidOs.fluid_os_v0_delete_fluid_osnavigation(client, defId, navId);
2485
2054
  currentMappings = removeMapping(currentMappings, "navigations", slug);
2486
2055
  results.push({
2487
2056
  file,
@@ -2515,7 +2084,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2515
2084
  for (const file of changes.new) {
2516
2085
  const slug = slugFromPath(file);
2517
2086
  try {
2518
- const newId = (await fluid_os_v0_create_fluid_osprofile(client, defId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) })).profile?.id;
2087
+ const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
2088
+ const newId = (await fluidOs.fluid_os_v0_create_fluid_osprofile(client, defId, { profile: body })).profile?.id;
2519
2089
  if (newId != null) currentMappings = updateMapping(currentMappings, "profiles", slug, newId);
2520
2090
  results.push({
2521
2091
  file,
@@ -2551,7 +2121,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2551
2121
  };
2552
2122
  }
2553
2123
  try {
2554
- await fluid_os_v0_update_fluid_osprofile(client, defId, profileId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) });
2124
+ const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
2125
+ await fluidOs.fluid_os_v0_update_fluid_osprofile(client, defId, profileId, { profile: body });
2555
2126
  results.push({
2556
2127
  file,
2557
2128
  action: "updated",
@@ -2586,7 +2157,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2586
2157
  };
2587
2158
  }
2588
2159
  try {
2589
- await fluid_os_v0_delete_fluid_osprofile(client, defId, profileId);
2160
+ await fluidOs.fluid_os_v0_delete_fluid_osprofile(client, defId, profileId);
2590
2161
  currentMappings = removeMapping(currentMappings, "profiles", slug);
2591
2162
  results.push({
2592
2163
  file,
@@ -2843,7 +2414,7 @@ async function selectDefinition(client, appName) {
2843
2414
  const definitions = [];
2844
2415
  let page = 1;
2845
2416
  while (true) {
2846
- const response = await fluid_os_v0_list_fluid_osdefinitions(client, {
2417
+ const response = await fluidOs.fluid_os_v0_list_fluid_osdefinitions(client, {
2847
2418
  page,
2848
2419
  per_page: 500
2849
2420
  });
@@ -2988,138 +2559,340 @@ function insertImport(source, importLine) {
2988
2559
  if (lineEnd === -1) return source + "\n" + importLine + "\n";
2989
2560
  return source.slice(0, lineEnd + 1) + importLine + "\n" + source.slice(lineEnd + 1);
2990
2561
  }
2991
- /**
2992
- * Insert a manifest reference into the customWidgets array.
2993
- * Preserves developer comments. Skips if already present.
2994
- * Returns null if the array pattern isn't found.
2995
- *
2996
- * Searches for the LAST `export const customWidgets` declaration to skip
2997
- * matches inside JSDoc @example blocks.
2998
- */
2999
- function insertIntoCustomWidgets(source, camelName) {
3000
- const pattern = /(export const customWidgets(?::[\s\S]*?)?\s*=\s*)\[([^\]]*)\]/g;
2562
+ /** Insert a widget definition into a defineWidgetPackage widgets array. */
2563
+ function insertIntoWidgetPackage(source, exportName) {
2564
+ const packageStart = source.lastIndexOf("defineWidgetPackage(");
2565
+ if (packageStart === -1) return null;
2566
+ const packageSource = source.slice(packageStart);
2567
+ const pattern = /(widgets\s*:\s*)\[([^\]]*)\]/g;
3001
2568
  let lastMatch = null;
3002
2569
  let m;
3003
- while ((m = pattern.exec(source)) !== null) lastMatch = m;
2570
+ while ((m = pattern.exec(packageSource)) !== null) lastMatch = m;
3004
2571
  if (!lastMatch) return null;
3005
2572
  const fullMatch = lastMatch[0];
3006
2573
  const declaration = lastMatch[1];
3007
2574
  const inner = lastMatch[2];
3008
- const matchStart = lastMatch.index;
2575
+ const matchStart = packageStart + lastMatch.index;
3009
2576
  const lines = inner.split("\n");
3010
2577
  const existingEntries = [];
3011
2578
  for (const line of lines) {
3012
2579
  const trimmed = line.trim();
3013
2580
  if (trimmed && !trimmed.startsWith("//")) existingEntries.push(trimmed.replace(/,$/, ""));
3014
2581
  }
3015
- if (existingEntries.includes(camelName)) return source;
3016
- const commentLines = lines.filter((line) => line.trim().startsWith("//")).map((line) => line.trimEnd());
3017
- const replacement = `${declaration}[${commentLines.length > 0 ? "\n" + commentLines.join("\n") : ""}\n${[...existingEntries, camelName].map((e) => ` ${e},`).join("\n")}\n]`;
3018
- return source.slice(0, matchStart) + replacement + source.slice(matchStart + fullMatch.length);
2582
+ if (existingEntries.includes(exportName)) return source;
2583
+ const commentLines = lines.filter((line) => line.trim().startsWith("//")).map((line) => line.trimEnd());
2584
+ const replacement = `${declaration}[${commentLines.length > 0 ? "\n" + commentLines.join("\n") : ""}\n${[...existingEntries, exportName].map((e) => ` ${e},`).join("\n")}\n]`;
2585
+ return source.slice(0, matchStart) + replacement + source.slice(matchStart + fullMatch.length);
2586
+ }
2587
+ //#endregion
2588
+ //#region src/commands/widget-create.ts
2589
+ const WIDGET_CONFIG_PATH$1 = path.join("src", "widgets.config.ts");
2590
+ const createSubcommand = new Command("create").description("Scaffold a new Remote DOM widget").argument("<name>", "Widget name in kebab-case (e.g., stock-ticker)").option("-c, --category <category>", "Widget category for palette grouping", "components").action(async (name, options) => {
2591
+ const cwd = process.cwd();
2592
+ const packageJsonPath = path.join(cwd, "package.json");
2593
+ const configPath = path.join(cwd, WIDGET_CONFIG_PATH$1);
2594
+ const widgetDir = path.join(cwd, "src", "widgets", name);
2595
+ const validationError = validateWidgetName(name);
2596
+ if (validationError) {
2597
+ console.error(chalk.red(`Error: ${validationError}`));
2598
+ process.exit(1);
2599
+ }
2600
+ if (!await fs.pathExists(packageJsonPath)) {
2601
+ console.error(chalk.red("Error: No package.json found."));
2602
+ console.error(chalk.yellow("Run this command from a Fluid portal project root."));
2603
+ process.exit(1);
2604
+ }
2605
+ if (await fs.pathExists(widgetDir)) {
2606
+ console.error(chalk.red(`Error: Widget directory already exists: src/widgets/${name}`));
2607
+ process.exit(1);
2608
+ }
2609
+ const widgetType = toWidgetType(name);
2610
+ const componentName = toComponentName(widgetType);
2611
+ const camelName = toCamelCase(name);
2612
+ const exportName = camelName.endsWith("Widget") ? camelName : `${camelName}Widget`;
2613
+ const displayName = toDisplayName(name);
2614
+ const category = options.category ?? "components";
2615
+ const importLine = `import { ${exportName} } from "./widgets/${name}";`;
2616
+ try {
2617
+ await fs.ensureDir(widgetDir);
2618
+ await fs.writeFile(path.join(widgetDir, "index.tsx"), createWidgetSource({
2619
+ name,
2620
+ widgetType,
2621
+ componentName,
2622
+ exportName,
2623
+ displayName,
2624
+ category
2625
+ }), "utf-8");
2626
+ if (await fs.pathExists(configPath)) {
2627
+ const configSource = await fs.readFile(configPath, "utf-8");
2628
+ if (isEmptyWidgetConfig(configSource)) await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName), "utf-8");
2629
+ else {
2630
+ const withImport = insertImport(configSource, importLine);
2631
+ const updated = withImport ? insertIntoWidgetPackage(withImport, exportName) : null;
2632
+ if (!updated) throw new Error(`${WIDGET_CONFIG_PATH$1} must export the starter widgetPackages placeholder or contain a defineWidgetPackage({ widgets: [...] }) declaration.`);
2633
+ await fs.writeFile(configPath, updated, "utf-8");
2634
+ }
2635
+ } else await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName), "utf-8");
2636
+ } catch (error) {
2637
+ await fs.remove(widgetDir).catch(() => {});
2638
+ console.error(chalk.red("Error: Failed to scaffold Remote DOM widget."));
2639
+ console.error(error);
2640
+ process.exit(1);
2641
+ }
2642
+ console.log();
2643
+ console.log(chalk.green("Created widget:") + ` src/widgets/${name}/index.tsx`);
2644
+ console.log(chalk.green("Registered") + ` in ${chalk.cyan(WIDGET_CONFIG_PATH$1)}`);
2645
+ console.log();
2646
+ console.log(chalk.yellow("Next steps:"));
2647
+ console.log(` 1. Edit ${chalk.cyan(`src/widgets/${name}/index.tsx`)}`);
2648
+ console.log(` 2. Run ${chalk.cyan("fluid portal deploy --dry-run")} to verify the package`);
2649
+ });
2650
+ function createWidgetSource(options) {
2651
+ return `import { defineWidget } from "@fluid-app/portal-sdk/widgets/worker";
2652
+
2653
+ interface ${options.widgetType}Props {
2654
+ readonly title?: string;
2655
+ }
2656
+
2657
+ function ${options.componentName}({ title = ${JSON.stringify(options.displayName)} }: ${options.widgetType}Props) {
2658
+ return (
2659
+ <section>
2660
+ <h3>{title}</h3>
2661
+ <p>Edit this widget in src/widgets/${options.name}/index.tsx</p>
2662
+ </section>
2663
+ );
2664
+ }
2665
+
2666
+ export const ${options.exportName} = defineWidget({
2667
+ name: ${JSON.stringify(options.componentName)},
2668
+ component: ${options.componentName},
2669
+ displayName: ${JSON.stringify(options.displayName)},
2670
+ description: ${JSON.stringify(`A custom ${options.displayName.toLowerCase()} widget`)},
2671
+ icon: "puzzle-piece",
2672
+ category: ${JSON.stringify(options.category)},
2673
+ propertySchema: {
2674
+ fields: [{ key: "title", label: "Title", type: "text" }],
2675
+ },
2676
+ defaultProps: {
2677
+ title: ${JSON.stringify(options.displayName)},
2678
+ },
2679
+ });
2680
+ `;
2681
+ }
2682
+ function isEmptyWidgetConfig(source) {
2683
+ return /export\s+const\s+widgetPackages\s*=\s*\[\]\s+as\s+const\s*;/u.test(source);
2684
+ }
2685
+ function createWidgetConfigSource(importLine, exportName) {
2686
+ return `import { defineWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
2687
+ ${importLine}
2688
+
2689
+ export const widgetPackage = defineWidgetPackage({
2690
+ scope: "company",
2691
+ packageStableId: "portal-widgets",
2692
+ version: "0.0.0-dev",
2693
+ widgets: [${exportName}],
2694
+ });
2695
+ `;
2696
+ }
2697
+ const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
2698
+ //#endregion
2699
+ //#region src/utils/widget-package-config.ts
2700
+ const WIDGET_CONFIG_PATH = "src/widgets.config.ts";
2701
+ const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
2702
+ const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
2703
+ const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
2704
+ const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
2705
+ async function resolvePortalWidgetSourceConfig(projectDir) {
2706
+ const candidate = path.join(projectDir, WIDGET_CONFIG_PATH);
2707
+ return await fs.pathExists(candidate) ? {
2708
+ path: candidate,
2709
+ relativePath: WIDGET_CONFIG_PATH
2710
+ } : void 0;
2711
+ }
2712
+ async function loadSourceWidgetPackages(projectDir) {
2713
+ const config = await resolvePortalWidgetSourceConfig(projectDir);
2714
+ if (!config) return success([]);
2715
+ let tempDir;
2716
+ try {
2717
+ tempDir = await createTempDirectory(projectDir);
2718
+ const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
2719
+ const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
2720
+ await fs.writeFile(extractFile, createSourcePackageExtractorScript({
2721
+ projectDir,
2722
+ configPath: config.path
2723
+ }), {
2724
+ encoding: "utf-8",
2725
+ flag: "wx"
2726
+ });
2727
+ await execa(process.execPath, [
2728
+ TSX_CLI_PATH,
2729
+ extractFile,
2730
+ outputFile
2731
+ ], {
2732
+ cwd: projectDir,
2733
+ stdio: "pipe",
2734
+ env: {
2735
+ ...process.env,
2736
+ NODE_ENV: "production"
2737
+ }
2738
+ });
2739
+ const outputResult = await readSourcePackageExtractorOutput(outputFile);
2740
+ if (!outputResult.success) return outputResult;
2741
+ const parsed = outputResult.value;
2742
+ if (parsed.length === 0) return success([]);
2743
+ return success(parsed);
2744
+ } catch (err) {
2745
+ const error = err;
2746
+ return failure({
2747
+ code: "CONFIG_LOAD_FAILED",
2748
+ message: `Failed to load widget packages from ${config.relativePath}`,
2749
+ details: error.stderr ?? error.message ?? String(err)
2750
+ });
2751
+ } finally {
2752
+ if (tempDir) await fs.remove(tempDir).catch(() => {});
2753
+ }
3019
2754
  }
3020
- //#endregion
3021
- //#region src/commands/widget-create.ts
3022
- const createSubcommand = new Command("create").description("Scaffold a new custom widget").argument("<name>", "Widget name in kebab-case (e.g., stock-ticker)").option("-c, --category <category>", "Widget category for palette grouping", "components").action(async (name, options) => {
3023
- const cwd = process.cwd();
3024
- const widgetDir = path.join(cwd, "src", "widgets", name);
3025
- const validationError = validateWidgetName(name);
3026
- if (validationError) {
3027
- console.error(chalk.red(`Error: ${validationError}`));
3028
- process.exit(1);
3029
- }
3030
- if (!fs.existsSync(path.join(cwd, "src", "portal.config.ts"))) {
3031
- console.error(chalk.red("Error: No src/portal.config.ts found."));
3032
- console.error(chalk.yellow("Run this command from a Fluid portal project root."));
3033
- process.exit(1);
2755
+ async function createTempDirectory(projectDir) {
2756
+ const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
2757
+ try {
2758
+ await fs.ensureDir(projectTmpDir);
2759
+ return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
2760
+ } catch (err) {
2761
+ const message = err instanceof Error ? err.message : String(err);
2762
+ throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
3034
2763
  }
3035
- if (fs.existsSync(widgetDir)) {
3036
- console.error(chalk.red(`Error: Widget directory already exists: src/widgets/${name}`));
3037
- process.exit(1);
2764
+ }
2765
+ async function readSourcePackageExtractorOutput(outputFile) {
2766
+ let output;
2767
+ try {
2768
+ output = await fs.readFile(outputFile, "utf-8");
2769
+ } catch (err) {
2770
+ return failure({
2771
+ code: "CONFIG_LOAD_FAILED",
2772
+ message: "Widget package extractor did not write an output file",
2773
+ details: err instanceof Error ? err.message : String(err)
2774
+ });
3038
2775
  }
3039
- const widgetType = toWidgetType(name);
3040
- const componentName = toComponentName(widgetType);
3041
- const displayName = toDisplayName(name);
3042
- const category = options.category ?? "components";
2776
+ let parsed;
3043
2777
  try {
3044
- await fs.ensureDir(widgetDir);
3045
- await fs.writeFile(path.join(widgetDir, "component.tsx"), `interface ${widgetType}Props {
3046
- title?: string;
2778
+ parsed = JSON.parse(output);
2779
+ } catch {
2780
+ return failure({
2781
+ code: "INVALID_FORMAT",
2782
+ message: "Failed to parse widget package output file as JSON",
2783
+ details: `Output was: ${output.slice(0, 200)}`
2784
+ });
2785
+ }
2786
+ if (!isRecord$4(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
2787
+ code: "INVALID_FORMAT",
2788
+ message: "Widget package extractor output file had an invalid sentinel"
2789
+ });
2790
+ if (!Array.isArray(parsed.data)) return failure({
2791
+ code: "INVALID_FORMAT",
2792
+ message: "Extracted widget package output is not an array",
2793
+ details: `Expected an array, got: ${typeof parsed.data}`
2794
+ });
2795
+ return success(parsed.data);
3047
2796
  }
2797
+ function createSourcePackageExtractorScript(options) {
2798
+ return `
2799
+ import fs from "node:fs/promises";
2800
+ import path from "node:path";
2801
+ import { createServer, normalizePath } from "vite";
3048
2802
 
3049
- export function ${componentName}({ title = "${displayName}" }: ${widgetType}Props) {
3050
- return (
3051
- <div className="p-4">
3052
- <h3 className="text-lg font-semibold">{title}</h3>
3053
- <p className="text-sm text-gray-500">
3054
- Edit this widget in src/widgets/${name}/component.tsx
3055
- </p>
3056
- </div>
3057
- );
2803
+ const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
2804
+ const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
2805
+ const projectRoot = ${JSON.stringify(options.projectDir)};
2806
+ const widgetConfigPath = ${JSON.stringify(options.configPath)};
2807
+ const outputPath = process.argv[2];
2808
+ if (!outputPath) throw new Error("Missing widget package extractor output path.");
2809
+
2810
+ function toViteModuleId(modulePath) {
2811
+ const relativePath = path.relative(projectRoot, modulePath);
2812
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
2813
+ return "/" + normalizePath(relativePath);
2814
+ }
2815
+ return normalizePath(modulePath);
3058
2816
  }
3059
- `);
3060
- await fs.writeFile(path.join(widgetDir, "manifest.ts"), `import type { WidgetManifest } from "@fluid-app/portal-sdk";
3061
- import { ${componentName} } from "./component";
3062
2817
 
3063
- export const manifest: WidgetManifest = {
3064
- manifestVersion: 1,
3065
- type: "${widgetType}",
3066
- component: ${componentName},
3067
- displayName: "${displayName}",
3068
- description: "A custom ${displayName.toLowerCase()} widget",
3069
- icon: "puzzle-piece",
3070
- category: "${category}",
3071
- propertySchema: {
3072
- widgetType: "${widgetType}",
3073
- displayName: "${displayName}",
3074
- fields: [{ key: "title", label: "Title", type: "text" }],
2818
+ const server = await createServer({
2819
+ root: projectRoot,
2820
+ mode: "production",
2821
+ server: { middlewareMode: true },
2822
+ appType: "custom",
2823
+ logLevel: "error",
2824
+ clearScreen: false,
2825
+ resolve: {
2826
+ conditions: ["module", "browser", "development|production"],
3075
2827
  },
3076
- defaultProps: {
3077
- title: "${displayName}",
2828
+ ssr: {
2829
+ noExternal: ["@fluid-app/portal-sdk"],
2830
+ resolve: {
2831
+ conditions: ["module", "node", "development|production"],
2832
+ },
3078
2833
  },
3079
- };
3080
- `);
3081
- await fs.writeFile(path.join(widgetDir, "index.ts"), `export { ${componentName} } from "./component";
3082
- export { manifest } from "./manifest";
3083
- `);
3084
- } catch (err) {
3085
- await fs.remove(widgetDir).catch(() => {});
3086
- console.error(chalk.red("Error: Failed to scaffold widget files."));
3087
- console.error(err);
3088
- process.exit(1);
3089
- }
3090
- const configPath = path.join(cwd, "src", "portal.config.ts");
3091
- const camelName = toCamelCase(name);
3092
- const importLine = `import { manifest as ${camelName} } from "./widgets/${name}";`;
3093
- const configSource = await fs.readFile(configPath, "utf-8");
3094
- const withImport = insertImport(configSource, importLine);
3095
- if (withImport === null) {
3096
- console.warn(chalk.yellow("Warning: Could not find import statements in portal.config.ts. Add the import manually:"));
3097
- console.warn(chalk.cyan(` ${importLine}`));
3098
- }
3099
- let updated = insertIntoCustomWidgets(withImport ?? configSource, camelName);
3100
- if (updated === null) if (withImport === null) {
3101
- console.warn(chalk.yellow("Warning: Could not register widget in portal.config.ts. Add manually:"));
3102
- console.warn(chalk.cyan(` ${importLine}`));
3103
- console.warn(chalk.cyan(` customWidgets: [..., ${camelName}]`));
3104
- } else updated = withImport.trimEnd() + `\n\nexport const customWidgets = [${camelName}];\n`;
3105
- if (updated !== null) await fs.writeFile(configPath, updated, "utf-8");
3106
- else if (withImport !== null) await fs.writeFile(configPath, withImport, "utf-8");
3107
- console.log();
3108
- console.log(chalk.green("Created widget:") + ` src/widgets/${name}/`);
3109
- console.log(chalk.gray(" component.tsx — React component"));
3110
- console.log(chalk.gray(" manifest.ts — WidgetManifest"));
3111
- console.log(chalk.gray(" index.ts — Re-exports"));
3112
- console.log();
3113
- if (updated !== null) {
3114
- console.log(chalk.green("Registered") + ` in ${chalk.cyan("src/portal.config.ts")}`);
3115
- console.log();
3116
- }
3117
- console.log(chalk.yellow("Next steps:"));
3118
- console.log(` 1. Edit the component in ${chalk.cyan(`src/widgets/${name}/component.tsx`)}`);
3119
- console.log(` 2. Customize the manifest fields in ${chalk.cyan(`src/widgets/${name}/manifest.ts`)}`);
3120
- console.log(` 3. Run ${chalk.cyan("fluid portal dev")} to preview in the builder`);
3121
2834
  });
3122
- const widgetCommand = new Command("widget").description("Manage custom portal widgets").addCommand(createSubcommand);
2835
+
2836
+ try {
2837
+ const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
2838
+
2839
+ function isSourceWidgetPackage(value) {
2840
+ return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
2841
+ }
2842
+
2843
+ function collectSourceWidgetPackages(mod) {
2844
+ const candidates = [
2845
+ mod.widgetPackage,
2846
+ ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
2847
+ mod.default,
2848
+ ];
2849
+ const byPackageId = new Map();
2850
+ for (const candidate of candidates) {
2851
+ if (!isSourceWidgetPackage(candidate)) continue;
2852
+ if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
2853
+ }
2854
+ return Array.from(byPackageId.values());
2855
+ }
2856
+
2857
+ function serializeWidget(widget) {
2858
+ if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
2859
+ return widget;
2860
+ }
2861
+ const { component, ...metadata } = widget;
2862
+ return metadata;
2863
+ }
2864
+
2865
+ function serializePackage(sourcePackage) {
2866
+ return {
2867
+ manifestVersion: sourcePackage.manifestVersion,
2868
+ scope: sourcePackage.scope,
2869
+ packageStableId: sourcePackage.packageStableId,
2870
+ packageId: sourcePackage.packageId,
2871
+ packageType: sourcePackage.packageType,
2872
+ version: sourcePackage.version,
2873
+ cssUrls: sourcePackage.cssUrls,
2874
+ widgets: Array.isArray(sourcePackage.widgets)
2875
+ ? sourcePackage.widgets.map(serializeWidget)
2876
+ : sourcePackage.widgets,
2877
+ };
2878
+ }
2879
+
2880
+ await fs.writeFile(
2881
+ outputPath,
2882
+ JSON.stringify({
2883
+ sentinel: OUTPUT_SENTINEL,
2884
+ data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
2885
+ }),
2886
+ "utf-8",
2887
+ );
2888
+ } finally {
2889
+ await server.close();
2890
+ }
2891
+ `;
2892
+ }
2893
+ function isRecord$4(value) {
2894
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2895
+ }
3123
2896
  //#endregion
3124
2897
  //#region src/utils/widget-package-artifacts.ts
3125
2898
  const WIDGET_PACKAGE_BUILDER_VERSION = "1";
@@ -3240,7 +3013,7 @@ const DEFAULT_MIN_SDK_VERSION = "0.0.0";
3240
3013
  function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
3241
3014
  if (sourcePackages.length === 0) return validationFailure({
3242
3015
  code: "NO_SOURCE_PACKAGE",
3243
- 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."
3016
+ message: "No defineWidgetPackage source package was found. Export widgetPackage, widgetPackages, or a default widget package from src/widgets.config.ts."
3244
3017
  });
3245
3018
  if (sourcePackages.length > 1) return validationFailure({
3246
3019
  code: "MULTIPLE_SOURCE_PACKAGES",
@@ -3342,22 +3115,19 @@ function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
3342
3115
  errors: []
3343
3116
  };
3344
3117
  }
3345
- function buildWidgetPackageDescriptor(validated, options = {}) {
3346
- const remoteEntryUrl = options.remoteEntryUrl ?? "widget.js";
3347
- validateRuntimeDescriptorUrl(remoteEntryUrl, "remoteEntryUrl");
3118
+ function buildWidgetPackagePublishManifest(validated, options = {}) {
3348
3119
  return {
3349
3120
  manifestVersion: WIDGET_PACKAGE_MANIFEST_VERSION,
3350
- packageId: getWidgetPackageDescriptorPackageId(validated),
3351
- packageType: validated.owner,
3121
+ runtime: "remote-dom",
3122
+ packageId: validated.packageId,
3123
+ source: validated.owner,
3352
3124
  version: validated.version,
3353
- remoteEntryUrl,
3125
+ workerEntryUrl: toWidgetPackageArtifactPath("widget.js"),
3354
3126
  cssUrls: mergeCssUrls(validated.sourcePackage.cssUrls, options.cssUrls),
3355
- widgets: validated.widgets
3127
+ assetUrls: [],
3128
+ widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
3356
3129
  };
3357
3130
  }
3358
- function getWidgetPackageDescriptorPackageId(validated) {
3359
- return validated.owner === "droplet" ? validated.packageKey : validated.packageId;
3360
- }
3361
3131
  function sourceWidgetToDescriptor(widget, type) {
3362
3132
  const displayName = widget.displayName ?? widget.name;
3363
3133
  return {
@@ -3371,9 +3141,16 @@ function sourceWidgetToDescriptor(widget, type) {
3371
3141
  defaultProps: widget.defaultProps ?? {},
3372
3142
  container: normalizeContainer(widget.container),
3373
3143
  minSdkVersion: widget.minSdkVersion ?? DEFAULT_MIN_SDK_VERSION,
3374
- resizable: normalizeResizable(widget.resizable)
3144
+ resizable: normalizeResizable(widget.resizable),
3145
+ capabilities: normalizeCapabilities(widget.capabilities ?? [])
3375
3146
  };
3376
3147
  }
3148
+ function normalizeCapabilities(capabilities) {
3149
+ return capabilities.map((capability) => ({
3150
+ name: capability.name,
3151
+ version: capability.version ?? "1"
3152
+ }));
3153
+ }
3377
3154
  function validationFailure(error) {
3378
3155
  return {
3379
3156
  success: false,
@@ -3426,8 +3203,9 @@ function validateSourcePackageShape(value) {
3426
3203
  validateOptionalStringField(widget, "icon", `${widgetPath}.icon`, errors, { requireNonEmpty: true });
3427
3204
  validateOptionalStringField(widget, "category", `${widgetPath}.category`, errors, { requireNonEmpty: true });
3428
3205
  validateOptionalStringField(widget, "minSdkVersion", `${widgetPath}.minSdkVersion`, errors, { requireNonEmpty: true });
3429
- validateOptionalRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
3430
- validateOptionalRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
3206
+ validateOptionalJsonRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
3207
+ validateOptionalJsonRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
3208
+ validateOptionalCapabilities(widget, "capabilities", `${widgetPath}.capabilities`, errors);
3431
3209
  });
3432
3210
  return errors;
3433
3211
  }
@@ -3473,85 +3251,67 @@ function validateCssUrl(value, cssUrlPath, errors) {
3473
3251
  message: "Widget package cssUrls entries must be top-level generated CSS artifact filenames matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them."
3474
3252
  });
3475
3253
  }
3476
- function validateRuntimeDescriptorUrl(value, urlPath) {
3477
- if (value.trim().length === 0) throw new Error(`${urlPath} must be a non-empty string.`);
3478
- if (!isValidRuntimeDescriptorUrl(value)) throw new Error(`${urlPath} must be an https URL, trusted local http URL, or relative URL.`);
3479
- }
3480
- function isValidRuntimeDescriptorUrl(value) {
3481
- if (value.trim() !== value || containsUnsafeUrlCharacter(value)) return false;
3482
- if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.exec(value)) {
3483
- let parsedUrl;
3484
- try {
3485
- parsedUrl = new URL(value);
3486
- } catch {
3487
- return false;
3488
- }
3489
- return parsedUrl.protocol === "https:" || isTrustedLocalHttpUrl(parsedUrl, value);
3490
- }
3491
- if (value.slice(0, 2) === "//") return false;
3492
- try {
3493
- new URL(value, "http://localhost");
3494
- return true;
3495
- } catch {
3496
- return false;
3497
- }
3498
- }
3499
- function isTrustedLocalHttpUrl(url, rawUrl) {
3500
- if (url.protocol !== "http:") return false;
3501
- const hostname = extractRawHostname(rawUrl).toLowerCase();
3502
- return hostname === "localhost" || hostname === "[::1]" || hostname === "::1" || isLoopbackIpv4Hostname(hostname);
3503
- }
3504
- function extractRawHostname(rawUrl) {
3505
- const authorityStart = rawUrl.indexOf("://") + 3;
3506
- const authorityEnd = findAuthorityEnd(rawUrl, authorityStart);
3507
- const authority = rawUrl.slice(authorityStart, authorityEnd);
3508
- const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
3509
- if (hostAndPort.charAt(0) === "[") {
3510
- const bracketEnd = hostAndPort.indexOf("]");
3511
- return bracketEnd === -1 ? hostAndPort : hostAndPort.slice(0, bracketEnd + 1);
3512
- }
3513
- const portStart = hostAndPort.indexOf(":");
3514
- return portStart === -1 ? hostAndPort : hostAndPort.slice(0, portStart);
3515
- }
3516
- function findAuthorityEnd(rawUrl, authorityStart) {
3517
- let authorityEnd = rawUrl.length;
3518
- for (const delimiter of [
3519
- "/",
3520
- "?",
3521
- "#"
3522
- ]) {
3523
- const delimiterIndex = rawUrl.indexOf(delimiter, authorityStart);
3524
- if (delimiterIndex !== -1 && delimiterIndex < authorityEnd) authorityEnd = delimiterIndex;
3525
- }
3526
- return authorityEnd;
3527
- }
3528
- function isLoopbackIpv4Hostname(hostname) {
3529
- const octets = hostname.split(".");
3530
- if (octets.length !== 4 || octets[0] !== "127") return false;
3531
- return octets.every(isBoundedIpv4Octet);
3532
- }
3533
- function isBoundedIpv4Octet(octet) {
3534
- if (!/^\d{1,3}$/.test(octet)) return false;
3535
- if (octet.length > 1 && octet.charAt(0) === "0") return false;
3536
- const value = Number(octet);
3537
- return value >= 0 && value <= 255;
3538
- }
3539
- function containsUnsafeUrlCharacter(value) {
3540
- for (let index = 0; index < value.length; index += 1) {
3541
- const charCode = value.charCodeAt(index);
3542
- if (charCode <= 31 || charCode === 127 || charCode === 92) return true;
3543
- }
3544
- return false;
3545
- }
3546
- function validateOptionalRecordField(record, key, path, errors) {
3254
+ function validateOptionalJsonRecordField(record, key, path, errors) {
3547
3255
  const value = record[key];
3548
- if (value === void 0 || isRecord$3(value)) return;
3256
+ if (value === void 0) return;
3257
+ if (isRecord$3(value) && isJsonValue(value)) return;
3549
3258
  errors.push({
3550
3259
  code: "INVALID_WIDGET_METADATA",
3551
3260
  path,
3552
3261
  message: `${path} must be a JSON object when present.`
3553
3262
  });
3554
3263
  }
3264
+ function validateOptionalCapabilities(record, key, path, errors) {
3265
+ const value = record[key];
3266
+ if (value === void 0) return;
3267
+ if (!Array.isArray(value)) {
3268
+ errors.push({
3269
+ code: "INVALID_WIDGET_METADATA",
3270
+ path,
3271
+ message: `${path} must be an array when present.`
3272
+ });
3273
+ return;
3274
+ }
3275
+ const names = /* @__PURE__ */ new Set();
3276
+ value.forEach((candidate, index) => {
3277
+ const capabilityPath = `${path}[${index}]`;
3278
+ if (!isRecord$3(candidate)) {
3279
+ errors.push({
3280
+ code: "INVALID_WIDGET_METADATA",
3281
+ path: capabilityPath,
3282
+ message: `${capabilityPath} must be a JSON object.`
3283
+ });
3284
+ return;
3285
+ }
3286
+ const name = candidate.name;
3287
+ if (!isCanonicalMetadataString(name)) {
3288
+ errors.push({
3289
+ code: "INVALID_WIDGET_METADATA",
3290
+ path: `${capabilityPath}.name`,
3291
+ message: `${capabilityPath}.name must be a non-empty string without surrounding whitespace or control characters.`
3292
+ });
3293
+ return;
3294
+ }
3295
+ const version = candidate.version;
3296
+ if (version !== void 0 && !isCanonicalMetadataString(version)) errors.push({
3297
+ code: "INVALID_WIDGET_METADATA",
3298
+ path: `${capabilityPath}.version`,
3299
+ message: `${capabilityPath}.version must be a non-empty string without surrounding whitespace or control characters when present.`
3300
+ });
3301
+ if (names.has(name)) {
3302
+ errors.push({
3303
+ code: "INVALID_WIDGET_METADATA",
3304
+ path: `${capabilityPath}.name`,
3305
+ message: `${path} must not contain duplicate capability name ${JSON.stringify(name)}.`
3306
+ });
3307
+ return;
3308
+ }
3309
+ names.add(name);
3310
+ });
3311
+ }
3312
+ function isCanonicalMetadataString(value) {
3313
+ return typeof value === "string" && value.length > 0 && value === value.trim() && !/\p{Cc}/u.test(value);
3314
+ }
3555
3315
  function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
3556
3316
  const generatedCssUrlSet = new Set(generatedCssUrls ?? []);
3557
3317
  generatedCssUrls?.forEach((cssUrl, index) => {
@@ -3562,7 +3322,10 @@ function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
3562
3322
  if (generatedCssUrlSet.has(cssUrl)) return;
3563
3323
  throw new Error(`cssUrls[${index}] URL ${JSON.stringify(cssUrl)} must match a generated top-level CSS artifact. Import the CSS from the widget package bundle or remove it from cssUrls.`);
3564
3324
  });
3565
- return Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []]));
3325
+ return Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []])).map(toWidgetPackageArtifactPath);
3326
+ }
3327
+ function toWidgetPackageArtifactPath(value) {
3328
+ return value;
3566
3329
  }
3567
3330
  function validateGeneratedCssArtifactUrl(value, cssUrlPath) {
3568
3331
  if (value.trim().length === 0) throw new Error(`${cssUrlPath} must be a non-empty string.`);
@@ -3639,15 +3402,7 @@ const PROJECT_VITE_CONFIG_CANDIDATES = [
3639
3402
  "vite.config.js",
3640
3403
  "vite.config.mjs"
3641
3404
  ];
3642
- const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = [
3643
- "fluid-manifest-plugin",
3644
- "fluid-preview-plugin",
3645
- "fluid-builder-preview",
3646
- "fluid-builder-preview-standalone",
3647
- "fluid-portal-dev",
3648
- "fluid-backend-dev"
3649
- ];
3650
- const WIDGET_PACKAGE_QUEUE_GLOBAL = "FluidWidgetPackageQueue";
3405
+ const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
3651
3406
  const require = createRequire(import.meta.url);
3652
3407
  function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
3653
3408
  const label = options.optionName ?? "publishDir";
@@ -3686,9 +3441,11 @@ async function buildSharedWidgetPackage(options) {
3686
3441
  try {
3687
3442
  await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
3688
3443
  const publishDir = path.resolve(options.projectDir, publishDirInput);
3444
+ const widgetScriptPath = path.join(publishDir, "widget.js");
3689
3445
  await fs.emptyDir(publishDir);
3690
3446
  const tempDir = path.resolve(options.projectDir, TEMP_BUILD_DIR);
3691
3447
  await fs.emptyDir(tempDir);
3448
+ let buildFailure;
3692
3449
  try {
3693
3450
  await buildWidgetScriptBundle({
3694
3451
  projectDir: options.projectDir,
@@ -3696,15 +3453,25 @@ async function buildSharedWidgetPackage(options) {
3696
3453
  publishDir,
3697
3454
  validated: validation.value
3698
3455
  });
3699
- } finally {
3700
- await fs.remove(tempDir).catch(() => {});
3456
+ } catch (error) {
3457
+ buildFailure = { error };
3458
+ }
3459
+ try {
3460
+ await fs.remove(tempDir);
3461
+ } catch {
3462
+ const cleanupFailure = /* @__PURE__ */ new Error("Failed to remove reserved temporary build directory.");
3463
+ if (buildFailure) {
3464
+ const primaryError = buildFailure.error instanceof Error ? buildFailure.error : new Error(String(buildFailure.error));
3465
+ throw new AggregateError([primaryError, cleanupFailure], `${primaryError.message}\n${cleanupFailure.message}`);
3466
+ }
3467
+ throw cleanupFailure;
3701
3468
  }
3469
+ if (buildFailure) throw buildFailure.error;
3702
3470
  const cssUrls = await collectCssUrls(publishDir);
3703
- const descriptor = buildWidgetPackageDescriptor(validation.value, { cssUrls });
3471
+ const descriptor = buildWidgetPackagePublishManifest(validation.value, { cssUrls });
3704
3472
  const manifestContent = stableStringify(descriptor);
3705
3473
  const publishPackageId = descriptor.packageId;
3706
3474
  const manifestPath = path.join(publishDir, "manifest.json");
3707
- const widgetScriptPath = path.join(publishDir, "widget.js");
3708
3475
  const publishManifestPath = path.join(publishDir, "publish-manifest.json");
3709
3476
  await fs.writeFile(manifestPath, manifestContent, "utf-8");
3710
3477
  const artifacts = await collectWidgetPackageArtifacts(publishDir);
@@ -3719,7 +3486,7 @@ async function buildSharedWidgetPackage(options) {
3719
3486
  await fs.writeFile(publishManifestPath, stableStringify(publishManifest), "utf-8");
3720
3487
  return success({
3721
3488
  publishDir,
3722
- packageId: getWidgetPackageDescriptorPackageId(validation.value),
3489
+ packageId: validation.value.packageId,
3723
3490
  version: validation.value.version,
3724
3491
  manifestPath,
3725
3492
  publishManifestPath,
@@ -3736,13 +3503,12 @@ async function buildSharedWidgetPackage(options) {
3736
3503
  }
3737
3504
  }
3738
3505
  function createWidgetPackageEntrySource(options) {
3739
- const runtimePackageId = getWidgetPackageDescriptorPackageId(options.validated);
3740
- return `import * as widgetConfig from ${JSON.stringify(options.configImportPath)};
3741
- import { createElement } from "react";
3506
+ return `import { createElement } from "react";
3507
+ import { startWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
3508
+ ${options.cssImportPath ? `import ${JSON.stringify(options.cssImportPath)};` : ""}
3742
3509
 
3510
+ const widgetConfig = await import(${JSON.stringify(options.configImportPath)});
3743
3511
  const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
3744
- const WIDGET_PACKAGE_ID = ${JSON.stringify(runtimePackageId)};
3745
- const WIDGET_PACKAGE_VERSION = ${JSON.stringify(options.validated.version)};
3746
3512
  const descriptors = ${JSON.stringify(options.validated.widgets, null, 2)};
3747
3513
 
3748
3514
  function isSourceWidgetPackage(value) {
@@ -3759,50 +3525,25 @@ function collectSourceWidgetPackages(mod) {
3759
3525
 
3760
3526
  const sourcePackage = collectSourceWidgetPackages(widgetConfig)[0];
3761
3527
  if (!sourcePackage) {
3762
- throw new Error("No Fluid source widget package found while loading widget package bundle.");
3528
+ throw new Error("No Fluid source widget package found while loading widget package worker bundle.");
3763
3529
  }
3764
3530
 
3765
3531
  const componentsByName = new Map(
3766
3532
  sourcePackage.widgets.map((widget) => [widget.name, widget.component]),
3767
3533
  );
3768
3534
 
3769
- function createScopedWidgetComponent(component, descriptor) {
3770
- const wrapperTagName = descriptor.container === "inline" ? "span" : "div";
3771
- const WrappedFluidWidgetComponent = (props) => createElement(
3772
- wrapperTagName,
3773
- {
3774
- "data-fluid-widget-package": WIDGET_PACKAGE_ID,
3775
- "data-fluid-widget-version": WIDGET_PACKAGE_VERSION,
3776
- "data-fluid-widget-name": descriptor.name,
3777
- },
3778
- // PortalContainerProvider is not imported here because standalone widget packages
3779
- // only depend on @fluid-app/portal-sdk, which does not currently expose a
3780
- // public portal-container provider. Radix/ui-primitives portals remain a
3781
- // residual isolation risk until a public widget-safe provider is available.
3782
- createElement(component, props),
3783
- );
3784
- WrappedFluidWidgetComponent.displayName = "FluidWidgetPackage(" + WIDGET_PACKAGE_ID + "/" + descriptor.name + ")";
3785
- return WrappedFluidWidgetComponent;
3786
- }
3787
-
3788
3535
  const widgets = descriptors.map((descriptor) => {
3789
3536
  const component = componentsByName.get(descriptor.name);
3790
- if (typeof component !== "function" && typeof component !== "object") {
3791
- throw new Error("Widget package is missing component for " + descriptor.name + ".");
3537
+ if (typeof component !== "function") {
3538
+ throw new Error("Widget package is missing Remote DOM component for " + descriptor.name + ".");
3792
3539
  }
3793
- return { ...descriptor, component: createScopedWidgetComponent(component, descriptor) };
3540
+ return {
3541
+ ...descriptor,
3542
+ component: (props) => createElement(component, props),
3543
+ };
3794
3544
  });
3795
3545
 
3796
- var _fluidWidgets = globalObject.FluidWidgets;
3797
- if (!_fluidWidgets || typeof _fluidWidgets.registerPackage !== "function") {
3798
- throw new Error("FluidWidgets registry is not installed.");
3799
- }
3800
-
3801
- _fluidWidgets.registerPackage({
3802
- packageId: ${JSON.stringify(runtimePackageId)},
3803
- version: ${JSON.stringify(options.validated.version)},
3804
- widgets,
3805
- });
3546
+ startWidgetPackage({ widgets });
3806
3547
  `;
3807
3548
  }
3808
3549
  async function buildWidgetScriptBundle(options) {
@@ -3810,14 +3551,14 @@ async function buildWidgetScriptBundle(options) {
3810
3551
  if (!sourceConfig) throw new Error("No portal widget source config found.");
3811
3552
  const entryPath = path.join(options.tempDir, "widget-entry.ts");
3812
3553
  const viteConfigPath = path.join(options.tempDir, "vite.config.mjs");
3554
+ const rootStylesPath = path.join(options.projectDir, "styles.css");
3813
3555
  await fs.writeFile(entryPath, createWidgetPackageEntrySource({
3814
3556
  configImportPath: pathToFileURL(sourceConfig.path).href,
3557
+ cssImportPath: await fs.pathExists(rootStylesPath) ? pathToFileURL(rootStylesPath).href : void 0,
3815
3558
  validated: options.validated
3816
3559
  }), "utf-8");
3817
3560
  await fs.writeFile(viteConfigPath, createViteConfigSource({
3818
3561
  entryPath,
3819
- packageId: getWidgetPackageDescriptorPackageId(options.validated),
3820
- packageVersion: options.validated.version,
3821
3562
  publishDir: options.publishDir,
3822
3563
  projectConfigPath: await resolveProjectViteConfigPath(options.projectDir)
3823
3564
  }), "utf-8");
@@ -3832,43 +3573,9 @@ async function buildWidgetScriptBundle(options) {
3832
3573
  stdio: "pipe",
3833
3574
  env: {
3834
3575
  ...process.env,
3835
- NODE_ENV: "production",
3836
- NODE_OPTIONS: withFluidWidgetAuthoringNodeCondition(process.env.NODE_OPTIONS)
3576
+ NODE_ENV: "production"
3837
3577
  }
3838
3578
  });
3839
- const widgetScriptPath = path.join(options.publishDir, "widget.js");
3840
- const widgetScriptContent = await fs.readFile(widgetScriptPath, "utf-8");
3841
- await fs.writeFile(widgetScriptPath, wrapWidgetPackageScript(widgetScriptContent), "utf-8");
3842
- }
3843
- function wrapWidgetPackageScript(widgetScriptContent) {
3844
- return `;(function () {
3845
- var globalObject = window;
3846
- var runWidgetPackage = function fluidWidgetPackageFactory() {
3847
- // Intentionally shadow Rollup's expected FluidShared global with the wrapper-scoped host globals.
3848
- var FluidShared = globalObject.FluidShared;
3849
- if (!FluidShared || !FluidShared.PortalSdk) {
3850
- throw new Error("window.FluidShared.PortalSdk is not installed.");
3851
- }
3852
- ${indentScript(widgetScriptContent, " ")}
3853
- };
3854
- runWidgetPackage.__fluidRequiresPortalSdk = true;
3855
-
3856
- if (globalObject.FluidShared && globalObject.FluidShared.PortalSdk) {
3857
- runWidgetPackage();
3858
- return;
3859
- }
3860
-
3861
- var queue = globalObject.${WIDGET_PACKAGE_QUEUE_GLOBAL};
3862
- if (!Array.isArray(queue)) {
3863
- queue = [];
3864
- globalObject.${WIDGET_PACKAGE_QUEUE_GLOBAL} = queue;
3865
- }
3866
- queue.push(runWidgetPackage);
3867
- })();
3868
- `;
3869
- }
3870
- function indentScript(source, indent) {
3871
- return source.split("\n").map((line) => line.length > 0 ? `${indent}${line}` : line).join("\n");
3872
3579
  }
3873
3580
  function resolveViteCliPath(projectDir) {
3874
3581
  const projectViteCliPath = resolveViteCliPathFromRequire(createRequire(path.join(projectDir, "package.json")));
@@ -3896,6 +3603,9 @@ export default defineConfig(${widgetConfigSource});
3896
3603
 
3897
3604
  const projectConfigPath = ${JSON.stringify(options.projectConfigPath)};
3898
3605
  const widgetConfig = ${widgetConfigSource};
3606
+ const fluidWidgetDefineInvariants = {
3607
+ "process.env.NODE_ENV": JSON.stringify("production"),
3608
+ };
3899
3609
  const blockedFluidPluginNames = new Set(${JSON.stringify(BLOCKED_FLUID_PROJECT_PLUGIN_NAMES)});
3900
3610
 
3901
3611
  function isAllowedProjectPlugin(plugin) {
@@ -3936,30 +3646,35 @@ function sanitizeProjectConfig(config) {
3936
3646
  return sanitized;
3937
3647
  }
3938
3648
 
3649
+ function copyConfigDefineValue(value) {
3650
+ return value && typeof value === "object" && !Array.isArray(value)
3651
+ ? { ...value }
3652
+ : {};
3653
+ }
3654
+
3655
+ function mergeFluidWidgetDefineInvariants(mergedConfig, safeProjectConfig, widgetConfig) {
3656
+ mergedConfig.define = {
3657
+ ...copyConfigDefineValue(safeProjectConfig.define),
3658
+ ...copyConfigDefineValue(mergedConfig.define),
3659
+ ...copyConfigDefineValue(widgetConfig.define),
3660
+ ...fluidWidgetDefineInvariants,
3661
+ };
3662
+ return mergedConfig;
3663
+ }
3664
+
3939
3665
  export default defineConfig(async (configEnv) => {
3940
3666
  const projectConfig = await loadConfigFromFile(configEnv, projectConfigPath);
3941
3667
  const safeProjectConfig = sanitizeProjectConfig(projectConfig?.config ?? {});
3942
- return mergeConfig(safeProjectConfig, widgetConfig);
3668
+ return mergeFluidWidgetDefineInvariants(mergeConfig(safeProjectConfig, widgetConfig), safeProjectConfig, widgetConfig);
3943
3669
  });
3944
3670
  `;
3945
3671
  }
3946
3672
  function createWidgetViteConfigObjectSource(options) {
3947
3673
  const configFileSource = options.disableConfigFileLookup ? " configFile: false,\n" : "";
3948
3674
  return `(() => {
3949
- const widgetPackageId = ${JSON.stringify(options.packageId)};
3950
- const widgetPackageVersion = ${JSON.stringify(options.packageVersion)};
3951
- const widgetPackageScopeSelector = '[data-fluid-widget-package="' + widgetPackageId + '"][data-fluid-widget-version="' + widgetPackageVersion + '"]';
3952
- const widgetPackageGeneratedCssFileName = "widget.css";
3953
- const widgetPackageKeyframePrefix = "fluid-widget-" + encodeCssIdentifier(widgetPackageId) + "-" + encodeCssIdentifier(widgetPackageVersion) + "-";
3954
- const widgetPackageLayerPrefix = widgetPackageKeyframePrefix + "layer-";
3955
- const widgetPackageHostDefaultLayerName = widgetPackageLayerPrefix + "generated-host-defaults";
3956
- const widgetPackageHostDefaultRule = "@layer " + widgetPackageHostDefaultLayerName + " { " + widgetPackageScopeSelector + " { display: contents; } }";
3957
- const widgetPackageContainerPrefix = widgetPackageKeyframePrefix + "container-";
3958
- const widgetPackageCssGlobalNamePrefix = widgetPackageKeyframePrefix + "css-name-";
3959
- const groupingAtRules = new Set(["media", "supports", "container", "document", "starting-style"]);
3960
- const unsafeSelectorPattern = /(^|[\\s>+~,])(:root|html|body)(?=$|[\\s.#:[>+~,)])/i;
3961
- const universalSelectorPattern = /(^|[\\s>+~,])\\*(?=$|[\\s.#:[>+~,)])/g;
3962
- const pseudoElementOnlySelectorPattern = /^::[A-Za-z-]+(?:\\([^)]*\\))?$/;
3675
+ const fluidWidgetDefineInvariants = {
3676
+ "process.env.NODE_ENV": JSON.stringify("production"),
3677
+ };
3963
3678
 
3964
3679
  function createFluidWidgetBuildInvariants() {
3965
3680
  return {
@@ -3967,26 +3682,18 @@ function createWidgetViteConfigObjectSource(options) {
3967
3682
  emptyOutDir: false,
3968
3683
  outDir: ${JSON.stringify(options.publishDir)},
3969
3684
  sourcemap: false,
3685
+ target: "esnext",
3686
+ cssCodeSplit: false,
3970
3687
  lib: {
3971
3688
  entry: ${JSON.stringify(options.entryPath)},
3972
- formats: ["iife"],
3973
- name: "FluidWidgetPackage",
3689
+ formats: ["es"],
3974
3690
  fileName: () => "widget.js",
3691
+ cssFileName: "widget",
3975
3692
  },
3976
3693
  rollupOptions: {
3977
- external: [
3978
- "react",
3979
- "react-dom",
3980
- "react/jsx-runtime",
3981
- "@fluid-app/portal-sdk",
3982
- ],
3983
3694
  output: {
3984
- globals: {
3985
- react: "FluidShared.React",
3986
- "react-dom": "FluidShared.ReactDOM",
3987
- "react/jsx-runtime": "FluidShared.ReactJsxRuntime",
3988
- "@fluid-app/portal-sdk": "FluidShared.PortalSdk",
3989
- },
3695
+ inlineDynamicImports: true,
3696
+ hoistTransitiveImports: false,
3990
3697
  },
3991
3698
  },
3992
3699
  };
@@ -4000,1179 +3707,26 @@ function createWidgetViteConfigObjectSource(options) {
4000
3707
  return { build: createFluidWidgetBuildInvariants() };
4001
3708
  },
4002
3709
  configResolved(config) {
3710
+ config.define = {
3711
+ ...(config.define ?? {}),
3712
+ ...fluidWidgetDefineInvariants,
3713
+ };
4003
3714
  Object.assign(config.build, createFluidWidgetBuildInvariants());
4004
3715
  },
4005
3716
  };
4006
3717
  }
4007
3718
 
4008
- function fluidWidgetCssIsolationPlugin() {
4009
- return {
4010
- name: "fluid-widget-css-isolation",
4011
- enforce: "post",
4012
- generateBundle(_outputOptions, bundle) {
4013
- let hasCssAsset = false;
4014
- for (const asset of Object.values(bundle)) {
4015
- if (!asset || asset.type !== "asset" || !asset.fileName.endsWith(".css")) continue;
4016
- hasCssAsset = true;
4017
- const source = typeof asset.source === "string" ? asset.source : new TextDecoder().decode(asset.source);
4018
- asset.source = isolateWidgetCssAsset(source, asset.fileName);
4019
- }
4020
- if (!hasCssAsset) {
4021
- this.emitFile({
4022
- type: "asset",
4023
- fileName: widgetPackageGeneratedCssFileName,
4024
- source: widgetPackageHostDefaultRule + "\\n",
4025
- });
4026
- }
4027
- },
4028
- };
4029
- }
4030
-
4031
- function isolateWidgetCssAsset(css, assetName) {
4032
- const keyframeNames = collectKeyframeNames(css);
4033
- const fontFamilyNames = collectFontFamilyNames(css);
4034
- const customPropertyNames = collectRegisteredCustomPropertyNames(css);
4035
- const placementSensitivePrefix = readPlacementSensitiveCssPrefix(css, assetName);
4036
- const isolatedCss = isolateCssRange(css, assetName, keyframeNames, fontFamilyNames, customPropertyNames, placementSensitivePrefix.endIndex, css.length).css;
4037
- return placementSensitivePrefix.css + widgetPackageHostDefaultRule + "\\n" + isolatedCss;
4038
- }
4039
-
4040
- function collectKeyframeNames(css) {
4041
- const names = new Map();
4042
- visitCssAtRules(css, (atRuleName, atRule) => {
4043
- if (!isKeyframesAtRule(atRuleName)) return false;
4044
- const match = /^@(?:-[A-Za-z]+-)?keyframes\\s+([^\\s{]+)/i.exec(atRule.prelude);
4045
- const name = match?.[1];
4046
- if (name && !names.has(name)) names.set(name, widgetPackageKeyframePrefix + name);
4047
- return atRule.blockStart !== -1;
4048
- });
4049
- return names;
4050
- }
4051
-
4052
- function collectFontFamilyNames(css) {
4053
- const names = new Map();
4054
- visitCssAtRules(css, (atRuleName, atRule) => {
4055
- if (atRuleName !== "font-face" || atRule.blockStart === -1 || atRule.blockEnd === -1) return false;
4056
- let familyName = "";
4057
- rewriteDeclarationBlock(css.slice(atRule.blockStart + 1, atRule.blockEnd), (propertyName, rawProperty, value) => {
4058
- if (propertyName === "font-family" && !familyName) {
4059
- familyName = normalizeFontFamilyName(value);
4060
- }
4061
- return { property: rawProperty, value };
4062
- });
4063
- if (familyName && !names.has(familyName)) names.set(familyName, widgetPackageKeyframePrefix + familyName);
4064
- return true;
4065
- });
4066
- return names;
4067
- }
4068
-
4069
- function collectRegisteredCustomPropertyNames(css) {
4070
- const names = new Map();
4071
- visitCssAtRules(css, (atRuleName, atRule) => {
4072
- if (atRuleName !== "property") return false;
4073
- const match = /^@property\\s+(--[A-Za-z0-9_-]+)/i.exec(atRule.prelude);
4074
- const name = match?.[1];
4075
- if (name && !names.has(name)) names.set(name, "--" + widgetPackageKeyframePrefix + name.slice(2));
4076
- return atRule.blockStart !== -1;
4077
- });
4078
- return names;
4079
- }
4080
-
4081
- function visitCssAtRules(css, visitAtRule) {
4082
- let index = 0;
4083
- while (index < css.length) {
4084
- if (cssStartsWithComment(css, index)) {
4085
- const commentEnd = css.indexOf("*/", index + 2);
4086
- index = commentEnd === -1 ? css.length : commentEnd + 2;
4087
- continue;
4088
- }
4089
- const char = css[index];
4090
- if (char === '"' || char === "'") {
4091
- index = findCssStringEnd(css, index);
4092
- continue;
4093
- }
4094
- if (char !== "@") {
4095
- index += 1;
4096
- continue;
4097
- }
4098
- const atRule = readRulePrelude(css, index, css.length);
4099
- const blockEnd = atRule.blockStart === -1 ? -1 : findMatchingBrace(css, atRule.blockStart, css.length);
4100
- const shouldSkipBlock = visitAtRule(readAtRuleName(atRule.prelude), { ...atRule, blockEnd });
4101
- index = shouldSkipBlock && blockEnd !== -1 ? blockEnd + 1 : index + 1;
4102
- }
4103
- }
4104
-
4105
- function isolateCssRange(css, assetName, keyframeNames, fontFamilyNames, customPropertyNames, startIndex, endIndex) {
4106
- let output = "";
4107
- let index = startIndex;
4108
- while (index < endIndex) {
4109
- if (css.startsWith("/*", index)) {
4110
- const commentEnd = css.indexOf("*/", index + 2);
4111
- const nextIndex = commentEnd === -1 ? endIndex : commentEnd + 2;
4112
- output += css.slice(index, nextIndex);
4113
- index = nextIndex;
4114
- continue;
4115
- }
4116
-
4117
- if (/\\s/.test(css[index] ?? "")) {
4118
- output += css[index];
4119
- index += 1;
4120
- continue;
4121
- }
4122
-
4123
- if (css[index] === "@") {
4124
- const atRule = readRulePrelude(css, index, endIndex);
4125
- const atRuleName = readAtRuleName(atRule.prelude);
4126
- if (atRule.blockStart === -1) {
4127
- if (atRuleName === "import") {
4128
- throw new Error('Unsupported @import rule in CSS asset "' + assetName + '" cannot be isolated safely. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them.');
4129
- }
4130
- if (atRuleName === "charset" || atRuleName === "namespace") {
4131
- throw new Error('Unsupported @' + atRuleName + ' rule in CSS asset "' + assetName + '" cannot be isolated safely after generated host defaults. Move @' + atRuleName + ' before all non-@charset/@namespace rules.');
4132
- }
4133
- if (atRuleName === "layer") {
4134
- output += rewriteLayerPrelude(atRule.prelude) + css.slice(index + atRule.prelude.length, atRule.end);
4135
- index = atRule.end;
4136
- continue;
4137
- }
4138
- output += css.slice(index, atRule.end);
4139
- index = atRule.end;
4140
- continue;
4141
- }
4142
-
4143
- const blockEnd = findMatchingBrace(css, atRule.blockStart, endIndex);
4144
- if (blockEnd === -1) {
4145
- output += css.slice(index, endIndex);
4146
- index = endIndex;
4147
- continue;
4148
- }
4149
-
4150
- const ruleHeader = css.slice(index, atRule.blockStart + 1);
4151
- if (isKeyframesAtRule(atRuleName)) {
4152
- output += rewriteKeyframesHeader(ruleHeader, keyframeNames) + namespaceKeyframesBlock(css.slice(atRule.blockStart + 1, blockEnd), keyframeNames, fontFamilyNames, customPropertyNames, assetName) + "}";
4153
- } else if (atRuleName === "font-face") {
4154
- output += ruleHeader + namespaceFontFaceBlock(css.slice(atRule.blockStart + 1, blockEnd), fontFamilyNames, customPropertyNames) + "}";
4155
- } else if (atRuleName === "property") {
4156
- output += rewritePropertyHeader(ruleHeader, customPropertyNames) + namespaceCustomPropertyReferences(css.slice(atRule.blockStart + 1, blockEnd), customPropertyNames) + "}";
4157
- } else if (atRuleName === "layer") {
4158
- output += rewriteLayerPrelude(atRule.prelude) + css.slice(index + atRule.prelude.length, atRule.blockStart + 1) + isolateCssRange(css, assetName, keyframeNames, fontFamilyNames, customPropertyNames, atRule.blockStart + 1, blockEnd).css + "}";
4159
- } else if (groupingAtRules.has(atRuleName)) {
4160
- const groupingRuleHeader = atRuleName === "container" ? rewriteContainerAtRuleHeader(ruleHeader) : ruleHeader;
4161
- output += groupingRuleHeader + isolateCssRange(css, assetName, keyframeNames, fontFamilyNames, customPropertyNames, atRule.blockStart + 1, blockEnd).css + "}";
4162
- } else {
4163
- throw new Error('Unsupported @' + atRuleName + ' rule in CSS asset "' + assetName + '" cannot be isolated safely.');
4164
- }
4165
- index = blockEnd + 1;
4166
- continue;
4167
- }
4168
-
4169
- const styleRule = readRulePrelude(css, index, endIndex);
4170
- if (styleRule.blockStart === -1) {
4171
- output += css.slice(index, styleRule.end);
4172
- index = styleRule.end;
4173
- continue;
4174
- }
4175
-
4176
- const styleBlockEnd = findMatchingBrace(css, styleRule.blockStart, endIndex);
4177
- if (styleBlockEnd === -1) {
4178
- output += css.slice(index, endIndex);
4179
- index = endIndex;
4180
- continue;
4181
- }
4182
-
4183
- output += isolateSelectorList(styleRule.prelude, assetName) + "{" + namespaceStyleBlock(css.slice(styleRule.blockStart + 1, styleBlockEnd), keyframeNames, fontFamilyNames, customPropertyNames, assetName) + "}";
4184
- index = styleBlockEnd + 1;
4185
- }
4186
-
4187
- return { css: output, index };
4188
- }
4189
-
4190
- function readPlacementSensitiveCssPrefix(css, assetName) {
4191
- let output = "";
4192
- let index = 0;
4193
- let hasNamespace = false;
4194
- while (index < css.length) {
4195
- const leadingStart = index;
4196
- index = skipCssWhitespaceAndComments(css, index);
4197
- const leading = css.slice(leadingStart, index);
4198
- if (css[index] !== "@") {
4199
- return { css: output + leading, endIndex: index };
4200
- }
4201
-
4202
- const atRule = readRulePrelude(css, index, css.length);
4203
- const atRuleName = readAtRuleName(atRule.prelude);
4204
- if (atRuleName !== "charset" && atRuleName !== "namespace") {
4205
- return { css: output + leading, endIndex: index };
4206
- }
4207
- if (atRule.blockStart !== -1) {
4208
- throw new Error('Unsupported @' + atRuleName + ' rule in CSS asset "' + assetName + '" cannot be isolated safely.');
4209
- }
4210
- if (atRuleName === "charset" && hasNamespace) {
4211
- throw new Error('Unsupported @charset rule in CSS asset "' + assetName + '" cannot be isolated safely after @namespace. Move @charset before @namespace.');
4212
- }
4213
- if (atRuleName === "namespace") hasNamespace = true;
4214
- output += leading + css.slice(index, atRule.end);
4215
- index = atRule.end;
4216
- }
4217
- return { css: output, endIndex: index };
4218
- }
4219
-
4220
- function readRulePrelude(css, startIndex, endIndex) {
4221
- let index = startIndex;
4222
- let quote = "";
4223
- let bracketDepth = 0;
4224
- let parenDepth = 0;
4225
- while (index < endIndex) {
4226
- const char = css[index];
4227
- if (quote) {
4228
- if (char === "\\\\") index += 2;
4229
- else {
4230
- if (char === quote) quote = "";
4231
- index += 1;
4232
- }
4233
- continue;
4234
- }
4235
- if (cssStartsWithComment(css, index)) {
4236
- const commentEnd = css.indexOf("*/", index + 2);
4237
- index = commentEnd === -1 ? endIndex : Math.min(commentEnd + 2, endIndex);
4238
- continue;
4239
- }
4240
- if (char === '"' || char === "'") {
4241
- quote = char;
4242
- index += 1;
4243
- continue;
4244
- }
4245
- if (char === "[") bracketDepth += 1;
4246
- else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
4247
- else if (char === "(") parenDepth += 1;
4248
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
4249
- else if (bracketDepth === 0 && parenDepth === 0 && char === "{") {
4250
- return { prelude: css.slice(startIndex, index), blockStart: index, end: index + 1 };
4251
- } else if (bracketDepth === 0 && parenDepth === 0 && char === ";") {
4252
- return { prelude: css.slice(startIndex, index), blockStart: -1, end: index + 1 };
4253
- }
4254
- index += 1;
4255
- }
4256
- return { prelude: css.slice(startIndex, endIndex), blockStart: -1, end: endIndex };
4257
- }
4258
-
4259
- function findMatchingBrace(css, openBraceIndex, endIndex) {
4260
- let depth = 1;
4261
- let index = openBraceIndex + 1;
4262
- let quote = "";
4263
- while (index < endIndex) {
4264
- const char = css[index];
4265
- if (quote) {
4266
- if (char === "\\\\") index += 2;
4267
- else {
4268
- if (char === quote) quote = "";
4269
- index += 1;
4270
- }
4271
- continue;
4272
- }
4273
- if (char === '"' || char === "'") {
4274
- quote = char;
4275
- index += 1;
4276
- continue;
4277
- }
4278
- if (css.startsWith("/*", index)) {
4279
- const commentEnd = css.indexOf("*/", index + 2);
4280
- index = commentEnd === -1 ? endIndex : commentEnd + 2;
4281
- continue;
4282
- }
4283
- if (char === "{") depth += 1;
4284
- else if (char === "}") {
4285
- depth -= 1;
4286
- if (depth === 0) return index;
4287
- }
4288
- index += 1;
4289
- }
4290
- return -1;
4291
- }
4292
-
4293
- function readAtRuleName(prelude) {
4294
- const match = /^@([A-Za-z-]+)/.exec(prelude.trim());
4295
- return match?.[1]?.toLowerCase() ?? "";
4296
- }
4297
-
4298
- function isKeyframesAtRule(atRuleName) {
4299
- return atRuleName === "keyframes" || atRuleName.endsWith("-keyframes");
4300
- }
4301
-
4302
- function rewriteKeyframesHeader(header, keyframeNames) {
4303
- return header.replace(/(@(?:-[A-Za-z]+-)?keyframes\\s+)([^\\s{]+)/, (_match, prefix, name) => prefix + (keyframeNames.get(name) ?? name));
4304
- }
4305
-
4306
- function splitSelectorList(selectorList) {
4307
- const selectors = [];
4308
- let current = "";
4309
- let quote = "";
4310
- let bracketDepth = 0;
4311
- let parenDepth = 0;
4312
- for (let index = 0; index < selectorList.length; index += 1) {
4313
- const char = selectorList[index];
4314
- if (quote) {
4315
- current += char;
4316
- if (char === "\\\\") {
4317
- index += 1;
4318
- current += selectorList[index] ?? "";
4319
- } else if (char === quote) quote = "";
4320
- continue;
4321
- }
4322
- if (char === '"' || char === "'") {
4323
- quote = char;
4324
- current += char;
4325
- continue;
4326
- }
4327
- if (cssStartsWithComment(selectorList, index)) {
4328
- const commentEnd = selectorList.indexOf("*/", index + 2);
4329
- const nextIndex = commentEnd === -1 ? selectorList.length : commentEnd + 2;
4330
- current += selectorList.slice(index, nextIndex);
4331
- index = nextIndex - 1;
4332
- continue;
4333
- }
4334
- if (char === "[") bracketDepth += 1;
4335
- else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
4336
- else if (char === "(") parenDepth += 1;
4337
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
4338
- if (char === "," && bracketDepth === 0 && parenDepth === 0) {
4339
- selectors.push(current);
4340
- current = "";
4341
- } else current += char;
4342
- }
4343
- selectors.push(current);
4344
- return selectors;
4345
- }
4346
-
4347
- function isolateSelectorList(selectorList, assetName) {
4348
- return splitSelectorList(selectorList).map((selector) => isolateSelector(selector, assetName)).join(",");
4349
- }
4350
-
4351
- function isolateSelector(selector, assetName) {
4352
- const leadingWhitespace = selector.match(/^\\s*/)?.[0] ?? "";
4353
- const trailingWhitespace = selector.match(/\\s*$/)?.[0] ?? "";
4354
- const trimmed = selector.trim();
4355
- assertSafeSelector(trimmed, assetName);
4356
- if (trimmed.includes(":host")) {
4357
- return leadingWhitespace + isolateHostSelector(trimmed, assetName) + trailingWhitespace;
4358
- }
4359
- return leadingWhitespace + widgetPackageScopeSelector + " " + trimmed + trailingWhitespace;
4360
- }
4361
-
4362
- function isolateHostSelector(selector, assetName) {
4363
- if (!selector.startsWith(":host")) {
4364
- throw new Error('Unsupported :host selector "' + selector + '" in CSS asset "' + assetName + '" cannot be isolated safely.');
4365
- }
4366
- const match = /^:host(?:\\(([^()]*)\\))?($|[\\s>+~].*)/.exec(selector);
4367
- if (!match) {
4368
- throw new Error('Unsupported :host selector "' + selector + '" in CSS asset "' + assetName + '" cannot be isolated safely.');
4369
- }
4370
- const hostSelector = (match[1] ?? "").trim();
4371
- const selectorAfterHost = match[2] ?? "";
4372
- const firstSelectorAfterHostIndex = skipCssWhitespaceAndComments(selectorAfterHost, 0);
4373
- const firstSelectorAfterHostChar = selectorAfterHost[firstSelectorAfterHostIndex] ?? "";
4374
- if (hostSelector.includes(",") || hasTopLevelHostArgumentCombinator(hostSelector) || firstSelectorAfterHostChar === "+" || firstSelectorAfterHostChar === "~" || (hostSelector && (unsafeSelectorPattern.test(hostSelector) || hasUnsafeUniversalSelector(hostSelector)))) {
4375
- throw new Error('Unsupported :host selector "' + selector + '" in CSS asset "' + assetName + '" cannot be isolated safely.');
4376
- }
4377
- return widgetPackageScopeSelector + hostSelector + selectorAfterHost;
4378
- }
4379
-
4380
- function hasTopLevelHostArgumentCombinator(value) {
4381
- let quote = "";
4382
- let bracketDepth = 0;
4383
- let hasSelectorBeforeWhitespace = false;
4384
- let index = 0;
4385
- while (index < value.length) {
4386
- const char = value[index];
4387
- if (quote) {
4388
- if (char === "\\\\") index += 2;
4389
- else {
4390
- if (char === quote) quote = "";
4391
- index += 1;
4392
- }
4393
- continue;
4394
- }
4395
- if (char === '"' || char === "'") {
4396
- quote = char;
4397
- index += 1;
4398
- continue;
4399
- }
4400
- if (char === "[") {
4401
- bracketDepth += 1;
4402
- hasSelectorBeforeWhitespace = true;
4403
- } else if (char === "]") {
4404
- bracketDepth = Math.max(0, bracketDepth - 1);
4405
- } else if (bracketDepth === 0 && (char === "+" || char === "~" || char === ">")) {
4406
- return true;
4407
- } else if (bracketDepth === 0 && /\\s/.test(char)) {
4408
- const nextIndex = skipCssWhitespaceAndComments(value, index);
4409
- if (hasSelectorBeforeWhitespace && nextIndex < value.length) return true;
4410
- index = nextIndex;
4411
- continue;
4412
- } else if (bracketDepth === 0 && cssStartsWithComment(value, index)) {
4413
- const nextIndex = skipCssWhitespaceAndComments(value, index);
4414
- if (hasSelectorBeforeWhitespace && nextIndex < value.length) return true;
4415
- index = nextIndex;
4416
- continue;
4417
- } else if (bracketDepth === 0 && !/\\s/.test(char)) {
4418
- hasSelectorBeforeWhitespace = true;
4419
- }
4420
- index += 1;
4421
- }
4422
- return false;
4423
- }
4424
-
4425
- function assertSafeSelector(selector, assetName) {
4426
- if (!selector || unsafeSelectorPattern.test(selector) || hasUnsafeUniversalSelector(selector) || pseudoElementOnlySelectorPattern.test(selector)) {
4427
- throw new Error('Unsafe selector "' + selector + '" in CSS asset "' + assetName + '" cannot be isolated safely.');
4428
- }
4429
- }
4430
-
4431
- function hasUnsafeUniversalSelector(selector) {
4432
- universalSelectorPattern.lastIndex = 0;
4433
- let match;
4434
- while ((match = universalSelectorPattern.exec(selector)) !== null) {
4435
- const universalStart = match.index + match[1].length;
4436
- if (!hasAnchoringSelectorBefore(selector.slice(0, universalStart))) return true;
4437
- }
4438
- return false;
4439
- }
4440
-
4441
- function hasAnchoringSelectorBefore(value) {
4442
- return stripCssComments(value).replace(/[\\s>+~]+/g, "").length > 0;
4443
- }
4444
-
4445
- function namespaceKeyframesBlock(block, keyframeNames, fontFamilyNames, customPropertyNames, assetName) {
4446
- let output = "";
4447
- let index = 0;
4448
- while (index < block.length) {
4449
- if (cssStartsWithComment(block, index)) {
4450
- const commentEnd = block.indexOf("*/", index + 2);
4451
- const nextIndex = commentEnd === -1 ? block.length : commentEnd + 2;
4452
- output += block.slice(index, nextIndex);
4453
- index = nextIndex;
4454
- continue;
4455
- }
4456
- if (/\\s/.test(block[index] ?? "")) {
4457
- output += block[index];
4458
- index += 1;
4459
- continue;
4460
- }
4461
- const keyframeRule = readRulePrelude(block, index, block.length);
4462
- if (keyframeRule.blockStart === -1) {
4463
- output += block.slice(index, keyframeRule.end);
4464
- index = keyframeRule.end;
4465
- continue;
4466
- }
4467
- const keyframeBlockEnd = findMatchingBrace(block, keyframeRule.blockStart, block.length);
4468
- if (keyframeBlockEnd === -1) {
4469
- output += block.slice(index);
4470
- index = block.length;
4471
- continue;
4472
- }
4473
- output += keyframeRule.prelude + "{" + namespaceStyleBlock(block.slice(keyframeRule.blockStart + 1, keyframeBlockEnd), keyframeNames, fontFamilyNames, customPropertyNames, assetName) + "}";
4474
- index = keyframeBlockEnd + 1;
4475
- }
4476
- return output;
4477
- }
4478
-
4479
- function namespaceStyleBlock(block, keyframeNames, fontFamilyNames, customPropertyNames, assetName) {
4480
- assertNoNestedCssBlock(block, assetName);
4481
- return rewriteDeclarationBlock(block, (propertyName, rawProperty, value) => {
4482
- const nextProperty = namespaceCustomPropertyName(rawProperty, propertyName, customPropertyNames);
4483
- let nextValue = value;
4484
- if (propertyName === "animation" || propertyName === "-webkit-animation") {
4485
- nextValue = namespaceAnimationShorthandValue(nextValue, keyframeNames);
4486
- } else if (propertyName === "animation-name" || propertyName === "-webkit-animation-name") {
4487
- nextValue = namespaceAnimationNameValue(nextValue, keyframeNames);
4488
- } else if (propertyName === "font-family") {
4489
- nextValue = namespaceFontFamilyValue(nextValue, fontFamilyNames);
4490
- } else if (propertyName === "font") {
4491
- nextValue = namespaceFontShorthandValue(nextValue, fontFamilyNames, assetName);
4492
- }
4493
- if (propertyName === "container-name") {
4494
- nextValue = namespaceContainerNameValue(nextValue);
4495
- } else if (propertyName === "container") {
4496
- nextValue = namespaceContainerShorthandValue(nextValue);
4497
- }
4498
- if (propertyName === "transition" || propertyName === "transition-property" || propertyName === "will-change") {
4499
- nextValue = namespaceCustomPropertyIdentifierValue(nextValue, customPropertyNames);
4500
- }
4501
- if (propertyName === "view-transition-name" || propertyName === "scroll-timeline-name" || propertyName === "animation-timeline") {
4502
- nextValue = namespaceCssGlobalNameValue(nextValue);
4503
- }
4504
- nextValue = namespaceCustomPropertyReferencesValue(nextValue, customPropertyNames);
4505
- return { property: nextProperty, value: nextValue };
4506
- });
4507
- }
4508
-
4509
- function namespaceFontFaceBlock(block, fontFamilyNames, customPropertyNames) {
4510
- return rewriteDeclarationBlock(block, (propertyName, rawProperty, value) => {
4511
- const nextValue = propertyName === "font-family"
4512
- ? namespaceFontFamilyValue(value, fontFamilyNames)
4513
- : namespaceCustomPropertyReferencesValue(value, customPropertyNames);
4514
- return { property: rawProperty, value: nextValue };
4515
- });
4516
- }
4517
-
4518
- function namespaceAnimationNameValue(value, keyframeNames) {
4519
- return replaceCssValueOutsideProtectedRanges(value, (chunk) => {
4520
- let nextChunk = chunk;
4521
- for (const [name, scopedName] of keyframeNames) {
4522
- nextChunk = nextChunk.replace(new RegExp("(^|[^A-Za-z0-9_-])" + escapeRegExp(name) + "(?=$|[^A-Za-z0-9_-])", "g"), (_match, prefix) => prefix + scopedName);
4523
- }
4524
- return nextChunk;
4525
- });
4526
- }
4527
-
4528
- function namespaceAnimationShorthandValue(value, keyframeNames) {
4529
- if (keyframeNames.size === 0) return value;
4530
- return splitCssCommaList(value).map((item) => rewriteAnimationShorthandItem(item, keyframeNames)).join(",");
4531
- }
4532
-
4533
- function rewriteAnimationShorthandItem(item, keyframeNames) {
4534
- const tokens = readCssValueTokens(item);
4535
- const reservedState = createAnimationShorthandReservedState();
4536
- let output = "";
4537
- let lastIndex = 0;
4538
- for (const token of tokens) {
4539
- const scopedName = keyframeNames.get(token.text);
4540
- const isReservedToken = consumeAnimationShorthandReservedToken(token.text, reservedState);
4541
- if (!scopedName || isReservedToken) continue;
4542
- output += item.slice(lastIndex, token.start) + scopedName;
4543
- lastIndex = token.end;
4544
- }
4545
- return output + item.slice(lastIndex);
4546
- }
4547
-
4548
- function createAnimationShorthandReservedState() {
4549
- return {
4550
- duration: false,
4551
- delay: false,
4552
- timingFunction: false,
4553
- iterationCount: false,
4554
- direction: false,
4555
- fillMode: false,
4556
- playState: false,
4557
- };
4558
- }
4559
-
4560
- function consumeAnimationShorthandReservedToken(token, state) {
4561
- const lower = token.toLowerCase();
4562
- if (isAnimationTimeToken(token)) {
4563
- if (!state.duration) {
4564
- state.duration = true;
4565
- return true;
4566
- }
4567
- if (!state.delay) {
4568
- state.delay = true;
4569
- return true;
4570
- }
4571
- return false;
4572
- }
4573
- if (!state.timingFunction && isAnimationTimingFunctionToken(lower)) {
4574
- state.timingFunction = true;
4575
- return true;
4576
- }
4577
- if (!state.iterationCount && isAnimationIterationCountToken(token, lower)) {
4578
- state.iterationCount = true;
4579
- return true;
4580
- }
4581
- if (!state.direction && animationDirectionKeywords.has(lower)) {
4582
- state.direction = true;
4583
- return true;
4584
- }
4585
- if (!state.fillMode && animationFillModeKeywords.has(lower)) {
4586
- state.fillMode = true;
4587
- return true;
4588
- }
4589
- if (!state.playState && animationPlayStateKeywords.has(lower)) {
4590
- state.playState = true;
4591
- return true;
4592
- }
4593
- if (animationGlobalKeywords.has(lower)) return true;
4594
- return false;
4595
- }
4596
-
4597
- function isAnimationTimeToken(token) {
4598
- return /^-?(?:\\d+|\\d*\\.\\d+)(?:ms|s)$/i.test(token);
4599
- }
4600
-
4601
- function isAnimationTimingFunctionToken(lowerToken) {
4602
- return animationTimingFunctionKeywords.has(lowerToken) || lowerToken.includes("(");
4603
- }
4604
-
4605
- function isAnimationIterationCountToken(token, lowerToken) {
4606
- return lowerToken === "infinite" || /^-?(?:\\d+|\\d*\\.\\d+)$/.test(token);
4607
- }
4608
-
4609
- const animationTimingFunctionKeywords = new Set([
4610
- "ease",
4611
- "ease-in",
4612
- "ease-in-out",
4613
- "ease-out",
4614
- "linear",
4615
- "step-end",
4616
- "step-start",
4617
- ]);
4618
- const animationDirectionKeywords = new Set(["normal", "reverse", "alternate", "alternate-reverse"]);
4619
- const animationFillModeKeywords = new Set(["none", "forwards", "backwards", "both"]);
4620
- const animationPlayStateKeywords = new Set(["running", "paused"]);
4621
- const animationGlobalKeywords = new Set(["inherit", "initial", "revert", "revert-layer", "unset"]);
4622
-
4623
- function namespaceFontFamilyValue(value, fontFamilyNames) {
4624
- if (fontFamilyNames.size === 0) return value;
4625
- const important = splitCssImportantSuffix(value);
4626
- return splitCssCommaList(important.value).map((family) => namespaceSingleFontFamily(family, fontFamilyNames)).join(",") + important.suffix;
4627
- }
4628
-
4629
- function namespaceFontShorthandValue(value, fontFamilyNames, assetName) {
4630
- if (fontFamilyNames.size === 0) return value;
4631
- const familyStart = findFontShorthandFamilyStart(value);
4632
- if (familyStart === -1) {
4633
- if (fontShorthandReferencesScopedFontFamily(value, fontFamilyNames)) {
4634
- throw new Error('Unsupported font shorthand in CSS asset "' + assetName + '" cannot be isolated safely. Use font-family with font-style/font-weight/font-size longhands, or use a supported font shorthand with an explicit font-size before the family list.');
4635
- }
4636
- return value;
4637
- }
4638
- return value.slice(0, familyStart) + namespaceFontFamilyValue(value.slice(familyStart), fontFamilyNames);
4639
- }
4640
-
4641
- function fontShorthandReferencesScopedFontFamily(value, fontFamilyNames) {
4642
- const tokens = readCssValueTokens(value);
4643
- return tokens.some((token) => fontFamilyNames.has(normalizeFontFamilyName(token.text)));
4644
- }
4645
-
4646
- function splitCssImportantSuffix(value) {
4647
- const match = /!\\s*important\\s*$/i.exec(value);
4648
- if (!match) return { value, suffix: "" };
4649
- return {
4650
- value: value.slice(0, match.index),
4651
- suffix: value.slice(match.index),
4652
- };
4653
- }
4654
-
4655
- function namespaceSingleFontFamily(family, fontFamilyNames) {
4656
- const leadingWhitespace = family.match(/^\\s*/)?.[0] ?? "";
4657
- const trailingWhitespace = family.match(/\\s*$/)?.[0] ?? "";
4658
- const trimmed = family.trim();
4659
- const normalized = normalizeFontFamilyName(trimmed);
4660
- const scopedName = fontFamilyNames.get(normalized);
4661
- if (!scopedName) return family;
4662
- const quote = trimmed[0] === '"' || trimmed[0] === "'" ? trimmed[0] : "";
4663
- return leadingWhitespace + (quote ? quote + scopedName + quote : scopedName) + trailingWhitespace;
4664
- }
4665
-
4666
- function normalizeFontFamilyName(value) {
4667
- const trimmed = value.trim();
4668
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
4669
- return trimmed.slice(1, -1);
4670
- }
4671
- return trimmed;
4672
- }
4673
-
4674
- function rewritePropertyHeader(header, customPropertyNames) {
4675
- let nextHeader = header;
4676
- for (const [name, scopedName] of customPropertyNames) {
4677
- nextHeader = nextHeader.replace(new RegExp("(@property\\\\s+)" + escapeRegExp(name) + "(?=$|[\\\\s{])", "g"), (_match, prefix) => prefix + scopedName);
4678
- }
4679
- return nextHeader;
4680
- }
4681
-
4682
- function rewriteLayerPrelude(prelude) {
4683
- const match = /^(@layer\\b)([\\s\\S]*)$/i.exec(prelude);
4684
- if (!match) return prelude;
4685
- const layerList = match[2] ?? "";
4686
- if (!layerList.trim()) return prelude;
4687
- return match[1] + splitCssCommaList(layerList).map(namespaceLayerName).join(",");
4688
- }
4689
-
4690
- function namespaceLayerName(layerName) {
4691
- const leadingWhitespace = layerName.match(/^\\s*/)?.[0] ?? "";
4692
- const trailingWhitespace = layerName.match(/\\s*$/)?.[0] ?? "";
4693
- const trimmed = layerName.trim();
4694
- return trimmed ? leadingWhitespace + widgetPackageLayerPrefix + trimmed + trailingWhitespace : layerName;
4695
- }
4696
-
4697
- function rewriteContainerAtRuleHeader(header) {
4698
- if (!header.endsWith("{")) return header;
4699
- return rewriteContainerAtRulePrelude(header.slice(0, -1)) + "{";
4700
- }
4701
-
4702
- function rewriteContainerAtRulePrelude(prelude) {
4703
- const match = /^(@container\\b)([\\s\\S]*)$/i.exec(prelude);
4704
- if (!match) return prelude;
4705
- const condition = match[2] ?? "";
4706
- const leadingWhitespace = condition.match(/^\\s*/)?.[0] ?? "";
4707
- const conditionBody = condition.slice(leadingWhitespace.length);
4708
- if (!conditionBody || conditionBody.startsWith("(") || /^(?:style|scroll-state)\\s*\\(/.test(conditionBody)) {
4709
- return prelude;
4710
- }
4711
-
4712
- const tokens = readCssValueTokens(conditionBody);
4713
- const nameToken = tokens[0];
4714
- if (!nameToken || nameToken.start !== 0 || isContainerQueryConditionKeyword(nameToken.text) || isReservedContainerName(nameToken.text) || nameToken.text.includes("(")) {
4715
- return prelude;
4716
- }
4717
- return match[1] + leadingWhitespace + namespaceContainerName(nameToken.text) + conditionBody.slice(nameToken.end);
4718
- }
4719
-
4720
- function namespaceContainerNameValue(value) {
4721
- return replaceCssValueOutsideProtectedRanges(value, (chunk) => namespaceContainerNamesInChunk(chunk));
4722
- }
4723
-
4724
- function namespaceContainerShorthandValue(value) {
4725
- const slashIndex = findTopLevelSlash(value);
4726
- if (slashIndex !== -1) {
4727
- return namespaceContainerNameValue(value.slice(0, slashIndex)) + value.slice(slashIndex);
4728
- }
4729
-
4730
- const trimmed = stripCssComments(value).trim().toLowerCase();
4731
- if (isReservedContainerName(trimmed) || containerTypeKeywords.has(trimmed)) return value;
4732
- return namespaceContainerNameValue(value);
4733
- }
4734
-
4735
- function namespaceContainerNamesInChunk(chunk) {
4736
- return chunk.replace(/(^|[^A-Za-z0-9_-])([A-Za-z_][A-Za-z0-9_-]*|-[A-Za-z_][A-Za-z0-9_-]*)(?=$|[^A-Za-z0-9_-])/g, (_match, prefix, name) => {
4737
- if (isReservedContainerName(name)) return prefix + name;
4738
- return prefix + namespaceContainerName(name);
4739
- });
4740
- }
4741
-
4742
- function namespaceContainerName(name) {
4743
- return widgetPackageContainerPrefix + name;
4744
- }
4745
-
4746
- function isReservedContainerName(name) {
4747
- return containerNameReservedKeywords.has(name.toLowerCase());
4748
- }
4749
-
4750
- function isContainerQueryConditionKeyword(name) {
4751
- return containerQueryConditionKeywords.has(name.toLowerCase());
4752
- }
4753
-
4754
- const containerTypeKeywords = new Set(["normal", "size", "inline-size", "scroll-state"]);
4755
- const containerQueryConditionKeywords = new Set(["not", "and", "or"]);
4756
- const containerNameReservedKeywords = new Set(["none", "inherit", "initial", "revert", "revert-layer", "unset"]);
4757
-
4758
- function namespaceCustomPropertyName(rawProperty, propertyName, customPropertyNames) {
4759
- const scopedName = customPropertyNames.get(propertyName);
4760
- if (!scopedName) return rawProperty;
4761
- return rawProperty.replace(new RegExp(escapeRegExp(propertyName) + "(?=\\\\s*$)"), scopedName);
4762
- }
4763
-
4764
- function namespaceCustomPropertyReferences(block, customPropertyNames) {
4765
- return rewriteDeclarationBlock(block, (propertyName, rawProperty, value) => ({
4766
- property: rawProperty,
4767
- value: namespaceCustomPropertyReferencesValue(value, customPropertyNames),
4768
- }));
4769
- }
4770
-
4771
- function namespaceCustomPropertyReferencesValue(value, customPropertyNames) {
4772
- if (customPropertyNames.size === 0) return value;
4773
- return replaceCssValueOutsideProtectedRanges(value, (chunk) => {
4774
- let nextChunk = chunk;
4775
- for (const [name, scopedName] of customPropertyNames) {
4776
- nextChunk = nextChunk.replace(new RegExp("(\\\\bvar\\\\(\\\\s*)" + escapeRegExp(name) + "(?=\\\\s*[,)])", "g"), (_match, prefix) => prefix + scopedName);
4777
- }
4778
- return nextChunk;
4779
- });
4780
- }
4781
-
4782
- function namespaceCustomPropertyIdentifierValue(value, customPropertyNames) {
4783
- if (customPropertyNames.size === 0) return value;
4784
- return replaceCssValueOutsideProtectedRanges(value, (chunk) => {
4785
- let nextChunk = chunk;
4786
- for (const [name, scopedName] of customPropertyNames) {
4787
- nextChunk = nextChunk.replace(new RegExp("(^|[^A-Za-z0-9_-])" + escapeRegExp(name) + "(?=$|[^A-Za-z0-9_-])", "g"), (_match, prefix) => prefix + scopedName);
4788
- }
4789
- return nextChunk;
4790
- });
4791
- }
4792
-
4793
- function namespaceCssGlobalNameValue(value) {
4794
- return replaceCssValueOutsideProtectedRanges(value, (chunk) => {
4795
- return chunk.replace(/(^|[^A-Za-z0-9_-])(--[A-Za-z0-9_-]+|[A-Za-z_][A-Za-z0-9_-]*|-[A-Za-z_][A-Za-z0-9_-]*)(?=$|[^A-Za-z0-9_-])/g, (_match, prefix, name, offset, source) => {
4796
- const nextChar = source[offset + prefix.length + name.length] ?? "";
4797
- const beforeName = source.slice(0, offset + prefix.length);
4798
- if (nextChar === "(" || /(?:^|[^A-Za-z0-9_-])var\\s*\\($/i.test(beforeName) || cssGlobalNameReservedKeywords.has(name.toLowerCase())) return prefix + name;
4799
- return prefix + widgetPackageCssGlobalNamePrefix + name;
4800
- });
4801
- });
4802
- }
4803
-
4804
- const cssGlobalNameReservedKeywords = new Set(["auto", "none", "inherit", "initial", "revert", "revert-layer", "unset"]);
4805
-
4806
- function assertNoNestedCssBlock(block, assetName) {
4807
- let index = 0;
4808
- let quote = "";
4809
- let parenDepth = 0;
4810
- while (index < block.length) {
4811
- const char = block[index];
4812
- if (quote) {
4813
- if (char === "\\\\") index += 2;
4814
- else {
4815
- if (char === quote) quote = "";
4816
- index += 1;
4817
- }
4818
- continue;
4819
- }
4820
- if (cssStartsWithComment(block, index)) {
4821
- const commentEnd = block.indexOf("*/", index + 2);
4822
- index = commentEnd === -1 ? block.length : commentEnd + 2;
4823
- continue;
4824
- }
4825
- if (char === '"' || char === "'") {
4826
- quote = char;
4827
- index += 1;
4828
- continue;
4829
- }
4830
- if (char === "(") parenDepth += 1;
4831
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
4832
- else if (char === "{" && parenDepth === 0) {
4833
- throw new Error('Unsupported nested CSS rule in CSS asset "' + assetName + '" cannot be isolated safely. Compile nested CSS before building the widget package.');
4834
- }
4835
- index += 1;
4836
- }
4837
- }
4838
-
4839
- function rewriteDeclarationBlock(block, rewriteDeclaration) {
4840
- let output = "";
4841
- let declarationStart = 0;
4842
- let index = 0;
4843
- let quote = "";
4844
- let parenDepth = 0;
4845
- while (index < block.length) {
4846
- const char = block[index];
4847
- if (quote) {
4848
- if (char === "\\\\") index += 2;
4849
- else {
4850
- if (char === quote) quote = "";
4851
- index += 1;
4852
- }
4853
- continue;
4854
- }
4855
- if (cssStartsWithComment(block, index)) {
4856
- const commentEnd = block.indexOf("*/", index + 2);
4857
- index = commentEnd === -1 ? block.length : commentEnd + 2;
4858
- continue;
4859
- }
4860
- if (char === '"' || char === "'") {
4861
- quote = char;
4862
- index += 1;
4863
- continue;
4864
- }
4865
- if (char === "(") parenDepth += 1;
4866
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
4867
- else if (char === ";" && parenDepth === 0) {
4868
- output += rewriteDeclarationSegment(block.slice(declarationStart, index), rewriteDeclaration) + ";";
4869
- declarationStart = index + 1;
4870
- }
4871
- index += 1;
4872
- }
4873
- return output + rewriteDeclarationSegment(block.slice(declarationStart), rewriteDeclaration);
4874
- }
4875
-
4876
- function rewriteDeclarationSegment(segment, rewriteDeclaration) {
4877
- const colonIndex = findDeclarationColon(segment);
4878
- if (colonIndex === -1) return segment;
4879
- const rawProperty = segment.slice(0, colonIndex);
4880
- const rawPropertyName = stripCssComments(rawProperty).trim();
4881
- const propertyName = rawPropertyName.startsWith("--") ? rawPropertyName : rawPropertyName.toLowerCase();
4882
- if (!propertyName) return segment;
4883
- const rewritten = rewriteDeclaration(propertyName, rawProperty, segment.slice(colonIndex + 1));
4884
- return rewritten.property + ":" + rewritten.value;
4885
- }
4886
-
4887
- function findDeclarationColon(segment) {
4888
- let index = 0;
4889
- let quote = "";
4890
- let parenDepth = 0;
4891
- while (index < segment.length) {
4892
- const char = segment[index];
4893
- if (quote) {
4894
- if (char === "\\\\") index += 2;
4895
- else {
4896
- if (char === quote) quote = "";
4897
- index += 1;
4898
- }
4899
- continue;
4900
- }
4901
- if (cssStartsWithComment(segment, index)) {
4902
- const commentEnd = segment.indexOf("*/", index + 2);
4903
- index = commentEnd === -1 ? segment.length : commentEnd + 2;
4904
- continue;
4905
- }
4906
- if (char === '"' || char === "'") {
4907
- quote = char;
4908
- index += 1;
4909
- continue;
4910
- }
4911
- if (char === "(") parenDepth += 1;
4912
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
4913
- else if (char === ":" && parenDepth === 0) return index;
4914
- index += 1;
4915
- }
4916
- return -1;
4917
- }
4918
-
4919
- function replaceCssValueOutsideProtectedRanges(value, replaceChunk) {
4920
- let output = "";
4921
- let chunkStart = 0;
4922
- let index = 0;
4923
- while (index < value.length) {
4924
- if (cssStartsWithComment(value, index)) {
4925
- output += replaceChunk(value.slice(chunkStart, index));
4926
- const commentEnd = value.indexOf("*/", index + 2);
4927
- const nextIndex = commentEnd === -1 ? value.length : commentEnd + 2;
4928
- output += value.slice(index, nextIndex);
4929
- index = nextIndex;
4930
- chunkStart = index;
4931
- continue;
4932
- }
4933
- const char = value[index];
4934
- if (char === '"' || char === "'") {
4935
- output += replaceChunk(value.slice(chunkStart, index));
4936
- const stringEnd = findCssStringEnd(value, index);
4937
- output += value.slice(index, stringEnd);
4938
- index = stringEnd;
4939
- chunkStart = index;
4940
- continue;
4941
- }
4942
- if (isCssUrlFunctionStart(value, index)) {
4943
- output += replaceChunk(value.slice(chunkStart, index));
4944
- const urlEnd = findCssFunctionEnd(value, value.indexOf("(", index));
4945
- output += value.slice(index, urlEnd);
4946
- index = urlEnd;
4947
- chunkStart = index;
4948
- continue;
4949
- }
4950
- index += 1;
4951
- }
4952
- return output + replaceChunk(value.slice(chunkStart));
4953
- }
4954
-
4955
- function findCssStringEnd(value, quoteIndex) {
4956
- const quote = value[quoteIndex];
4957
- let index = quoteIndex + 1;
4958
- while (index < value.length) {
4959
- const char = value[index];
4960
- if (char === "\\\\") index += 2;
4961
- else {
4962
- index += 1;
4963
- if (char === quote) return index;
4964
- }
4965
- }
4966
- return value.length;
4967
- }
4968
-
4969
- function findCssFunctionEnd(value, openParenIndex) {
4970
- let index = openParenIndex + 1;
4971
- let depth = 1;
4972
- let quote = "";
4973
- while (index < value.length) {
4974
- const char = value[index];
4975
- if (quote) {
4976
- if (char === "\\\\") index += 2;
4977
- else {
4978
- if (char === quote) quote = "";
4979
- index += 1;
4980
- }
4981
- continue;
4982
- }
4983
- if (char === '"' || char === "'") {
4984
- quote = char;
4985
- index += 1;
4986
- continue;
4987
- }
4988
- if (cssStartsWithComment(value, index)) {
4989
- const commentEnd = value.indexOf("*/", index + 2);
4990
- index = commentEnd === -1 ? value.length : commentEnd + 2;
4991
- continue;
4992
- }
4993
- if (char === "(") depth += 1;
4994
- else if (char === ")") {
4995
- depth -= 1;
4996
- if (depth === 0) return index + 1;
4997
- }
4998
- index += 1;
4999
- }
5000
- return value.length;
5001
- }
5002
-
5003
- function findFontShorthandFamilyStart(value) {
5004
- const tokens = readCssValueTokens(value);
5005
- for (let index = 0; index < tokens.length; index += 1) {
5006
- const token = tokens[index];
5007
- if (!token || !isFontSizeToken(token.text)) continue;
5008
- if (findTopLevelSlash(token.text) !== -1) return token.end;
5009
- const slashIndex = skipCssWhitespaceAndComments(value, token.end);
5010
- if (value[slashIndex] !== "/") return token.end;
5011
- const lineHeightStart = skipCssWhitespaceAndComments(value, slashIndex + 1);
5012
- const lineHeightToken = tokens.find((candidate) => candidate.start >= lineHeightStart);
5013
- return lineHeightToken ? lineHeightToken.end : token.end;
5014
- }
5015
- return -1;
5016
- }
5017
-
5018
- function readCssValueTokens(value) {
5019
- const tokens = [];
5020
- let index = 0;
5021
- while (index < value.length) {
5022
- if (/\\s/.test(value[index] ?? "")) {
5023
- index += 1;
5024
- continue;
5025
- }
5026
- if (cssStartsWithComment(value, index)) {
5027
- const commentEnd = value.indexOf("*/", index + 2);
5028
- index = commentEnd === -1 ? value.length : commentEnd + 2;
5029
- continue;
5030
- }
5031
- const start = index;
5032
- const char = value[index];
5033
- if (char === '"' || char === "'") {
5034
- index = findCssStringEnd(value, index);
5035
- } else {
5036
- let parenDepth = 0;
5037
- while (index < value.length) {
5038
- if (cssStartsWithComment(value, index)) {
5039
- if (parenDepth === 0) break;
5040
- const commentEnd = value.indexOf("*/", index + 2);
5041
- index = commentEnd === -1 ? value.length : commentEnd + 2;
5042
- continue;
5043
- }
5044
- const tokenChar = value[index];
5045
- if ((tokenChar === '"' || tokenChar === "'") && parenDepth > 0) {
5046
- index = findCssStringEnd(value, index);
5047
- continue;
5048
- }
5049
- if (tokenChar === "(") parenDepth += 1;
5050
- else if (tokenChar === ")") parenDepth = Math.max(0, parenDepth - 1);
5051
- if (parenDepth === 0 && /\\s/.test(tokenChar ?? "")) break;
5052
- index += 1;
5053
- }
5054
- }
5055
- tokens.push({ start, end: index, text: value.slice(start, index) });
5056
- }
5057
- return tokens;
5058
- }
5059
-
5060
- function isFontSizeToken(token) {
5061
- const slashIndex = findTopLevelSlash(token);
5062
- const sizeToken = slashIndex === -1 ? token : token.slice(0, slashIndex);
5063
- return /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|smaller|larger|math|(?:\\d+|\\d*\\.\\d+)(?:px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cap|ic|lh|rlh|cm|mm|q|in|pc|pt))$/i.test(sizeToken) || isFontSizeFunctionToken(sizeToken);
5064
- }
5065
-
5066
- function isFontSizeFunctionToken(token) {
5067
- return /^(?:calc|clamp|min|max|var|env|abs|sign|round|mod|rem|sin|cos|tan|asin|acos|atan|atan2|pow|sqrt|hypot|log|exp)\\(/i.test(token);
5068
- }
5069
-
5070
- function findTopLevelSlash(value) {
5071
- let index = 0;
5072
- let quote = "";
5073
- let parenDepth = 0;
5074
- while (index < value.length) {
5075
- const char = value[index];
5076
- if (quote) {
5077
- if (char === "\\\\") index += 2;
5078
- else {
5079
- if (char === quote) quote = "";
5080
- index += 1;
5081
- }
5082
- continue;
5083
- }
5084
- if (char === '"' || char === "'") {
5085
- quote = char;
5086
- index += 1;
5087
- continue;
5088
- }
5089
- if (char === "(") parenDepth += 1;
5090
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
5091
- else if (char === "/" && parenDepth === 0) return index;
5092
- index += 1;
5093
- }
5094
- return -1;
5095
- }
5096
-
5097
- function skipCssWhitespaceAndComments(value, startIndex) {
5098
- let index = startIndex;
5099
- while (index < value.length) {
5100
- if (/\\s/.test(value[index] ?? "")) {
5101
- index += 1;
5102
- continue;
5103
- }
5104
- if (cssStartsWithComment(value, index)) {
5105
- const commentEnd = value.indexOf("*/", index + 2);
5106
- index = commentEnd === -1 ? value.length : commentEnd + 2;
5107
- continue;
5108
- }
5109
- return index;
5110
- }
5111
- return index;
5112
- }
5113
-
5114
- function cssStartsWithComment(value, index) {
5115
- return value[index] === "/" && value[index + 1] === "*";
5116
- }
5117
-
5118
- function isCssUrlFunctionStart(value, index) {
5119
- return /^url\\s*\\(/i.test(value.slice(index)) && !/[A-Za-z0-9_-]/.test(value[index - 1] ?? "");
5120
- }
5121
-
5122
- function stripCssComments(value) {
5123
- return value.replace(/\\/\\*[\\s\\S]*?\\*\\//g, "");
5124
- }
5125
-
5126
- function splitCssCommaList(value) {
5127
- const parts = [];
5128
- let current = "";
5129
- let quote = "";
5130
- let parenDepth = 0;
5131
- for (let index = 0; index < value.length; index += 1) {
5132
- const char = value[index];
5133
- if (quote) {
5134
- current += char;
5135
- if (char === "\\\\") {
5136
- index += 1;
5137
- current += value[index] ?? "";
5138
- } else if (char === quote) quote = "";
5139
- continue;
5140
- }
5141
- if (char === '"' || char === "'") {
5142
- quote = char;
5143
- current += char;
5144
- continue;
5145
- }
5146
- if (char === "(") parenDepth += 1;
5147
- else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
5148
- if (char === "," && parenDepth === 0) {
5149
- parts.push(current);
5150
- current = "";
5151
- } else current += char;
5152
- }
5153
- parts.push(current);
5154
- return parts;
5155
- }
5156
-
5157
- function escapeRegExp(value) {
5158
- return value.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
5159
- }
5160
-
5161
- function encodeCssIdentifier(value) {
5162
- return Array.from(value, (char) => char.codePointAt(0)?.toString(16).padStart(2, "0") ?? "00").join("_");
5163
- }
5164
-
5165
3719
  return {
5166
3720
  ${configFileSource} publicDir: false,
3721
+ define: fluidWidgetDefineInvariants,
5167
3722
  resolve: {
5168
3723
  conditions: [
5169
- "fluid-widget-authoring",
5170
3724
  "module",
5171
3725
  "browser",
5172
3726
  "development|production",
5173
3727
  ],
5174
3728
  },
5175
- plugins: [fluidWidgetBuildInvariantsPlugin(), fluidWidgetCssIsolationPlugin()],
3729
+ plugins: [fluidWidgetBuildInvariantsPlugin()],
5176
3730
  build: createFluidWidgetBuildInvariants(),
5177
3731
  };
5178
3732
  })()`;
@@ -5223,12 +3777,22 @@ function stableStringify(value) {
5223
3777
  return `${JSON.stringify(value, null, 2)}\n`;
5224
3778
  }
5225
3779
  function readCliVersion() {
5226
- const startDir = path.dirname(fileURLToPath(import.meta.url));
5227
- for (const packageJsonPath of candidatePackageJsonPaths(startDir)) try {
5228
- const packageJson = require(packageJsonPath);
5229
- if (packageJson.name === "@fluid-app/fluid-cli-portal" && typeof packageJson.version === "string") return packageJson.version;
5230
- } catch {}
5231
- return "0.0.0";
3780
+ return resolveCliVersionFromPackageMetadata(path.dirname(fileURLToPath(import.meta.url)), require);
3781
+ }
3782
+ function resolveCliVersionFromPackageMetadata(startDir, loadPackageMetadata) {
3783
+ for (const packageJsonPath of candidatePackageJsonPaths(startDir)) {
3784
+ let packageJson;
3785
+ try {
3786
+ packageJson = loadPackageMetadata(packageJsonPath);
3787
+ } catch (error) {
3788
+ if (isModuleResolutionError(error)) continue;
3789
+ throw new Error("Failed to read Fluid CLI package metadata.", { cause: error });
3790
+ }
3791
+ if (!isRecord$2(packageJson) || packageJson.name !== "@fluid-app/fluid-cli-portal") continue;
3792
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) throw new Error("Fluid CLI package metadata does not declare a valid version.");
3793
+ return packageJson.version;
3794
+ }
3795
+ throw new Error("Unable to find Fluid CLI package metadata.");
5232
3796
  }
5233
3797
  function resolveViteCliPathFromRequire(moduleRequire) {
5234
3798
  try {
@@ -5276,22 +3840,6 @@ var WidgetPackageUploadError = class extends Error {
5276
3840
  this.code = code;
5277
3841
  }
5278
3842
  };
5279
- const SESSION_ARRAY_KEYS = [
5280
- "uploads",
5281
- "artifacts",
5282
- "signedUploads",
5283
- "signed_uploads",
5284
- "signedUrls",
5285
- "signed_urls",
5286
- "uploadUrls",
5287
- "upload_urls"
5288
- ];
5289
- const SESSION_RECORD_KEYS = [
5290
- "signedUrls",
5291
- "signed_urls",
5292
- "uploadUrls",
5293
- "upload_urls"
5294
- ];
5295
3843
  function buildWidgetPackageArtifactPayload(artifact) {
5296
3844
  return {
5297
3845
  path: artifact.path,
@@ -5333,7 +3881,7 @@ function getWidgetPackageCompleteUploadRoute(owner, version) {
5333
3881
  async function requestWidgetPackageUploadSession(client, owner, request) {
5334
3882
  const payload = buildWidgetPackageUploadSessionPayload(request);
5335
3883
  try {
5336
- 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);
3884
+ return normalizeWidgetPackageUploadSession(owner.kind === "company" ? await fluidOs.fluid_os_v0_create_widget_package_version(client, payload) : await client.post(getWidgetPackageUploadSessionRoute(owner), payload), request.version, request.artifacts);
5337
3885
  } catch (err) {
5338
3886
  if (err instanceof WidgetPackageUploadError) throw err;
5339
3887
  throw new WidgetPackageUploadError("SESSION_REQUEST_FAILED", `Failed to create widget package upload session: ${formatUnknownError(err)}`);
@@ -5372,7 +3920,7 @@ async function uploadWidgetPackageArtifacts(session, artifacts, fetchImpl = fetc
5372
3920
  async function completeWidgetPackageUpload(client, owner, version, packageKey, artifacts) {
5373
3921
  const payload = buildWidgetPackageCompleteUploadPayload(packageKey, artifacts);
5374
3922
  try {
5375
- 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);
3923
+ return normalizeWidgetPackageCompleteResponse(owner.kind === "company" ? await fluidOs.fluid_os_v0_complete_widget_package_version_upload(client, version, payload) : await client.post(getWidgetPackageCompleteUploadRoute(owner, version), payload), version);
5376
3924
  } catch (err) {
5377
3925
  if (err instanceof WidgetPackageUploadError) throw err;
5378
3926
  throw new WidgetPackageUploadError("COMPLETE_UPLOAD_FAILED", `Failed to complete widget package upload: ${formatUnknownError(err)}`);
@@ -5395,128 +3943,53 @@ async function publishWidgetPackageVersion(options) {
5395
3943
  };
5396
3944
  }
5397
3945
  function normalizeWidgetPackageUploadSession(response, requestedVersion, requestedArtifacts) {
5398
- const record = requireRecord(response, "Upload session response must be a JSON object.");
5399
- const responseVersion = getResponseVersion(record);
5400
- if (responseVersion && responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
5401
- const version = responseVersion ?? requestedVersion;
5402
- const nestedRecord = getNestedVersionRecord(record);
5403
- const uploads = getSignedUploads(nestedRecord ? [record, nestedRecord] : [record], requestedArtifacts);
5404
- const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !uploads.some((upload) => upload.path === artifactPath));
3946
+ const record = requireRecord(response, "INVALID_SESSION_RESPONSE", "Upload session response must be a JSON object.");
3947
+ requireFiniteNumberProperty(record, "version_id", "INVALID_SESSION_RESPONSE");
3948
+ requireStringProperty(record, "artifact_base_url", "INVALID_SESSION_RESPONSE");
3949
+ const uploadValues = record["uploads"];
3950
+ if (!Array.isArray(uploadValues)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", "Upload session response must contain an \"uploads\" array.");
3951
+ const requestedArtifactsByPath = new Map(requestedArtifacts.map((artifact) => [artifact.path, artifact]));
3952
+ const seenUploadPaths = /* @__PURE__ */ new Set();
3953
+ const uploads = uploadValues.map((value, index) => {
3954
+ const upload = requireRecord(value, "INVALID_SESSION_RESPONSE", `Upload session uploads[${index}] must be a JSON object.`);
3955
+ const path = requireStringProperty(upload, "path", "INVALID_SESSION_RESPONSE");
3956
+ const url = requireStringProperty(upload, "upload_url", "INVALID_SESSION_RESPONSE");
3957
+ requireStringProperty(upload, "public_url", "INVALID_SESSION_RESPONSE");
3958
+ const requestedArtifact = requestedArtifactsByPath.get(path);
3959
+ if (!requestedArtifact) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included unexpected artifact "${path}".`);
3960
+ if (seenUploadPaths.has(path)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included duplicate artifact "${path}".`);
3961
+ seenUploadPaths.add(path);
3962
+ return {
3963
+ path,
3964
+ url,
3965
+ contentType: requestedArtifact.contentType,
3966
+ headers: {}
3967
+ };
3968
+ });
3969
+ const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !seenUploadPaths.has(artifactPath));
5405
3970
  if (missingUploadPaths.length > 0) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response is missing signed URLs for artifact(s): ${missingUploadPaths.join(", ")}.`);
5406
3971
  return {
5407
- version,
3972
+ version: requestedVersion,
5408
3973
  uploads,
5409
3974
  raw: response
5410
3975
  };
5411
3976
  }
5412
3977
  function normalizeWidgetPackageCompleteResponse(response, requestedVersion) {
5413
- if (response == null) return {
5414
- version: requestedVersion,
5415
- raw: response
5416
- };
5417
- const record = requireRecord(response, "Complete upload response must be a JSON object or empty response.");
5418
- const nested = getNestedVersionRecord(record) ?? record;
5419
- const responseVersion = getResponseVersion(nested);
5420
- if (responseVersion && responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
5421
- const packageKey = getStringProperty(nested, "package_key") ?? getStringProperty(nested, "packageKey") ?? getStringProperty(nested, "packageId");
5422
- const status = getStringProperty(nested, "status") ?? getStringProperty(nested, "state");
3978
+ const record = requireRecord(response, "INVALID_COMPLETE_RESPONSE", "Complete upload response must be a JSON object.");
3979
+ const responseVersion = requireStringProperty(record, "version", "INVALID_COMPLETE_RESPONSE");
3980
+ if (responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
3981
+ const packageKey = requireStringProperty(record, "packageId", "INVALID_COMPLETE_RESPONSE");
3982
+ const status = requireStringProperty(record, "status", "INVALID_COMPLETE_RESPONSE");
3983
+ requireStringProperty(record, "scriptUrl", "INVALID_COMPLETE_RESPONSE");
3984
+ requireStringProperty(record, "manifestUrl", "INVALID_COMPLETE_RESPONSE");
3985
+ requireStringArrayProperty(record, "cssUrls", "INVALID_COMPLETE_RESPONSE");
5423
3986
  return {
5424
- version: responseVersion ?? requestedVersion,
5425
- ...status ? { status } : {},
5426
- ...packageKey ? { packageKey } : {},
3987
+ version: responseVersion,
3988
+ status,
3989
+ packageKey,
5427
3990
  raw: response
5428
3991
  };
5429
3992
  }
5430
- function getSignedUploads(records, requestedArtifacts) {
5431
- const uploads = [];
5432
- for (const record of records) {
5433
- for (const key of SESSION_ARRAY_KEYS) {
5434
- const value = record[key];
5435
- if (!Array.isArray(value)) continue;
5436
- for (const item of value) {
5437
- const upload = signedUploadFromRecord(item, requestedArtifacts);
5438
- if (upload) uploads.push(upload);
5439
- }
5440
- }
5441
- for (const key of SESSION_RECORD_KEYS) {
5442
- const value = record[key];
5443
- if (!isRecord$1(value) || Array.isArray(value)) continue;
5444
- for (const [artifactPath, uploadValue] of Object.entries(value)) {
5445
- const upload = signedUploadFromKeyedValue(artifactPath, uploadValue, requestedArtifacts);
5446
- if (upload) uploads.push(upload);
5447
- }
5448
- }
5449
- }
5450
- return dedupeUploads(uploads);
5451
- }
5452
- function signedUploadFromRecord(value, requestedArtifacts) {
5453
- if (!isRecord$1(value)) return null;
5454
- const artifactPath = readFirstString(value, [
5455
- "path",
5456
- "key",
5457
- "artifactPath",
5458
- "artifact_path",
5459
- "artifactKey",
5460
- "artifact_key",
5461
- "name"
5462
- ]);
5463
- const url = readFirstString(value, [
5464
- "url",
5465
- "signedUrl",
5466
- "signed_url",
5467
- "uploadUrl",
5468
- "upload_url"
5469
- ]);
5470
- if (!artifactPath || !url) return null;
5471
- const requestedArtifact = findRequestedArtifact(requestedArtifacts, artifactPath);
5472
- const contentType = readFirstString(value, [
5473
- "contentType",
5474
- "content_type",
5475
- "expectedContentType",
5476
- "expected_content_type"
5477
- ]) ?? requestedArtifact?.contentType;
5478
- if (!contentType) return null;
5479
- return {
5480
- path: artifactPath,
5481
- url,
5482
- contentType,
5483
- headers: getHeaders(value["headers"])
5484
- };
5485
- }
5486
- function signedUploadFromKeyedValue(artifactPath, value, requestedArtifacts) {
5487
- const requestedArtifact = findRequestedArtifact(requestedArtifacts, artifactPath);
5488
- if (typeof value === "string") {
5489
- if (!requestedArtifact) return null;
5490
- return {
5491
- path: artifactPath,
5492
- url: value,
5493
- contentType: requestedArtifact.contentType,
5494
- headers: {}
5495
- };
5496
- }
5497
- if (!isRecord$1(value)) return null;
5498
- const url = readFirstString(value, [
5499
- "url",
5500
- "signedUrl",
5501
- "signed_url",
5502
- "uploadUrl",
5503
- "upload_url"
5504
- ]);
5505
- if (!url) return null;
5506
- const contentType = readFirstString(value, [
5507
- "contentType",
5508
- "content_type",
5509
- "expectedContentType",
5510
- "expected_content_type"
5511
- ]) ?? requestedArtifact?.contentType;
5512
- if (!contentType) return null;
5513
- return {
5514
- path: artifactPath,
5515
- url,
5516
- contentType,
5517
- headers: getHeaders(value["headers"])
5518
- };
5519
- }
5520
3993
  function normalizeHeaderName(name) {
5521
3994
  return name.toLowerCase();
5522
3995
  }
@@ -5529,57 +4002,47 @@ function mergeUploadHeaders(headers, contentType) {
5529
4002
  merged["Content-Type"] = contentType;
5530
4003
  return merged;
5531
4004
  }
5532
- function dedupeUploads(uploads) {
5533
- const deduped = /* @__PURE__ */ new Map();
5534
- for (const upload of uploads) deduped.set(upload.path, upload);
5535
- return Array.from(deduped.values());
5536
- }
5537
- function findRequestedArtifact(requestedArtifacts, artifactPath) {
5538
- return requestedArtifacts.find((artifact) => artifact.path === artifactPath);
5539
- }
5540
- function requireRecord(value, message) {
5541
- if (!isRecord$1(value) || Array.isArray(value)) throw new WidgetPackageUploadError("INVALID_RESPONSE", message);
4005
+ function requireRecord(value, code, message) {
4006
+ if (!isRecord$1(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
5542
4007
  return value;
5543
4008
  }
5544
4009
  function isRecord$1(value) {
5545
4010
  return typeof value === "object" && value !== null;
5546
4011
  }
5547
- function getStringProperty(record, key) {
4012
+ function requireStringProperty(record, key, code) {
5548
4013
  const value = record[key];
5549
4014
  if (typeof value === "string" && value.trim().length > 0) return value;
5550
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
5551
- }
5552
- function readFirstString(record, keys) {
5553
- for (const key of keys) {
5554
- const value = getStringProperty(record, key);
5555
- if (value) return value;
5556
- }
5557
- }
5558
- function getHeaders(value) {
5559
- if (!isRecord$1(value) || Array.isArray(value)) return {};
5560
- const headers = {};
5561
- for (const [key, headerValue] of Object.entries(value)) if (typeof headerValue === "string") headers[key] = headerValue;
5562
- return headers;
5563
- }
5564
- function getResponseVersion(record) {
5565
- const directVersion = getStringProperty(record, "version");
5566
- if (directVersion) return directVersion;
5567
- const nested = getNestedVersionRecord(record);
5568
- return nested ? getStringProperty(nested, "version") : void 0;
5569
- }
5570
- function getNestedVersionRecord(record) {
5571
- const candidates = [
5572
- record["portal_widget_package_version"],
5573
- record["widget_package_version"],
5574
- record["package_version"]
5575
- ];
5576
- for (const candidate of candidates) if (isRecord$1(candidate) && !Array.isArray(candidate)) return candidate;
4015
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be a non-empty string.`);
4016
+ }
4017
+ function requireFiniteNumberProperty(record, key, code) {
4018
+ const value = record[key];
4019
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4020
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be a finite number.`);
4021
+ }
4022
+ function requireStringArrayProperty(record, key, code) {
4023
+ const value = record[key];
4024
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim().length > 0)) return value;
4025
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be an array of non-empty strings.`);
5577
4026
  }
5578
4027
  function formatUnknownError(err) {
5579
4028
  const base = err instanceof Error ? err.message : String(err);
5580
- if (isRecord$1(err) && "data" in err) return `${base} — ${JSON.stringify(err.data)}`;
4029
+ if (isRecord$1(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
5581
4030
  return base;
5582
4031
  }
4032
+ function safeStringify(value) {
4033
+ const seen = /* @__PURE__ */ new WeakSet();
4034
+ try {
4035
+ return JSON.stringify(value, (_key, item) => {
4036
+ if (typeof item === "bigint") return `${item}n`;
4037
+ if (typeof item !== "object" || item === null) return item;
4038
+ if (seen.has(item)) return "[Circular]";
4039
+ seen.add(item);
4040
+ return item;
4041
+ }) ?? String(value);
4042
+ } catch {
4043
+ return "[unserializable error data]";
4044
+ }
4045
+ }
5583
4046
  //#endregion
5584
4047
  //#region src/commands/widget-package-publish.ts
5585
4048
  async function publishWidgetRuntimeArtifacts(options, dependencies = {}) {
@@ -6074,7 +4537,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
6074
4537
  console.log(chalk.bold("Creating version..."));
6075
4538
  let result;
6076
4539
  try {
6077
- result = await fluid_os_v0_create_fluid_osversion(client, definitionId);
4540
+ result = await fluidOs.fluid_os_v0_create_fluid_osversion(client, definitionId);
6078
4541
  } catch (err) {
6079
4542
  console.error(chalk.red("Error:") + " Failed to create version — " + (err instanceof Error ? err.message : String(err)));
6080
4543
  process.exit(1);
@@ -6092,7 +4555,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
6092
4555
  if (options.activate) {
6093
4556
  console.log("Activating version...");
6094
4557
  try {
6095
- await fluid_os_v0_update_fluid_osversion(client, definitionId, version.id, { version: { active: true } });
4558
+ await fluidOs.fluid_os_v0_update_fluid_osversion(client, definitionId, version.id, { version: { active: true } });
6096
4559
  } catch (err) {
6097
4560
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
6098
4561
  process.exit(1);
@@ -6107,7 +4570,7 @@ const listVersionCommand = new Command("list").description("List all versions of
6107
4570
  const definitionId = await requireDefinitionId();
6108
4571
  let result;
6109
4572
  try {
6110
- result = await fluid_os_v0_list_fluid_osversions(client, definitionId);
4573
+ result = await fluidOs.fluid_os_v0_list_fluid_osversions(client, definitionId);
6111
4574
  } catch (err) {
6112
4575
  console.error(chalk.red("Error:") + " Failed to list versions — " + (err instanceof Error ? err.message : String(err)));
6113
4576
  process.exit(1);
@@ -6150,7 +4613,7 @@ const activateVersionCommand = new Command("activate").description("Activate a s
6150
4613
  console.log();
6151
4614
  console.log(chalk.bold("Activating version..."));
6152
4615
  try {
6153
- await fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
4616
+ await fluidOs.fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
6154
4617
  } catch (err) {
6155
4618
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
6156
4619
  process.exit(1);
@@ -6233,6 +4696,6 @@ const plugin = {
6233
4696
  }
6234
4697
  };
6235
4698
  //#endregion
6236
- export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, WIDGET_PACKAGE_BUILDER_VERSION, WidgetPackageUploadError, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSharedWidgetPackage, buildSnapshot, buildThemeIdToSlugMap, buildWidgetPackageCompleteUploadPayload, buildWidgetPackageDescriptor, buildWidgetPackageUploadSessionPayload, categorizeChanges, cloneCommand, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPublishManifestMetadata, createWidgetPackageEntrySource, createWidgetPackagePublishRequest, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getArtifactContentType, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, getWidgetPackageCompleteUploadRoute, getWidgetPackageUploadSessionRoute, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, loadSourceWidgetPackages, normalizeWidgetPackageCompleteResponse, normalizeWidgetPackageUploadSession, pathExists, portalGitCredentialPath, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, promptProjectConfig, publishWidgetPackageVersion, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, requestWidgetPackageUploadSession, resolveIdToSlug, resolvePortalWidgetSourceConfig, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, sha256, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, uploadWidgetPackageArtifacts, validateCrossReferences, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
4699
+ export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, WIDGET_PACKAGE_BUILDER_VERSION, WidgetPackageUploadError, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSharedWidgetPackage, buildSnapshot, buildThemeIdToSlugMap, buildWidgetPackageCompleteUploadPayload, buildWidgetPackagePublishManifest, buildWidgetPackageUploadSessionPayload, categorizeChanges, cloneCommand, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPublishManifestMetadata, createWidgetPackageEntrySource, createWidgetPackagePublishRequest, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getArtifactContentType, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, getWidgetPackageCompleteUploadRoute, getWidgetPackageUploadSessionRoute, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, loadSourceWidgetPackages, normalizeWidgetPackageCompleteResponse, normalizeWidgetPackageUploadSession, pathExists, portalGitCredentialPath, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, promptProjectConfig, publishWidgetPackageVersion, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, requestWidgetPackageUploadSession, resolveIdToSlug, resolvePortalWidgetSourceConfig, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, sha256, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, uploadWidgetPackageArtifacts, validateCrossReferences, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
6237
4700
 
6238
4701
  //# sourceMappingURL=index.mjs.map