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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1997 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "fs-extra";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { execa } from "execa";
6
+ import { pathToFileURL } from "node:url";
7
+ import { failure, success } from "@fluid-app/fluid-cli";
8
+ //#region ../widget-tooling/src/errors.ts
9
+ var WidgetToolingError = class extends Error {
10
+ code;
11
+ details;
12
+ diagnostics;
13
+ constructor(code, message, options = {}) {
14
+ super(message, options);
15
+ this.name = "WidgetToolingError";
16
+ this.code = code;
17
+ this.details = options.details;
18
+ this.diagnostics = options.diagnostics;
19
+ }
20
+ };
21
+ const CONTENT_TYPES = {
22
+ ".css": "text/css",
23
+ ".js": "application/javascript",
24
+ ".json": "application/json",
25
+ ".map": "application/json"
26
+ };
27
+ const CSS_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.css$/;
28
+ const JS_MAP_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.js\.map$/;
29
+ async function computeArtifactMetadata(filePath, baseDir) {
30
+ const relativePath = toPosixPath$1(path.relative(baseDir, filePath));
31
+ assertAllowedArtifactPath(relativePath);
32
+ await assertRegularArtifactFile(filePath, relativePath);
33
+ const buffer = await fs.readFile(filePath);
34
+ return {
35
+ path: relativePath,
36
+ sha256: sha256(buffer),
37
+ bytes: buffer.byteLength,
38
+ contentType: getArtifactContentType(filePath)
39
+ };
40
+ }
41
+ async function collectWidgetPackageArtifacts(publishDir) {
42
+ const artifactPaths = await findArtifactPaths(publishDir);
43
+ const artifacts = await Promise.all(artifactPaths.map((artifactPath) => computeArtifactMetadata(artifactPath, publishDir)));
44
+ assertRequiredUploadArtifacts(artifacts);
45
+ return artifacts.sort((a, b) => a.path.localeCompare(b.path));
46
+ }
47
+ function createPublishManifestMetadata(options) {
48
+ return {
49
+ packageId: options.packageId,
50
+ version: options.version,
51
+ builderVersion: options.builderVersion ?? "0.1.0",
52
+ cliVersion: options.cliVersion,
53
+ runtimeVersion: options.runtimeVersion,
54
+ manifestHash: sha256(options.manifestContent),
55
+ artifacts: options.artifacts
56
+ };
57
+ }
58
+ function getArtifactContentType(filePath) {
59
+ if (path.basename(filePath).endsWith(".js.map")) return "application/json";
60
+ return CONTENT_TYPES[path.extname(filePath)] ?? "application/octet-stream";
61
+ }
62
+ function sha256(content) {
63
+ return createHash("sha256").update(content).digest("hex");
64
+ }
65
+ function isWidgetPackageCssArtifactPath(relativePath) {
66
+ return CSS_ARTIFACT_PATH_PATTERN.test(relativePath);
67
+ }
68
+ function isWidgetPackageJsMapArtifactPath(relativePath) {
69
+ return JS_MAP_ARTIFACT_PATH_PATTERN.test(relativePath);
70
+ }
71
+ async function assertRegularArtifactFile(filePath, relativePath) {
72
+ const stat = await fs.lstat(filePath);
73
+ if (stat.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
74
+ if (!stat.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
75
+ }
76
+ async function findArtifactPaths(dir, baseDir = dir) {
77
+ if (!await fs.pathExists(dir)) return [];
78
+ const entries = await fs.readdir(dir, { withFileTypes: true });
79
+ return (await Promise.all(entries.map(async (entry) => {
80
+ const entryPath = path.join(dir, entry.name);
81
+ const relativePath = toPosixPath$1(path.relative(baseDir, entryPath));
82
+ if (entry.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
83
+ if (isUploadArtifact(relativePath)) {
84
+ if (!entry.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
85
+ return [entryPath];
86
+ }
87
+ if (entry.isDirectory()) {
88
+ const nestedPaths = await findArtifactPaths(entryPath, baseDir);
89
+ if (nestedPaths.length === 0) throwUnsupportedArtifactPath(`${relativePath}/`);
90
+ return nestedPaths;
91
+ }
92
+ if (isAllowedGeneratedFile(relativePath)) return [];
93
+ throwUnsupportedArtifactPath(relativePath);
94
+ }))).flat();
95
+ }
96
+ function assertRequiredUploadArtifacts(artifacts) {
97
+ const artifactPaths = new Set(artifacts.map((artifact) => artifact.path));
98
+ for (const requiredPath of ["widget.js", "manifest.json"]) {
99
+ if (artifactPaths.has(requiredPath)) continue;
100
+ throw new Error(`Widget package publish output must include regular file ${JSON.stringify(requiredPath)}.`);
101
+ }
102
+ }
103
+ function assertAllowedArtifactPath(relativePath) {
104
+ if (isUploadArtifact(relativePath) || isAllowedGeneratedFile(relativePath)) return;
105
+ throwUnsupportedArtifactPath(relativePath);
106
+ }
107
+ function isUploadArtifact(relativePath) {
108
+ return relativePath === "widget.js" || relativePath === "manifest.json" || isWidgetPackageCssArtifactPath(relativePath) || isWidgetPackageJsMapArtifactPath(relativePath);
109
+ }
110
+ function isAllowedGeneratedFile(relativePath) {
111
+ return isFlatArtifactPath(relativePath) && relativePath === "publish-manifest.json";
112
+ }
113
+ function isFlatArtifactPath(relativePath) {
114
+ return relativePath.length > 0 && !relativePath.includes("/") && !relativePath.includes("\\");
115
+ }
116
+ function throwUnsupportedArtifactPath(relativePath) {
117
+ throw new Error(`Unsupported widget publish artifact ${JSON.stringify(relativePath)}. Allowed files are widget.js, manifest.json, top-level [A-Za-z0-9._~-]+.css, top-level [A-Za-z0-9._~-]+.js.map, and publish-manifest.json.`);
118
+ }
119
+ function toPosixPath$1(value) {
120
+ return value.split(path.sep).join("/");
121
+ }
122
+ //#endregion
123
+ //#region ../../portal/core/src/remote-dom-widget-package.ts
124
+ const UNSAFE_JSON_KEYS = new Set([
125
+ "__proto__",
126
+ "constructor",
127
+ "prototype"
128
+ ]);
129
+ function isJsonValue(value) {
130
+ return isJsonValueAtDepth(value, /* @__PURE__ */ new WeakSet(), 0);
131
+ }
132
+ function isJsonValueAtDepth(value, ancestors, depth) {
133
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
134
+ if (typeof value === "number") return Number.isFinite(value);
135
+ if (typeof value !== "object" || depth > 50 || ancestors.has(value)) return false;
136
+ ancestors.add(value);
137
+ const prototype = Object.getPrototypeOf(value);
138
+ const isJson = Array.isArray(value) ? value.every((item) => isJsonValueAtDepth(item, ancestors, depth + 1)) : (prototype === Object.prototype || prototype === null) && isRecord$5(value) && Object.entries(value).every(([key, item]) => !UNSAFE_JSON_KEYS.has(key) && isJsonValueAtDepth(item, ancestors, depth + 1));
139
+ ancestors.delete(value);
140
+ return isJson;
141
+ }
142
+ function isRecord$5(value) {
143
+ return typeof value === "object" && value !== null && !Array.isArray(value);
144
+ }
145
+ //#endregion
146
+ //#region ../widget-tooling/src/internal/widget-package-validation.ts
147
+ const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
148
+ const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
149
+ const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
150
+ const SEMVER_CORE_PATTERN = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)";
151
+ const SEMVER_PRERELEASE_IDENTIFIER_PATTERN = "(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)";
152
+ const SEMVER_URL_SAFE_PATTERN = new RegExp(`^${SEMVER_CORE_PATTERN}(?:-${SEMVER_PRERELEASE_IDENTIFIER_PATTERN}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER_PATTERN})*)?$`);
153
+ const WIDGET_PACKAGE_MANIFEST_VERSION = 1;
154
+ const DEFAULT_WIDGET_ICON = "box";
155
+ const DEFAULT_WIDGET_CATEGORY = "components";
156
+ const DEFAULT_WIDGET_CONTAINER = "block";
157
+ const DEFAULT_MIN_SDK_VERSION = "0.0.0";
158
+ function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
159
+ if (sourcePackages.length === 0) return validationFailure({
160
+ code: "NO_SOURCE_PACKAGE",
161
+ message: "No defineWidgetPackage source package was found. Export widgetPackage, widgetPackages, or a default widget package from src/widgets.config.ts."
162
+ });
163
+ if (sourcePackages.length > 1) return validationFailure({
164
+ code: "MULTIPLE_SOURCE_PACKAGES",
165
+ message: "Publish/deploy supports exactly one defineWidgetPackage source package. Export one package or split packages into separate builds."
166
+ });
167
+ const sourcePackageValue = sourcePackages[0];
168
+ if (!sourcePackageValue) return validationFailure({
169
+ code: "NO_SOURCE_PACKAGE",
170
+ message: "No defineWidgetPackage source package was found."
171
+ });
172
+ const shapeErrors = validateSourcePackageShape(sourcePackageValue);
173
+ if (shapeErrors.length > 0) return {
174
+ success: false,
175
+ errors: shapeErrors
176
+ };
177
+ const sourcePackage = sourcePackageValue;
178
+ const errors = [];
179
+ const owner = options.owner ?? sourcePackage.packageType;
180
+ if (owner !== "company" && owner !== "droplet") errors.push({
181
+ code: "UNSUPPORTED_OWNER",
182
+ path: "packageType",
183
+ message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
184
+ });
185
+ else if (options.owner !== void 0 && sourcePackage.packageType !== options.owner) errors.push({
186
+ code: "UNSUPPORTED_OWNER",
187
+ path: "packageType",
188
+ message: `Widget package metadata must set packageType to "${options.owner}" for this workflow.`
189
+ });
190
+ const resolvedOwner = owner === "droplet" ? "droplet" : "company";
191
+ if (resolvedOwner === "droplet" && !readNonEmptyTrimmedString(sourcePackage.packageStableId)) errors.push({
192
+ code: "INVALID_PACKAGE_KEY",
193
+ path: "packageStableId",
194
+ message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
195
+ });
196
+ const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
197
+ const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
198
+ if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
199
+ code: "INVALID_PACKAGE_KEY",
200
+ path: packageKeyPath,
201
+ message: "Widget package key must be URL-safe dot-separated text (letters, numbers, '.', '_', '~', and '-' only)."
202
+ });
203
+ if (!isUrlSafeSemver(sourcePackage.version)) errors.push({
204
+ code: "INVALID_VERSION",
205
+ path: "version",
206
+ message: "Widget package version must be a URL-safe SemVer version without build metadata (for example 1.2.3 or 1.2.3-beta.1)."
207
+ });
208
+ if (sourcePackage.widgets.length === 0) errors.push({
209
+ code: "NO_WIDGETS",
210
+ path: "widgets",
211
+ message: "Widget package must include at least one widget."
212
+ });
213
+ const packageId = `${resolvedOwner}.${packageKey}`;
214
+ const seenNames = /* @__PURE__ */ new Set();
215
+ const seenTypes = /* @__PURE__ */ new Set();
216
+ const widgets = [];
217
+ sourcePackage.widgets.forEach((widget, index) => {
218
+ const path = `widgets[${index}]`;
219
+ if (!isUrlSafeWidgetName(widget.name)) {
220
+ errors.push({
221
+ code: "INVALID_WIDGET_NAME",
222
+ path: `${path}.name`,
223
+ message: "Widget name must be URL-safe text (letters, numbers, '_', '~', and '-' only) and cannot be empty."
224
+ });
225
+ return;
226
+ }
227
+ if (seenNames.has(widget.name)) {
228
+ errors.push({
229
+ code: "DUPLICATE_WIDGET",
230
+ path: `${path}.name`,
231
+ message: `Duplicate widget name "${widget.name}" found in package ${packageKey}.`
232
+ });
233
+ return;
234
+ }
235
+ seenNames.add(widget.name);
236
+ const type = `${packageId}.${widget.name}`;
237
+ if (!isUrlSafeDottedIdentifier(type)) {
238
+ errors.push({
239
+ code: "INVALID_WIDGET_TYPE",
240
+ path: `${path}.type`,
241
+ message: `Generated widget type "${type}" is not URL-safe.`
242
+ });
243
+ return;
244
+ }
245
+ if (seenTypes.has(type)) {
246
+ errors.push({
247
+ code: "DUPLICATE_WIDGET",
248
+ path: `${path}.type`,
249
+ message: `Duplicate generated widget type "${type}" found.`
250
+ });
251
+ return;
252
+ }
253
+ seenTypes.add(type);
254
+ widgets.push(sourceWidgetToDescriptor(widget, type));
255
+ });
256
+ if (errors.length > 0) return {
257
+ success: false,
258
+ errors
259
+ };
260
+ return {
261
+ success: true,
262
+ value: {
263
+ sourcePackage,
264
+ packageKey,
265
+ packageId,
266
+ owner: resolvedOwner,
267
+ version: sourcePackage.version,
268
+ widgets
269
+ },
270
+ errors: []
271
+ };
272
+ }
273
+ function buildWidgetPackagePublishManifest(validated, options = {}) {
274
+ return {
275
+ manifestVersion: WIDGET_PACKAGE_MANIFEST_VERSION,
276
+ runtime: "remote-dom",
277
+ packageId: validated.packageId,
278
+ source: validated.owner,
279
+ version: validated.version,
280
+ workerEntryUrl: toWidgetPackageArtifactPath("widget.js"),
281
+ cssUrls: mergeCssUrls(validated.sourcePackage.cssUrls, options.cssUrls),
282
+ assetUrls: [],
283
+ widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
284
+ };
285
+ }
286
+ function sourceWidgetToDescriptor(widget, type) {
287
+ const displayName = widget.displayName ?? widget.name;
288
+ return {
289
+ type,
290
+ name: widget.name,
291
+ displayName,
292
+ description: widget.description ?? `Custom widget ${displayName}`,
293
+ icon: widget.icon ?? DEFAULT_WIDGET_ICON,
294
+ category: widget.category ?? DEFAULT_WIDGET_CATEGORY,
295
+ propertySchema: normalizePropertySchema(widget.propertySchema, type),
296
+ defaultProps: widget.defaultProps ?? {},
297
+ container: normalizeContainer(widget.container),
298
+ minSdkVersion: widget.minSdkVersion ?? DEFAULT_MIN_SDK_VERSION,
299
+ resizable: normalizeResizable(widget.resizable),
300
+ capabilities: normalizeCapabilities(widget.capabilities ?? [])
301
+ };
302
+ }
303
+ function normalizeCapabilities(capabilities) {
304
+ return capabilities.map((capability) => ({
305
+ name: capability.name,
306
+ version: capability.version ?? "1"
307
+ }));
308
+ }
309
+ function validationFailure(error) {
310
+ return {
311
+ success: false,
312
+ errors: [error]
313
+ };
314
+ }
315
+ function validateSourcePackageShape(value) {
316
+ if (!isRecord$4(value)) return [{
317
+ code: "INVALID_SOURCE_PACKAGE",
318
+ message: "Widget source package must be a JSON object."
319
+ }];
320
+ const errors = [];
321
+ requireStringField(value, "packageId", "packageId", errors);
322
+ requireStringField(value, "version", "version", errors);
323
+ validateOptionalStringField(value, "scope", "scope", errors);
324
+ validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
325
+ validateOptionalStringField(value, "packageType", "packageType", errors);
326
+ if (Object.prototype.hasOwnProperty.call(value, "navigationOrigins")) errors.push({
327
+ code: "INVALID_SOURCE_PACKAGE",
328
+ path: "navigationOrigins",
329
+ message: "navigationOrigins has been removed. Declare the externalNavigation capability and call allowAnchorUrl(url) for each external link."
330
+ });
331
+ if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
332
+ code: "INVALID_SOURCE_PACKAGE",
333
+ path: "cssUrls",
334
+ message: "Widget package cssUrls must be an array of strings when present."
335
+ });
336
+ else value.cssUrls.forEach((cssUrl, index) => {
337
+ validateCssUrl(cssUrl, `cssUrls[${index}]`, errors);
338
+ });
339
+ if (!Array.isArray(value.widgets)) {
340
+ errors.push({
341
+ code: "INVALID_SOURCE_PACKAGE",
342
+ path: "widgets",
343
+ message: "Widget package widgets must be an array."
344
+ });
345
+ return errors;
346
+ }
347
+ value.widgets.forEach((widget, index) => {
348
+ const widgetPath = `widgets[${index}]`;
349
+ if (!isRecord$4(widget)) {
350
+ errors.push({
351
+ code: "INVALID_WIDGET_METADATA",
352
+ path: widgetPath,
353
+ message: "Widget metadata must be a JSON object."
354
+ });
355
+ return;
356
+ }
357
+ requireStringField(widget, "name", `${widgetPath}.name`, errors, {
358
+ code: "INVALID_WIDGET_NAME",
359
+ message: "Widget name must be a string and cannot be empty."
360
+ });
361
+ validateOptionalStringField(widget, "displayName", `${widgetPath}.displayName`, errors, { requireNonEmpty: true });
362
+ validateOptionalStringField(widget, "description", `${widgetPath}.description`, errors, { requireNonEmpty: true });
363
+ validateOptionalStringField(widget, "icon", `${widgetPath}.icon`, errors, { requireNonEmpty: true });
364
+ validateOptionalStringField(widget, "category", `${widgetPath}.category`, errors, { requireNonEmpty: true });
365
+ validateOptionalStringField(widget, "minSdkVersion", `${widgetPath}.minSdkVersion`, errors, { requireNonEmpty: true });
366
+ validateOptionalJsonRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
367
+ validateOptionalJsonRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
368
+ validateOptionalCapabilities(widget, "capabilities", `${widgetPath}.capabilities`, errors);
369
+ });
370
+ return errors;
371
+ }
372
+ function requireStringField(record, key, path, errors, override) {
373
+ const value = record[key];
374
+ if (typeof value === "string" && value.trim().length > 0) return;
375
+ errors.push({
376
+ code: override?.code ?? "INVALID_SOURCE_PACKAGE",
377
+ path,
378
+ message: override?.message ?? `${path} must be a string and cannot be empty.`
379
+ });
380
+ }
381
+ function validateOptionalStringField(record, key, path, errors, options = {}) {
382
+ const value = record[key];
383
+ if (value === void 0) return;
384
+ if (typeof value === "string") {
385
+ if (!options.requireNonEmpty || value.trim().length > 0) return;
386
+ errors.push({
387
+ code: "INVALID_SOURCE_PACKAGE",
388
+ path,
389
+ message: `${path} must be a non-empty string when present.`
390
+ });
391
+ return;
392
+ }
393
+ errors.push({
394
+ code: "INVALID_SOURCE_PACKAGE",
395
+ path,
396
+ message: `${path} must be a string when present.`
397
+ });
398
+ }
399
+ function validateCssUrl(value, cssUrlPath, errors) {
400
+ if (typeof value !== "string" || value.trim().length === 0) {
401
+ errors.push({
402
+ code: "INVALID_SOURCE_PACKAGE",
403
+ path: cssUrlPath,
404
+ message: "Widget package cssUrls entries must be non-empty strings."
405
+ });
406
+ return;
407
+ }
408
+ if (!isWidgetPackageCssArtifactPath(value)) errors.push({
409
+ code: "INVALID_SOURCE_PACKAGE",
410
+ path: cssUrlPath,
411
+ 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."
412
+ });
413
+ }
414
+ function validateOptionalJsonRecordField(record, key, path, errors) {
415
+ const value = record[key];
416
+ if (value === void 0) return;
417
+ if (isRecord$4(value) && isJsonValue(value)) return;
418
+ errors.push({
419
+ code: "INVALID_WIDGET_METADATA",
420
+ path,
421
+ message: `${path} must be a JSON object when present.`
422
+ });
423
+ }
424
+ function validateOptionalCapabilities(record, key, path, errors) {
425
+ const value = record[key];
426
+ if (value === void 0) return;
427
+ if (!Array.isArray(value)) {
428
+ errors.push({
429
+ code: "INVALID_WIDGET_METADATA",
430
+ path,
431
+ message: `${path} must be an array when present.`
432
+ });
433
+ return;
434
+ }
435
+ const names = /* @__PURE__ */ new Set();
436
+ value.forEach((candidate, index) => {
437
+ const capabilityPath = `${path}[${index}]`;
438
+ if (!isRecord$4(candidate)) {
439
+ errors.push({
440
+ code: "INVALID_WIDGET_METADATA",
441
+ path: capabilityPath,
442
+ message: `${capabilityPath} must be a JSON object.`
443
+ });
444
+ return;
445
+ }
446
+ const name = candidate.name;
447
+ if (!isCanonicalMetadataString(name)) {
448
+ errors.push({
449
+ code: "INVALID_WIDGET_METADATA",
450
+ path: `${capabilityPath}.name`,
451
+ message: `${capabilityPath}.name must be a non-empty string without surrounding whitespace or control characters.`
452
+ });
453
+ return;
454
+ }
455
+ const version = candidate.version;
456
+ if (version !== void 0 && !isCanonicalMetadataString(version)) errors.push({
457
+ code: "INVALID_WIDGET_METADATA",
458
+ path: `${capabilityPath}.version`,
459
+ message: `${capabilityPath}.version must be a non-empty string without surrounding whitespace or control characters when present.`
460
+ });
461
+ if (names.has(name)) {
462
+ errors.push({
463
+ code: "INVALID_WIDGET_METADATA",
464
+ path: `${capabilityPath}.name`,
465
+ message: `${path} must not contain duplicate capability name ${JSON.stringify(name)}.`
466
+ });
467
+ return;
468
+ }
469
+ names.add(name);
470
+ });
471
+ }
472
+ function isCanonicalMetadataString(value) {
473
+ return typeof value === "string" && value.length > 0 && value === value.trim() && !/\p{Cc}/u.test(value);
474
+ }
475
+ function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
476
+ const generatedCssUrlSet = new Set(generatedCssUrls ?? []);
477
+ generatedCssUrls?.forEach((cssUrl, index) => {
478
+ validateGeneratedCssArtifactUrl(cssUrl, `generated cssUrls[${index}]`);
479
+ });
480
+ sourceCssUrls?.forEach((cssUrl, index) => {
481
+ validateGeneratedCssArtifactUrl(cssUrl, `cssUrls[${index}]`);
482
+ if (generatedCssUrlSet.has(cssUrl)) return;
483
+ 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.`);
484
+ });
485
+ return Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []])).map(toWidgetPackageArtifactPath);
486
+ }
487
+ function toWidgetPackageArtifactPath(value) {
488
+ return value;
489
+ }
490
+ function validateGeneratedCssArtifactUrl(value, cssUrlPath) {
491
+ if (value.trim().length === 0) throw new Error(`${cssUrlPath} must be a non-empty string.`);
492
+ if (!isWidgetPackageCssArtifactPath(value)) throw new Error(`${cssUrlPath} must be a top-level generated CSS artifact filename matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them.`);
493
+ }
494
+ function normalizePropertySchema(value, widgetType) {
495
+ return {
496
+ ...value ?? {},
497
+ widgetType
498
+ };
499
+ }
500
+ function normalizeContainer(value) {
501
+ if (value === "inline" || value === "block" || value === "card" || value === "fullscreen") return value;
502
+ return DEFAULT_WIDGET_CONTAINER;
503
+ }
504
+ function normalizeResizable(value) {
505
+ if (typeof value === "boolean") return value;
506
+ if (value === "horizontal" || value === "vertical" || value === "both") return value;
507
+ if (!isRecord$4(value)) return false;
508
+ const horizontal = value.horizontal === true;
509
+ const vertical = value.vertical === true;
510
+ if (horizontal && vertical) return "both";
511
+ if (horizontal) return "horizontal";
512
+ if (vertical) return "vertical";
513
+ return false;
514
+ }
515
+ function resolvePackageKey(sourcePackage, owner) {
516
+ const packageStableId = readNonEmptyTrimmedString(sourcePackage.packageStableId);
517
+ if (packageStableId) return prefixPackageStableIdWithNonOwnerScope(packageStableId, sourcePackage.scope);
518
+ return stripOwnerScopeFromPackageId(sourcePackage.packageId, sourcePackage.scope, owner);
519
+ }
520
+ function prefixPackageStableIdWithNonOwnerScope(packageStableId, sourceScope) {
521
+ const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
522
+ if (!sourceScopeText || isWidgetPackageOwnerKind(sourceScopeText)) return packageStableId;
523
+ const sourceScopePrefix = `${sourceScopeText}.`;
524
+ if (packageStableId.startsWith(sourceScopePrefix)) return packageStableId;
525
+ return `${sourceScopeText}.${packageStableId}`;
526
+ }
527
+ function stripOwnerScopeFromPackageId(packageId, sourceScope, owner) {
528
+ const ownerPrefix = `${owner}.`;
529
+ if (packageId.startsWith(ownerPrefix) && packageId.length > ownerPrefix.length) return packageId.slice(ownerPrefix.length);
530
+ const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
531
+ if (isWidgetPackageOwnerKind(sourceScopeText) && packageId.startsWith(`${sourceScopeText}.`) && packageId.length > sourceScopeText.length + 1) return packageId.slice(sourceScopeText.length + 1);
532
+ return packageId;
533
+ }
534
+ function readNonEmptyTrimmedString(value) {
535
+ const trimmed = value?.trim();
536
+ return trimmed ? trimmed : void 0;
537
+ }
538
+ function isWidgetPackageOwnerKind(value) {
539
+ return value === "company" || value === "droplet";
540
+ }
541
+ function isUrlSafeDottedIdentifier(value) {
542
+ return URL_SAFE_DOTTED_IDENTIFIER_PATTERN.test(value);
543
+ }
544
+ function isUrlSafeWidgetName(value) {
545
+ return URL_SAFE_WIDGET_NAME_PATTERN.test(value);
546
+ }
547
+ function isUrlSafeSemver(value) {
548
+ return SEMVER_URL_SAFE_PATTERN.test(value);
549
+ }
550
+ function isRecord$4(value) {
551
+ return typeof value === "object" && value !== null && !Array.isArray(value);
552
+ }
553
+ //#endregion
554
+ //#region ../widget-tooling/src/development-descriptor.ts
555
+ function createWidgetPackageDevelopmentDescriptor(options) {
556
+ const validation = validateSingleSourceWidgetPackage(readSourcePackages(options.sourceModule), { owner: options.owner });
557
+ if (!validation.success || !validation.value) throw new WidgetToolingError("VALIDATION_FAILED", validation.errors.map((error) => error.message).join("\n") || "Widget package validation failed.", { diagnostics: validation.errors });
558
+ return validatedPackageToDescriptor(validation.value, options);
559
+ }
560
+ function validatedPackageToDescriptor(validated, options) {
561
+ const cssUrls = validated.sourcePackage.cssUrls ?? [];
562
+ const workerEntryPath = options.workerEntryPath ?? "/__runtime-entry__";
563
+ const defaultCssPath = options.defaultCssPath ?? "/styles.css";
564
+ return {
565
+ manifestVersion: 1,
566
+ runtime: "remote-dom",
567
+ packageId: validated.packageId,
568
+ source: validated.owner,
569
+ version: validated.version,
570
+ workerEntryUrl: new URL(workerEntryPath, options.origin).href,
571
+ cssUrls: cssUrls.length > 0 ? cssUrls.map((url) => toDevelopmentCssUrl(url, options.origin)) : [toDevelopmentCssUrl(defaultCssPath, options.origin)],
572
+ assetUrls: [],
573
+ widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
574
+ };
575
+ }
576
+ function readSourcePackages(sourceModule) {
577
+ const packages = sourceModule["widgetPackages"];
578
+ if (Array.isArray(packages) && packages.length > 0) return packages;
579
+ const widgetPackage = sourceModule["widgetPackage"];
580
+ if (widgetPackage !== void 0) return [widgetPackage];
581
+ const defaultPackage = sourceModule["default"];
582
+ return defaultPackage === void 0 ? [] : [defaultPackage];
583
+ }
584
+ function toDevelopmentCssUrl(url, origin) {
585
+ const { href } = new URL(url, origin);
586
+ return href.includes("?") ? `${href}&direct` : `${href}?direct`;
587
+ }
588
+ //#endregion
589
+ //#region ../widget-tooling/src/dev-routes.ts
590
+ function fluidWidgetDevPlugin(options = {}) {
591
+ const entryModulePath = options.entryModulePath ?? WIDGET_ENTRY_MODULE_PATH;
592
+ const configModulePath = options.configModulePath ?? WIDGET_CONFIG_MODULE_PATH;
593
+ const owner = options.owner ?? "droplet";
594
+ return {
595
+ name: "fluid-widget-dev-routes",
596
+ apply: "serve",
597
+ config: createWidgetDevServerConfig,
598
+ configureServer(server) {
599
+ server.middlewares.use(async (request, response, next) => {
600
+ const pathname = (request.url ?? "").split("?")[0] ?? "";
601
+ if (pathname === "/__runtime-entry__" || pathname === "/__runtime-entry__/") {
602
+ sendJavaScript(request, response, buildRuntimeEntryBootstrapScript(entryModulePath, getRequestOrigin(request)));
603
+ return;
604
+ }
605
+ if (pathname === "/__widget-packages__" || pathname === "/__widget-packages__/") {
606
+ await sendWidgetPackages(server, request, response, {
607
+ configModulePath,
608
+ owner,
609
+ allowNoPackages: options.allowNoPackages === true
610
+ });
611
+ return;
612
+ }
613
+ next();
614
+ });
615
+ }
616
+ };
617
+ }
618
+ /**
619
+ * Widget projects intentionally have no HTML entrypoint, so Vite cannot
620
+ * discover the worker's dependency graph during its normal startup scan.
621
+ * Without an explicit entry, the first live-preview request discovers and
622
+ * optimizes the portal worker after the server reports ready. Vite reloads
623
+ * the worker mid-mount, closing its message thread and stranding the preview
624
+ * in a permanent error state.
625
+ *
626
+ * Pre-optimizing the worker entry makes the initial graph stable before a
627
+ * preview can connect. Vite follows this entry's imports, so React and the
628
+ * Remote DOM worker runtime are optimized in the same startup pass.
629
+ */
630
+ function createWidgetDevServerConfig() {
631
+ return {
632
+ optimizeDeps: { include: ["@fluid-app/portal-sdk/widgets/worker"] },
633
+ ssr: { noExternal: ["@fluid-app/portal-sdk"] }
634
+ };
635
+ }
636
+ /** Source-relative path of a widget project's entry module. The dev
637
+ * scaffold always places it here (see the widget template) — this is
638
+ * the one thing the bootstrap script below needs from the plugin
639
+ * that isn't itself part of the race/preamble fix. */
640
+ const WIDGET_ENTRY_MODULE_PATH = "/src/index.ts";
641
+ /** Source-relative path of a standalone widget project's package source. */
642
+ const WIDGET_CONFIG_MODULE_PATH = "/fluid.widget.config.ts";
643
+ /**
644
+ * Builds the `/__runtime-entry__` worker bootstrap. Runtime-verified
645
+ * against a real widget dev server + `@fluid-app/widget-runtime` host
646
+ * in a Playwright harness (styled Leaderboard widget rendering,
647
+ * mutation records flowing, `mounted.ready` resolving) — every piece
648
+ * below exists to fix something that reproduced in that harness:
649
+ *
650
+ * 1. **Message-buffer race (the silent-mount killer).** The host
651
+ * (widget-runtime) posts the "mount" message immediately after
652
+ * `new Worker(...)`. A production-built worker bundle evaluates
653
+ * synchronously and never races that message. In dev, though, the
654
+ * worker's module graph loads asynchronously over many HTTP
655
+ * requests (this bootstrap script, then the entry module, then
656
+ * everything it imports, each a separate fetch) — so the "mount"
657
+ * message can be dispatched and consumed by the Worker's default
658
+ * (listener-less) handling before the widget entry's own
659
+ * `message` listener, wired up deep inside the awaited import
660
+ * below, is ever attached. The message is dropped silently, with
661
+ * no error anywhere — the old `void import("/src/index.ts")` also
662
+ * swallowed the import's own rejections, compounding the silence.
663
+ * Traced live: host→worker "mount" posts, zero worker→host replies
664
+ * until this buffer-and-replay was added; after it, mutation
665
+ * records flow and `mounted.ready` resolves. Fix: buffer every
666
+ * message from the instant the worker boots (before anything else
667
+ * runs), then replay the buffer, in order, once the entry module
668
+ * has finished importing and has had the chance to attach its
669
+ * real listener.
670
+ * 2. **React-refresh preamble.** `@vitejs/plugin-react` injects a
671
+ * preamble into every dev-transformed `.tsx` module referencing
672
+ * `window.$RefreshReg$` / `$RefreshSig$`. A Worker global scope has
673
+ * no `window` and no preamble, so importing a react-based widget
674
+ * entry throws `window is not defined`, and — once `window` is
675
+ * aliased to `globalThis` — throws again on the bare
676
+ * `$RefreshSig$` alias. Installing the canonical preamble
677
+ * (`RefreshRuntime.injectIntoGlobalHook` + no-op `$RefreshReg$` /
678
+ * `$RefreshSig$`) before the entry import fixes both. The
679
+ * `window = globalThis` alias was checked against widget-runtime's
680
+ * worker lockdown / scope detection (which is
681
+ * `globalThis.postMessage`-based) — no conflict. `/@react-refresh`
682
+ * is `@vitejs/plugin-react`'s stable virtual module id, but a
683
+ * widget project isn't guaranteed to use the react plugin, so the
684
+ * whole block is try/catch-guarded — a project with no
685
+ * `/@react-refresh` module (no react plugin) boots straight
686
+ * through to the entry import unaffected.
687
+ * 3. **`await import`, not `void import`.** A genuine failure to
688
+ * import the entry module (syntax error, missing dependency, …)
689
+ * must reject the worker's module evaluation loudly, not vanish
690
+ * the way a fire-and-forget `void import` did.
691
+ * 4. **Absolute dev-server imports.** Mist and portal previews load the
692
+ * worker from another origin, so the host wraps this entry in a blob URL.
693
+ * Root-relative specifiers inside a blob module resolve against the host
694
+ * page (or fail to resolve at all), not this Vite server. Anchoring both
695
+ * imports to the request origin keeps the entire dev module graph on the
696
+ * widget server.
697
+ *
698
+ * Dev-only: this only ever serves from `configureServer` (the
699
+ * plugin's `apply: "serve"` gate), never a built/published package.
700
+ */
701
+ function buildRuntimeEntryBootstrapScript(entryPath, origin) {
702
+ const refreshRuntimeUrl = new URL("/@react-refresh", origin).toString();
703
+ const entryUrl = new URL(entryPath, origin).toString();
704
+ return [
705
+ "// Dev-only worker bootstrap — see buildRuntimeEntryBootstrapScript",
706
+ "// in packages/cli/widget-tooling/src/dev-routes.ts for why each piece",
707
+ "// here exists (message-buffer race, react-refresh preamble,",
708
+ "// await-not-void import).",
709
+ "const __q = [];",
710
+ "const __buf = (e) => { __q.push(e.data); };",
711
+ "self.addEventListener(\"message\", __buf);",
712
+ "if (typeof window === \"undefined\") { globalThis.window = globalThis; }",
713
+ "try {",
714
+ ` const { default: RefreshRuntime } = await import(${JSON.stringify(refreshRuntimeUrl)});`,
715
+ " RefreshRuntime.injectIntoGlobalHook(window);",
716
+ " window.$RefreshReg$ = () => {};",
717
+ " window.$RefreshSig$ = () => (type) => type;",
718
+ " window.__vite_plugin_react_preamble_installed__ = true;",
719
+ "} catch {",
720
+ " // No @vitejs/plugin-react in this widget project (no",
721
+ " // /@react-refresh virtual module) — nothing to preinstall.",
722
+ "}",
723
+ `await import(${JSON.stringify(entryUrl)});`,
724
+ "self.removeEventListener(\"message\", __buf);",
725
+ "for (const data of __q) {",
726
+ " self.dispatchEvent(new MessageEvent(\"message\", { data }));",
727
+ "}",
728
+ ""
729
+ ].join("\n");
730
+ }
731
+ /**
732
+ * The shared development descriptor workflow appends Vite's `?direct` query
733
+ * to CSS URLs so preview hosts receive real CSS instead of JS style modules.
734
+ * Published descriptors use a separate build path and never receive it.
735
+ */
736
+ function createDevWidgetPackageDescriptors(configModule, origin, owner = "droplet") {
737
+ return [createWidgetPackageDevelopmentDescriptor({
738
+ sourceModule: configModule,
739
+ owner,
740
+ origin,
741
+ workerEntryPath: "/__runtime-entry__",
742
+ defaultCssPath: "/styles.css"
743
+ })];
744
+ }
745
+ /**
746
+ * True when validation failed *only* because the project declares no widget
747
+ * package. Checked against the typed diagnostics rather than message text, and
748
+ * deliberately narrow: a config that declares a package and gets it wrong must
749
+ * still be reported.
750
+ */
751
+ function isNoSourcePackageError(error) {
752
+ if (!(error instanceof WidgetToolingError)) return false;
753
+ const diagnostics = error.diagnostics ?? [];
754
+ return diagnostics.length > 0 && diagnostics.every((diagnostic) => diagnostic.code === "NO_SOURCE_PACKAGE");
755
+ }
756
+ async function sendWidgetPackages(server, request, response, options) {
757
+ if (handleOptions(request, response)) return;
758
+ try {
759
+ sendJson(response, createDevWidgetPackageDescriptors(await server.ssrLoadModule(options.configModulePath), getRequestOrigin(request), options.owner));
760
+ } catch (error) {
761
+ if (options.allowNoPackages && isNoSourcePackageError(error)) {
762
+ sendJson(response, []);
763
+ return;
764
+ }
765
+ server.config.logger.error(`[fluid] Failed to load widget packages: ${String(error)}`);
766
+ sendJson(response, { error: String(error) }, 500);
767
+ }
768
+ }
769
+ function sendJavaScript(request, response, source) {
770
+ if (handleOptions(request, response)) return;
771
+ setCorsHeaders(response);
772
+ response.setHeader("Content-Type", "text/javascript");
773
+ response.statusCode = 200;
774
+ response.end(source);
775
+ }
776
+ function sendJson(response, body, statusCode = 200) {
777
+ setCorsHeaders(response);
778
+ response.setHeader("Content-Type", "application/json");
779
+ response.statusCode = statusCode;
780
+ response.end(JSON.stringify(body, null, 2));
781
+ }
782
+ function handleOptions(request, response) {
783
+ if (request.method !== "OPTIONS") return false;
784
+ setCorsHeaders(response);
785
+ response.statusCode = 204;
786
+ response.end();
787
+ return true;
788
+ }
789
+ function setCorsHeaders(response) {
790
+ response.setHeader("Access-Control-Allow-Origin", "*");
791
+ response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
792
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
793
+ }
794
+ function getRequestOrigin(request) {
795
+ const host = readHeader(request.headers.host);
796
+ if (!host) throw new Error("Widget dev request is missing the Host header.");
797
+ return `${readHeader(request.headers["x-forwarded-proto"]) === "https" ? "https" : "http"}://${host}`;
798
+ }
799
+ function readHeader(value) {
800
+ const candidate = Array.isArray(value) ? value[0] : value;
801
+ return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : void 0;
802
+ }
803
+ //#endregion
804
+ //#region ../widget-tooling/src/node-runtime.ts
805
+ /**
806
+ * How this package runs the helper scripts it generates.
807
+ *
808
+ * Building a widget package writes two scripts to disk and runs them: a config
809
+ * extractor (under `tsx`) and a Vite build. Both used to assume the same thing —
810
+ * that `process.execPath` is Node and that `tsx` is resolvable from this
811
+ * module. Both assumptions hold for a CLI and neither holds in an Electron main
812
+ * process, where `process.execPath` is the Electron binary. Spawning it without
813
+ * `ELECTRON_RUN_AS_NODE` starts a second copy of the app instead of running the
814
+ * script, and `tsx` is not in a packaged app's dependency closure at all.
815
+ *
816
+ * So the host says how to run a script rather than this package guessing. The
817
+ * default reproduces the old behaviour exactly, which is why both CLIs are
818
+ * unchanged by this.
819
+ */
820
+ /** The executable to spawn, and the env to spawn it with. */
821
+ function resolveSpawnTarget(runtime) {
822
+ return {
823
+ execPath: runtime?.execPath ?? process.execPath,
824
+ env: runtime?.env ?? {}
825
+ };
826
+ }
827
+ /**
828
+ * Resolves `tsx`'s CLI, preferring an injected path.
829
+ *
830
+ * Deliberately a function rather than a module-scope constant. It used to be
831
+ * `const TSX_CLI_PATH = require.resolve("tsx/cli")`, which runs on IMPORT — so a
832
+ * host that merely imported this package, and never published anything, crashed
833
+ * at startup if `tsx` was absent. Resolving on use means a missing `tsx` is a
834
+ * publish-time error the caller can report, not a boot failure.
835
+ */
836
+ function resolveTsxCliPath(runtime) {
837
+ if (runtime?.tsxCliPath) return runtime.tsxCliPath;
838
+ return createRequire(import.meta.url).resolve("tsx/cli");
839
+ }
840
+ //#endregion
841
+ //#region ../widget-tooling/src/internal/widget-package-config.ts
842
+ const WIDGET_CONFIG_PATH = "src/widgets.config.ts";
843
+ /** Long enough for a cold tsx start on a slow machine, short enough to fail. */
844
+ const CONFIG_EVAL_TIMEOUT_MS = 6e4;
845
+ const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
846
+ const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
847
+ const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
848
+ async function resolvePortalWidgetSourceConfig(projectDir) {
849
+ const candidate = path.join(projectDir, WIDGET_CONFIG_PATH);
850
+ return await fs.pathExists(candidate) ? {
851
+ path: candidate,
852
+ relativePath: WIDGET_CONFIG_PATH
853
+ } : void 0;
854
+ }
855
+ async function loadSourceWidgetPackages(projectDir, nodeRuntime) {
856
+ const config = await resolvePortalWidgetSourceConfig(projectDir);
857
+ if (!config) return success([]);
858
+ let tempDir;
859
+ try {
860
+ tempDir = await createTempDirectory(projectDir);
861
+ const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
862
+ const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
863
+ await fs.writeFile(extractFile, createSourcePackageExtractorScript({
864
+ projectDir,
865
+ configPath: config.path
866
+ }), {
867
+ encoding: "utf-8",
868
+ flag: "wx"
869
+ });
870
+ const spawn = resolveSpawnTarget(nodeRuntime);
871
+ await execa(spawn.execPath, [
872
+ resolveTsxCliPath(nodeRuntime),
873
+ extractFile,
874
+ outputFile
875
+ ], {
876
+ cwd: projectDir,
877
+ stdio: "pipe",
878
+ env: {
879
+ ...process.env,
880
+ ...spawn.env,
881
+ NODE_ENV: "production"
882
+ },
883
+ timeout: CONFIG_EVAL_TIMEOUT_MS,
884
+ killSignal: "SIGKILL"
885
+ });
886
+ const outputResult = await readSourcePackageExtractorOutput(outputFile);
887
+ if (!outputResult.success) return outputResult;
888
+ const parsed = outputResult.value;
889
+ if (parsed.length === 0) return success([]);
890
+ return success(parsed);
891
+ } catch (err) {
892
+ const error = err;
893
+ return failure({
894
+ code: "CONFIG_LOAD_FAILED",
895
+ message: `Failed to load widget packages from ${config.relativePath}`,
896
+ details: error.stderr ?? error.message ?? String(err)
897
+ });
898
+ } finally {
899
+ if (tempDir) await fs.remove(tempDir).catch(() => {});
900
+ }
901
+ }
902
+ async function createTempDirectory(projectDir) {
903
+ const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
904
+ try {
905
+ await fs.ensureDir(projectTmpDir);
906
+ return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
907
+ } catch (err) {
908
+ const message = err instanceof Error ? err.message : String(err);
909
+ throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
910
+ }
911
+ }
912
+ async function readSourcePackageExtractorOutput(outputFile) {
913
+ let output;
914
+ try {
915
+ output = await fs.readFile(outputFile, "utf-8");
916
+ } catch (err) {
917
+ return failure({
918
+ code: "CONFIG_LOAD_FAILED",
919
+ message: "Widget package extractor did not write an output file",
920
+ details: err instanceof Error ? err.message : String(err)
921
+ });
922
+ }
923
+ let parsed;
924
+ try {
925
+ parsed = JSON.parse(output);
926
+ } catch {
927
+ return failure({
928
+ code: "INVALID_FORMAT",
929
+ message: "Failed to parse widget package output file as JSON",
930
+ details: `Output was: ${output.slice(0, 200)}`
931
+ });
932
+ }
933
+ if (!isRecord$3(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
934
+ code: "INVALID_FORMAT",
935
+ message: "Widget package extractor output file had an invalid sentinel"
936
+ });
937
+ if (!Array.isArray(parsed.data)) return failure({
938
+ code: "INVALID_FORMAT",
939
+ message: "Extracted widget package output is not an array",
940
+ details: `Expected an array, got: ${typeof parsed.data}`
941
+ });
942
+ return success(parsed.data);
943
+ }
944
+ function createSourcePackageExtractorScript(options) {
945
+ return `
946
+ import fs from "node:fs/promises";
947
+ import path from "node:path";
948
+ import { createServer, normalizePath } from "vite";
949
+
950
+ const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
951
+ const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
952
+ const projectRoot = ${JSON.stringify(options.projectDir)};
953
+ const widgetConfigPath = ${JSON.stringify(options.configPath)};
954
+ const outputPath = process.argv[2];
955
+ if (!outputPath) throw new Error("Missing widget package extractor output path.");
956
+
957
+ function toViteModuleId(modulePath) {
958
+ const relativePath = path.relative(projectRoot, modulePath);
959
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
960
+ return "/" + normalizePath(relativePath);
961
+ }
962
+ return normalizePath(modulePath);
963
+ }
964
+
965
+ const server = await createServer({
966
+ root: projectRoot,
967
+ mode: "production",
968
+ server: { middlewareMode: true },
969
+ appType: "custom",
970
+ logLevel: "error",
971
+ clearScreen: false,
972
+ resolve: {
973
+ conditions: ["module", "browser", "development|production"],
974
+ },
975
+ ssr: {
976
+ noExternal: ["@fluid-app/portal-sdk"],
977
+ resolve: {
978
+ conditions: ["module", "node", "development|production"],
979
+ },
980
+ },
981
+ });
982
+
983
+ try {
984
+ const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
985
+
986
+ function isSourceWidgetPackage(value) {
987
+ return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
988
+ }
989
+
990
+ function collectSourceWidgetPackages(mod) {
991
+ const candidates = [
992
+ mod.widgetPackage,
993
+ ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
994
+ mod.default,
995
+ ];
996
+ const byPackageId = new Map();
997
+ for (const candidate of candidates) {
998
+ if (!isSourceWidgetPackage(candidate)) continue;
999
+ if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
1000
+ }
1001
+ return Array.from(byPackageId.values());
1002
+ }
1003
+
1004
+ function serializeWidget(widget) {
1005
+ if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
1006
+ return widget;
1007
+ }
1008
+ const { component, ...metadata } = widget;
1009
+ return metadata;
1010
+ }
1011
+
1012
+ function serializePackage(sourcePackage) {
1013
+ return {
1014
+ manifestVersion: sourcePackage.manifestVersion,
1015
+ scope: sourcePackage.scope,
1016
+ packageStableId: sourcePackage.packageStableId,
1017
+ packageId: sourcePackage.packageId,
1018
+ packageType: sourcePackage.packageType,
1019
+ version: sourcePackage.version,
1020
+ cssUrls: sourcePackage.cssUrls,
1021
+ ...(Object.prototype.hasOwnProperty.call(sourcePackage, "navigationOrigins")
1022
+ ? { navigationOrigins: sourcePackage.navigationOrigins }
1023
+ : {}),
1024
+ widgets: Array.isArray(sourcePackage.widgets)
1025
+ ? sourcePackage.widgets.map(serializeWidget)
1026
+ : sourcePackage.widgets,
1027
+ };
1028
+ }
1029
+
1030
+ await fs.writeFile(
1031
+ outputPath,
1032
+ JSON.stringify({
1033
+ sentinel: OUTPUT_SENTINEL,
1034
+ data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
1035
+ }),
1036
+ "utf-8",
1037
+ );
1038
+ } finally {
1039
+ await server.close();
1040
+ }
1041
+ `;
1042
+ }
1043
+ function isRecord$3(value) {
1044
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1045
+ }
1046
+ //#endregion
1047
+ //#region ../widget-tooling/src/internal/widget-package-builder.ts
1048
+ const DEFAULT_PUBLISH_DIR = ".fluid/widget-dist";
1049
+ const DEFAULT_RUNTIME_VERSION = "1";
1050
+ const TEMP_BUILD_DIR = ".fluid/widget-build";
1051
+ const TEMP_CLI_DIR = ".fluid/tmp";
1052
+ const PROJECT_VITE_CONFIG_CANDIDATES = [
1053
+ "vite.config.ts",
1054
+ "vite.config.mts",
1055
+ "vite.config.js",
1056
+ "vite.config.mjs"
1057
+ ];
1058
+ const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
1059
+ const require = createRequire(import.meta.url);
1060
+ function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
1061
+ const label = options.optionName ?? "publishDir";
1062
+ if (path.isAbsolute(publishDir)) throw new Error(`${label} must be relative to the project directory.`);
1063
+ const normalizedPublishDir = normalizePublishDirForValidation(publishDir);
1064
+ if (normalizedPublishDir === ".fluid" || !normalizedPublishDir.startsWith(".fluid/")) throw new Error(`${label} must be a relative path under .fluid/.`);
1065
+ if (isPathWithinReservedDir(normalizedPublishDir, TEMP_BUILD_DIR)) throw new Error(`${label} must not be ${TEMP_BUILD_DIR} because it is reserved for temporary builder output.`);
1066
+ if (isPathWithinReservedDir(normalizedPublishDir, TEMP_CLI_DIR)) throw new Error(`${label} must not be ${TEMP_CLI_DIR} because it is reserved for Fluid CLI temporary files.`);
1067
+ const resolvedProjectDir = path.resolve(projectDir);
1068
+ const resolvedPublishDir = path.resolve(resolvedProjectDir, publishDir);
1069
+ const relative = path.relative(resolvedProjectDir, resolvedPublishDir);
1070
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${label} must stay inside the project directory.`);
1071
+ }
1072
+ /**
1073
+ * Restamps each loaded source package with `version`.
1074
+ *
1075
+ * Object spread carries own enumerable SYMBOL keys as well as string ones,
1076
+ * which matters here: `defineWidgetPackage` marks its result with a symbol that
1077
+ * validation uses to recognise a real source package. A copy that dropped it
1078
+ * would fail validation with a message about a missing package rather than
1079
+ * about the version.
1080
+ */
1081
+ function withVersionOverride(sourcePackages, version) {
1082
+ return sourcePackages.map((sourcePackage) => typeof sourcePackage === "object" && sourcePackage !== null ? {
1083
+ ...sourcePackage,
1084
+ version
1085
+ } : sourcePackage);
1086
+ }
1087
+ async function buildSharedWidgetPackage(options) {
1088
+ const publishDirInput = options.publishDir ?? DEFAULT_PUBLISH_DIR;
1089
+ try {
1090
+ await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
1091
+ } catch (err) {
1092
+ return failure({
1093
+ code: "INVALID_PUBLISH_DIR",
1094
+ message: "Unsafe widget package publish directory",
1095
+ details: (err instanceof Error ? err : new Error(String(err))).message
1096
+ });
1097
+ }
1098
+ const sourcePackageResult = await loadSourceWidgetPackages(options.projectDir, options.nodeRuntime);
1099
+ if (!sourcePackageResult.success) return failure({
1100
+ code: "CONFIG_LOAD_FAILED",
1101
+ message: sourcePackageResult.error.message,
1102
+ details: sourcePackageResult.error.details
1103
+ });
1104
+ const validation = validateSingleSourceWidgetPackage(options.versionOverride ? withVersionOverride(sourcePackageResult.value, options.versionOverride) : sourcePackageResult.value, { owner: options.owner });
1105
+ if (!validation.success || !validation.value) return failure({
1106
+ code: "VALIDATION_FAILED",
1107
+ message: validation.errors.map((error) => error.message).join("\n")
1108
+ });
1109
+ try {
1110
+ await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
1111
+ const publishDir = path.resolve(options.projectDir, publishDirInput);
1112
+ const widgetScriptPath = path.join(publishDir, "widget.js");
1113
+ await fs.emptyDir(publishDir);
1114
+ const tempDir = path.resolve(options.projectDir, TEMP_BUILD_DIR);
1115
+ await fs.emptyDir(tempDir);
1116
+ let buildFailure;
1117
+ try {
1118
+ await buildWidgetScriptBundle({
1119
+ projectDir: options.projectDir,
1120
+ tempDir,
1121
+ publishDir,
1122
+ validated: validation.value,
1123
+ ...options.nodeRuntime ? { nodeRuntime: options.nodeRuntime } : {}
1124
+ });
1125
+ } catch (error) {
1126
+ buildFailure = { error };
1127
+ }
1128
+ try {
1129
+ await fs.remove(tempDir);
1130
+ } catch {
1131
+ const cleanupFailure = /* @__PURE__ */ new Error("Failed to remove reserved temporary build directory.");
1132
+ if (buildFailure) {
1133
+ const primaryError = buildFailure.error instanceof Error ? buildFailure.error : new Error(String(buildFailure.error));
1134
+ throw new AggregateError([primaryError, cleanupFailure], `${primaryError.message}\n${cleanupFailure.message}`);
1135
+ }
1136
+ throw cleanupFailure;
1137
+ }
1138
+ if (buildFailure) throw buildFailure.error;
1139
+ const cssUrls = await collectCssUrls(publishDir);
1140
+ const descriptor = buildWidgetPackagePublishManifest(validation.value, { cssUrls });
1141
+ const manifestContent = stableStringify(descriptor);
1142
+ const publishPackageId = descriptor.packageId;
1143
+ const manifestPath = path.join(publishDir, "manifest.json");
1144
+ const publishManifestPath = path.join(publishDir, "publish-manifest.json");
1145
+ await fs.writeFile(manifestPath, manifestContent, "utf-8");
1146
+ const artifacts = await collectWidgetPackageArtifacts(publishDir);
1147
+ const publishManifest = createPublishManifestMetadata({
1148
+ packageId: publishPackageId,
1149
+ version: validation.value.version,
1150
+ cliVersion: options.cliVersion,
1151
+ runtimeVersion: DEFAULT_RUNTIME_VERSION,
1152
+ manifestContent,
1153
+ artifacts
1154
+ });
1155
+ await fs.writeFile(publishManifestPath, stableStringify(publishManifest), "utf-8");
1156
+ return success({
1157
+ publishDir,
1158
+ packageId: validation.value.packageId,
1159
+ version: validation.value.version,
1160
+ manifestPath,
1161
+ publishManifestPath,
1162
+ widgetScriptPath
1163
+ });
1164
+ } catch (err) {
1165
+ const error = err instanceof Error ? err : new Error(String(err));
1166
+ const isPublishDirValidationError = /^(publishDir|reserved temporary build directory) /.test(error.message);
1167
+ return failure({
1168
+ code: isPublishDirValidationError ? "INVALID_PUBLISH_DIR" : "WRITE_FAILED",
1169
+ message: isPublishDirValidationError ? "Unsafe widget package publish directory" : "Failed to build shared widget package artifacts",
1170
+ details: error.message
1171
+ });
1172
+ }
1173
+ }
1174
+ function createWidgetPackageEntrySource(options) {
1175
+ return `import { createElement } from "react";
1176
+ import { startWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
1177
+ ${options.cssImportPath ? `import ${JSON.stringify(options.cssImportPath)};` : ""}
1178
+
1179
+ const widgetConfig = await import(${JSON.stringify(options.configImportPath)});
1180
+ const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
1181
+ const descriptors = ${JSON.stringify(options.validated.widgets, null, 2)};
1182
+
1183
+ function isSourceWidgetPackage(value) {
1184
+ return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
1185
+ }
1186
+
1187
+ function collectSourceWidgetPackages(mod) {
1188
+ return [
1189
+ mod.widgetPackage,
1190
+ ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
1191
+ mod.default,
1192
+ ].filter(isSourceWidgetPackage);
1193
+ }
1194
+
1195
+ const sourcePackage = collectSourceWidgetPackages(widgetConfig)[0];
1196
+ if (!sourcePackage) {
1197
+ throw new Error("No Fluid source widget package found while loading widget package worker bundle.");
1198
+ }
1199
+
1200
+ const componentsByName = new Map(
1201
+ sourcePackage.widgets.map((widget) => [widget.name, widget.component]),
1202
+ );
1203
+
1204
+ const widgets = descriptors.map((descriptor) => {
1205
+ const component = componentsByName.get(descriptor.name);
1206
+ if (typeof component !== "function") {
1207
+ throw new Error("Widget package is missing Remote DOM component for " + descriptor.name + ".");
1208
+ }
1209
+ return {
1210
+ ...descriptor,
1211
+ component: (props) => createElement(component, props),
1212
+ };
1213
+ });
1214
+
1215
+ startWidgetPackage({ widgets });
1216
+ `;
1217
+ }
1218
+ async function buildWidgetScriptBundle(options) {
1219
+ const sourceConfig = await resolvePortalWidgetSourceConfig(options.projectDir);
1220
+ if (!sourceConfig) throw new Error("No portal widget source config found.");
1221
+ const entryPath = path.join(options.tempDir, "widget-entry.ts");
1222
+ const viteConfigPath = path.join(options.tempDir, "vite.config.mjs");
1223
+ const rootStylesPath = path.join(options.projectDir, "styles.css");
1224
+ await fs.writeFile(entryPath, createWidgetPackageEntrySource({
1225
+ configImportPath: pathToFileURL(sourceConfig.path).href,
1226
+ cssImportPath: await fs.pathExists(rootStylesPath) ? pathToFileURL(rootStylesPath).href : void 0,
1227
+ validated: options.validated
1228
+ }), "utf-8");
1229
+ await fs.writeFile(viteConfigPath, createViteConfigSource({
1230
+ entryPath,
1231
+ publishDir: options.publishDir,
1232
+ projectConfigPath: await resolveProjectViteConfigPath(options.projectDir)
1233
+ }), "utf-8");
1234
+ const viteCliPath = resolveViteCliPath(options.projectDir);
1235
+ const spawn = resolveSpawnTarget(options.nodeRuntime);
1236
+ await execa(spawn.execPath, [
1237
+ viteCliPath,
1238
+ "build",
1239
+ "--config",
1240
+ viteConfigPath
1241
+ ], {
1242
+ cwd: options.projectDir,
1243
+ stdio: "pipe",
1244
+ env: {
1245
+ ...process.env,
1246
+ ...spawn.env,
1247
+ NODE_ENV: "production"
1248
+ }
1249
+ });
1250
+ }
1251
+ function resolveViteCliPath(projectDir) {
1252
+ const projectViteCliPath = resolveViteCliPathFromRequire(createRequire(path.join(projectDir, "package.json")));
1253
+ if (projectViteCliPath) return projectViteCliPath;
1254
+ const cliViteCliPath = resolveViteCliPathFromRequire(require);
1255
+ if (cliViteCliPath) return cliViteCliPath;
1256
+ throw new Error("Unable to resolve Vite for the portal project. Install the project dependencies and ensure vite is available before building widget packages.");
1257
+ }
1258
+ async function resolveProjectViteConfigPath(projectDir) {
1259
+ for (const fileName of PROJECT_VITE_CONFIG_CANDIDATES) {
1260
+ const candidate = path.join(projectDir, fileName);
1261
+ if (await fs.pathExists(candidate)) return candidate;
1262
+ }
1263
+ }
1264
+ function createViteConfigSource(options) {
1265
+ const widgetConfigSource = createWidgetViteConfigObjectSource({
1266
+ ...options,
1267
+ disableConfigFileLookup: !options.projectConfigPath
1268
+ });
1269
+ if (!options.projectConfigPath) return `import { defineConfig } from "vite";
1270
+
1271
+ export default defineConfig(${widgetConfigSource});
1272
+ `;
1273
+ return `import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
1274
+
1275
+ const projectConfigPath = ${JSON.stringify(options.projectConfigPath)};
1276
+ const widgetConfig = ${widgetConfigSource};
1277
+ const fluidWidgetDefineInvariants = {
1278
+ "process.env.NODE_ENV": JSON.stringify("production"),
1279
+ };
1280
+ const blockedFluidPluginNames = new Set(${JSON.stringify(BLOCKED_FLUID_PROJECT_PLUGIN_NAMES)});
1281
+
1282
+ function isAllowedProjectPlugin(plugin) {
1283
+ return !plugin || typeof plugin !== "object" || !blockedFluidPluginNames.has(plugin.name);
1284
+ }
1285
+
1286
+ function sanitizeProjectPlugins(pluginOption) {
1287
+ if (!Array.isArray(pluginOption)) {
1288
+ return isAllowedProjectPlugin(pluginOption) ? pluginOption : [];
1289
+ }
1290
+
1291
+ return pluginOption
1292
+ .map((plugin) => Array.isArray(plugin) ? sanitizeProjectPlugins(plugin) : plugin)
1293
+ .filter(isAllowedProjectPlugin);
1294
+ }
1295
+
1296
+ function copyProjectConfigValue(source, target, key) {
1297
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
1298
+ target[key] = source[key];
1299
+ }
1300
+ }
1301
+
1302
+ function sanitizeProjectConfig(config) {
1303
+ if (!config || typeof config !== "object") return {};
1304
+
1305
+ const sanitized = {};
1306
+ if (Object.prototype.hasOwnProperty.call(config, "plugins")) {
1307
+ sanitized.plugins = sanitizeProjectPlugins(config.plugins);
1308
+ }
1309
+ copyProjectConfigValue(config, sanitized, "resolve");
1310
+ copyProjectConfigValue(config, sanitized, "define");
1311
+ copyProjectConfigValue(config, sanitized, "css");
1312
+ copyProjectConfigValue(config, sanitized, "esbuild");
1313
+ copyProjectConfigValue(config, sanitized, "json");
1314
+ copyProjectConfigValue(config, sanitized, "assetsInclude");
1315
+ copyProjectConfigValue(config, sanitized, "envDir");
1316
+ copyProjectConfigValue(config, sanitized, "envPrefix");
1317
+ return sanitized;
1318
+ }
1319
+
1320
+ function copyConfigDefineValue(value) {
1321
+ return value && typeof value === "object" && !Array.isArray(value)
1322
+ ? { ...value }
1323
+ : {};
1324
+ }
1325
+
1326
+ function mergeFluidWidgetDefineInvariants(mergedConfig, safeProjectConfig, widgetConfig) {
1327
+ mergedConfig.define = {
1328
+ ...copyConfigDefineValue(safeProjectConfig.define),
1329
+ ...copyConfigDefineValue(mergedConfig.define),
1330
+ ...copyConfigDefineValue(widgetConfig.define),
1331
+ ...fluidWidgetDefineInvariants,
1332
+ };
1333
+ return mergedConfig;
1334
+ }
1335
+
1336
+ export default defineConfig(async (configEnv) => {
1337
+ const projectConfig = await loadConfigFromFile(configEnv, projectConfigPath);
1338
+ const safeProjectConfig = sanitizeProjectConfig(projectConfig?.config ?? {});
1339
+ return mergeFluidWidgetDefineInvariants(mergeConfig(safeProjectConfig, widgetConfig), safeProjectConfig, widgetConfig);
1340
+ });
1341
+ `;
1342
+ }
1343
+ function createWidgetViteConfigObjectSource(options) {
1344
+ const configFileSource = options.disableConfigFileLookup ? " configFile: false,\n" : "";
1345
+ return `(() => {
1346
+ const fluidWidgetDefineInvariants = {
1347
+ "process.env.NODE_ENV": JSON.stringify("production"),
1348
+ };
1349
+
1350
+ function createFluidWidgetBuildInvariants() {
1351
+ return {
1352
+ copyPublicDir: false,
1353
+ emptyOutDir: false,
1354
+ outDir: ${JSON.stringify(options.publishDir)},
1355
+ sourcemap: false,
1356
+ target: "esnext",
1357
+ cssCodeSplit: false,
1358
+ lib: {
1359
+ entry: ${JSON.stringify(options.entryPath)},
1360
+ formats: ["es"],
1361
+ fileName: () => "widget.js",
1362
+ cssFileName: "widget",
1363
+ },
1364
+ rollupOptions: {
1365
+ output: {
1366
+ inlineDynamicImports: true,
1367
+ hoistTransitiveImports: false,
1368
+ },
1369
+ },
1370
+ };
1371
+ }
1372
+
1373
+ function fluidWidgetBuildInvariantsPlugin() {
1374
+ return {
1375
+ name: "fluid-widget-build-invariants",
1376
+ enforce: "post",
1377
+ config() {
1378
+ return { build: createFluidWidgetBuildInvariants() };
1379
+ },
1380
+ configResolved(config) {
1381
+ config.define = {
1382
+ ...(config.define ?? {}),
1383
+ ...fluidWidgetDefineInvariants,
1384
+ };
1385
+ Object.assign(config.build, createFluidWidgetBuildInvariants());
1386
+ },
1387
+ };
1388
+ }
1389
+
1390
+ return {
1391
+ ${configFileSource} publicDir: false,
1392
+ define: fluidWidgetDefineInvariants,
1393
+ resolve: {
1394
+ conditions: [
1395
+ "module",
1396
+ "browser",
1397
+ "development|production",
1398
+ ],
1399
+ },
1400
+ plugins: [fluidWidgetBuildInvariantsPlugin()],
1401
+ build: createFluidWidgetBuildInvariants(),
1402
+ };
1403
+ })()`;
1404
+ }
1405
+ async function validateWidgetPackageOutputPaths(projectDir, publishDir) {
1406
+ validateSharedWidgetPackagePublishDir(projectDir, publishDir);
1407
+ await rejectSymlinkedPathSegments({
1408
+ projectDir,
1409
+ relativePath: publishDir,
1410
+ label: "publishDir"
1411
+ });
1412
+ await rejectSymlinkedPathSegments({
1413
+ projectDir,
1414
+ relativePath: TEMP_BUILD_DIR,
1415
+ label: "reserved temporary build directory"
1416
+ });
1417
+ }
1418
+ async function collectCssUrls(publishDir) {
1419
+ return (await fs.readdir(publishDir, { withFileTypes: true })).filter((entry) => entry.isFile() && isWidgetPackageCssArtifactPath(entry.name)).map((entry) => entry.name).sort();
1420
+ }
1421
+ async function rejectSymlinkedPathSegments(options) {
1422
+ const resolvedProjectDir = path.resolve(options.projectDir);
1423
+ const resolvedTargetPath = path.resolve(resolvedProjectDir, options.relativePath);
1424
+ const segments = path.relative(resolvedProjectDir, resolvedTargetPath).split(path.sep).filter(Boolean);
1425
+ let currentPath = resolvedProjectDir;
1426
+ for (const segment of segments) {
1427
+ currentPath = path.join(currentPath, segment);
1428
+ let stat;
1429
+ try {
1430
+ stat = await fs.lstat(currentPath);
1431
+ } catch (err) {
1432
+ if (isNodeError(err) && err.code === "ENOENT") return;
1433
+ throw err;
1434
+ }
1435
+ if (stat.isSymbolicLink()) throw new Error(`${options.label} must not include symbolic links.`);
1436
+ }
1437
+ }
1438
+ function normalizePublishDirForValidation(value) {
1439
+ return toPosixPath(path.normalize(value)).replace(/\/+$/, "") || ".";
1440
+ }
1441
+ function isPathWithinReservedDir(value, reservedDir) {
1442
+ return value === reservedDir || value.startsWith(`${reservedDir}/`);
1443
+ }
1444
+ function toPosixPath(value) {
1445
+ return value.split(path.sep).join("/");
1446
+ }
1447
+ function stableStringify(value) {
1448
+ return `${JSON.stringify(value, null, 2)}\n`;
1449
+ }
1450
+ function resolveViteCliPathFromRequire(moduleRequire) {
1451
+ try {
1452
+ const packageJsonPath = moduleRequire.resolve("vite/package.json");
1453
+ const binPath = getViteBinPath(moduleRequire(packageJsonPath).bin);
1454
+ if (!binPath) return void 0;
1455
+ return path.resolve(path.dirname(packageJsonPath), binPath);
1456
+ } catch (err) {
1457
+ if (isModuleResolutionError(err)) return void 0;
1458
+ throw err;
1459
+ }
1460
+ }
1461
+ function getViteBinPath(bin) {
1462
+ if (typeof bin === "string") return bin;
1463
+ if (!isRecord$2(bin)) return void 0;
1464
+ const viteBin = bin.vite;
1465
+ return typeof viteBin === "string" ? viteBin : void 0;
1466
+ }
1467
+ function isRecord$2(value) {
1468
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1469
+ }
1470
+ function isNodeError(value) {
1471
+ return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string";
1472
+ }
1473
+ function isModuleResolutionError(value) {
1474
+ return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
1475
+ }
1476
+ //#endregion
1477
+ //#region ../widget-tooling/src/internal/widget-package-publish-request.ts
1478
+ async function createWidgetPackagePublishRequest(options) {
1479
+ const manifestContent = await fs.readFile(options.manifestPath);
1480
+ const manifest = parseJsonRecord(manifestContent.toString("utf-8"), options.manifestPath);
1481
+ const publishManifest = await readPublishManifest(options.publishManifestPath);
1482
+ assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
1483
+ assertMetadataMatches({
1484
+ label: "manifestHash",
1485
+ path: "manifest.json",
1486
+ expected: publishManifest.manifestHash,
1487
+ actual: sha256(manifestContent)
1488
+ });
1489
+ const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
1490
+ assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
1491
+ const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
1492
+ return {
1493
+ version: publishManifest.version,
1494
+ packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
1495
+ builderVersion: publishManifest.builderVersion,
1496
+ cliVersion: publishManifest.cliVersion,
1497
+ runtimeVersion: publishManifest.runtimeVersion,
1498
+ manifestHash: publishManifest.manifestHash,
1499
+ manifest,
1500
+ artifacts
1501
+ };
1502
+ }
1503
+ async function readJsonRecord(filePath) {
1504
+ return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
1505
+ }
1506
+ function parseJsonRecord(content, filePath) {
1507
+ const value = JSON.parse(content);
1508
+ if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
1509
+ return value;
1510
+ }
1511
+ async function readPublishManifest(filePath) {
1512
+ const record = await readJsonRecord(filePath);
1513
+ const artifactsValue = record["artifacts"];
1514
+ if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
1515
+ return {
1516
+ packageId: readRequiredString(record, "packageId", filePath),
1517
+ version: readRequiredString(record, "version", filePath),
1518
+ builderVersion: readRequiredString(record, "builderVersion", filePath),
1519
+ cliVersion: readRequiredString(record, "cliVersion", filePath),
1520
+ runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
1521
+ manifestHash: readRequiredString(record, "manifestHash", filePath),
1522
+ artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
1523
+ };
1524
+ }
1525
+ function readArtifactMetadata(value, index) {
1526
+ if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
1527
+ return {
1528
+ path: readRequiredString(value, "path", "publish-manifest.json"),
1529
+ sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
1530
+ bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
1531
+ contentType: readRequiredString(value, "contentType", "publish-manifest.json")
1532
+ };
1533
+ }
1534
+ async function readUploadArtifacts(publishDir, artifacts) {
1535
+ const resolvedPublishDir = path.resolve(publishDir);
1536
+ return Promise.all(artifacts.map(async (artifact) => {
1537
+ const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
1538
+ const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
1539
+ assertArtifactMetadataMatches(artifact, actual);
1540
+ return {
1541
+ ...actual,
1542
+ body: await fs.readFile(artifactPath)
1543
+ };
1544
+ }));
1545
+ }
1546
+ function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
1547
+ assertMetadataMatches({
1548
+ label: "packageId",
1549
+ path: "manifest.json",
1550
+ expected: publishManifest.packageId,
1551
+ actual: readRequiredString(manifest, "packageId", manifestPath)
1552
+ });
1553
+ assertMetadataMatches({
1554
+ label: "version",
1555
+ path: "manifest.json",
1556
+ expected: publishManifest.version,
1557
+ actual: readRequiredString(manifest, "version", manifestPath)
1558
+ });
1559
+ }
1560
+ function assertArtifactInventoryMatches(expected, actual) {
1561
+ const expectedByPath = /* @__PURE__ */ new Map();
1562
+ for (const artifact of expected) {
1563
+ if (expectedByPath.has(artifact.path)) throw new Error(`publish-manifest.json artifacts include duplicate path ${JSON.stringify(artifact.path)}. Rebuild the widget package before publishing.`);
1564
+ expectedByPath.set(artifact.path, artifact);
1565
+ }
1566
+ const actualPaths = /* @__PURE__ */ new Set();
1567
+ for (const actualArtifact of actual) {
1568
+ actualPaths.add(actualArtifact.path);
1569
+ const expectedArtifact = expectedByPath.get(actualArtifact.path);
1570
+ if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
1571
+ assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
1572
+ }
1573
+ for (const expectedArtifact of expected) {
1574
+ if (actualPaths.has(expectedArtifact.path)) continue;
1575
+ throw new Error(`publish-manifest.json artifacts include ${JSON.stringify(expectedArtifact.path)}, but that file is not present on disk. Rebuild the widget package before publishing.`);
1576
+ }
1577
+ }
1578
+ function assertArtifactMetadataMatches(expected, actual) {
1579
+ assertMetadataMatches({
1580
+ label: "path",
1581
+ path: expected.path,
1582
+ expected: expected.path,
1583
+ actual: actual.path
1584
+ });
1585
+ assertMetadataMatches({
1586
+ label: "sha256",
1587
+ path: expected.path,
1588
+ expected: expected.sha256,
1589
+ actual: actual.sha256
1590
+ });
1591
+ assertMetadataMatches({
1592
+ label: "bytes",
1593
+ path: expected.path,
1594
+ expected: expected.bytes,
1595
+ actual: actual.bytes
1596
+ });
1597
+ assertMetadataMatches({
1598
+ label: "contentType",
1599
+ path: expected.path,
1600
+ expected: expected.contentType,
1601
+ actual: actual.contentType
1602
+ });
1603
+ }
1604
+ function assertMetadataMatches(options) {
1605
+ if (options.expected === options.actual) return;
1606
+ throw new Error(`publish-manifest.json ${options.label} for ${JSON.stringify(options.path)} does not match the file on disk. Recorded ${JSON.stringify(options.expected)}, computed ${JSON.stringify(options.actual)}. Rebuild the widget package before publishing.`);
1607
+ }
1608
+ function resolveArtifactPath(publishDir, artifactPath) {
1609
+ const resolved = path.resolve(publishDir, artifactPath);
1610
+ const relative = path.relative(publishDir, resolved);
1611
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
1612
+ return resolved;
1613
+ }
1614
+ function stripOwnerPrefix(packageId, owner) {
1615
+ const prefix = `${owner}.`;
1616
+ if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
1617
+ return packageId;
1618
+ }
1619
+ function readRequiredString(record, key, filePath) {
1620
+ const value = record[key];
1621
+ if (typeof value === "string" && value.trim().length > 0) return value;
1622
+ throw new Error(`${path.basename(filePath)} must include string ${key}.`);
1623
+ }
1624
+ function readRequiredNumber(record, key, filePath) {
1625
+ const value = record[key];
1626
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1627
+ throw new Error(`${path.basename(filePath)} must include number ${key}.`);
1628
+ }
1629
+ function isRecord$1(value) {
1630
+ return typeof value === "object" && value !== null;
1631
+ }
1632
+ //#endregion
1633
+ //#region ../../api-clients/fluidos/src/hey-api/sdk.gen.ts
1634
+ /**
1635
+ * Create a Fluid OS widget package version
1636
+ *
1637
+ * Start a company-owned Fluid OS widget package publish flow and return signed upload URLs for each artifact.
1638
+ */
1639
+ const fluidOsV0CreateWidgetPackageVersion = (options) => options.client.post({
1640
+ responseStyle: "data",
1641
+ security: [{
1642
+ scheme: "bearer",
1643
+ type: "http"
1644
+ }],
1645
+ url: "/api/company/fluid_os/widget_package_versions",
1646
+ ...options,
1647
+ headers: {
1648
+ "Content-Type": "application/json",
1649
+ ...options.headers
1650
+ }
1651
+ });
1652
+ /**
1653
+ * Complete a Fluid OS widget package upload
1654
+ *
1655
+ * Verify uploaded artifacts for a company-owned Fluid OS widget package version and approve the version when verification succeeds. Uploaded manifest.json is verified for matching packageId, matching version, and valid company-owned `company.<package_key>.` widget type prefixes.
1656
+ */
1657
+ const fluidOsV0CompleteWidgetPackageVersionUpload = (options) => options.client.post({
1658
+ responseStyle: "data",
1659
+ security: [{
1660
+ scheme: "bearer",
1661
+ type: "http"
1662
+ }],
1663
+ url: "/api/company/fluid_os/widget_package_versions/{version}/complete_upload",
1664
+ ...options,
1665
+ headers: {
1666
+ "Content-Type": "application/json",
1667
+ ...options.headers
1668
+ }
1669
+ });
1670
+ /**
1671
+ * Mint a short-lived git credential for the company widget repo
1672
+ *
1673
+ * Returns an HTTPS clone URL with a fresh installation token from the
1674
+ * Fluid GitHub App embedded as the password. The CLI uses it
1675
+ * transparently — no `gh`, no PAT, no SSH key.
1676
+ *
1677
+ * Valid for ~1 hour (GitHub installation token TTL); re-mint on each
1678
+ * operation.
1679
+ *
1680
+ */
1681
+ const fluidOsV0WidgetPackageGitCredentialCreate = (options) => options.client.post({
1682
+ responseStyle: "data",
1683
+ security: [{
1684
+ scheme: "bearer",
1685
+ type: "http"
1686
+ }],
1687
+ url: "/api/company/fluid_os/widget_package/git_credential",
1688
+ ...options
1689
+ });
1690
+ //#endregion
1691
+ //#region ../widget-tooling/src/internal/widget-package-upload.ts
1692
+ var WidgetPackageUploadError = class extends Error {
1693
+ code;
1694
+ constructor(code, message) {
1695
+ super(message);
1696
+ this.name = "WidgetPackageUploadError";
1697
+ this.code = code;
1698
+ }
1699
+ };
1700
+ function buildWidgetPackageArtifactPayload(artifact) {
1701
+ return {
1702
+ path: artifact.path,
1703
+ sha256: artifact.sha256,
1704
+ bytes: artifact.bytes,
1705
+ contentType: artifact.contentType
1706
+ };
1707
+ }
1708
+ function buildWidgetPackageUploadSessionPayload(request) {
1709
+ return {
1710
+ version: request.version,
1711
+ package_key: request.packageKey,
1712
+ builder_version: request.builderVersion,
1713
+ cli_version: request.cliVersion,
1714
+ runtime_version: request.runtimeVersion,
1715
+ manifest_hash: request.manifestHash,
1716
+ manifest: request.manifest,
1717
+ artifacts: request.artifacts.map(buildWidgetPackageArtifactPayload)
1718
+ };
1719
+ }
1720
+ function buildWidgetPackageCompleteUploadPayload(packageKey, artifacts) {
1721
+ return {
1722
+ package_key: packageKey,
1723
+ artifacts: artifacts.map(buildWidgetPackageArtifactPayload)
1724
+ };
1725
+ }
1726
+ function getWidgetPackageUploadSessionRoute(owner) {
1727
+ switch (owner.kind) {
1728
+ case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions`;
1729
+ case "company": return "/api/company/fluid_os/widget_package_versions";
1730
+ }
1731
+ }
1732
+ function getWidgetPackageCompleteUploadRoute(owner, version) {
1733
+ switch (owner.kind) {
1734
+ case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
1735
+ case "company": return `/api/company/fluid_os/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
1736
+ }
1737
+ }
1738
+ async function requestWidgetPackageUploadSession(client, owner, request) {
1739
+ const payload = buildWidgetPackageUploadSessionPayload(request);
1740
+ try {
1741
+ return normalizeWidgetPackageUploadSession(owner.kind === "company" ? await fluidOsV0CreateWidgetPackageVersion({
1742
+ body: payload,
1743
+ client
1744
+ }) : await client.post({
1745
+ body: payload,
1746
+ responseStyle: "data",
1747
+ url: getWidgetPackageUploadSessionRoute(owner)
1748
+ }), request.version, request.artifacts);
1749
+ } catch (err) {
1750
+ if (err instanceof WidgetPackageUploadError) throw err;
1751
+ throw new WidgetPackageUploadError("SESSION_REQUEST_FAILED", `Failed to create widget package upload session: ${formatUnknownError(err)}`);
1752
+ }
1753
+ }
1754
+ async function uploadWidgetPackageArtifacts(session, artifacts, fetchImpl = fetch) {
1755
+ const uploadsByPath = new Map(session.uploads.map((upload) => [upload.path, upload]));
1756
+ const results = [];
1757
+ for (const artifact of artifacts) {
1758
+ const upload = uploadsByPath.get(artifact.path);
1759
+ if (!upload) throw new WidgetPackageUploadError("MISSING_SIGNED_URL", `Upload session did not include a signed URL for artifact "${artifact.path}".`);
1760
+ if (upload.contentType !== artifact.contentType) throw new WidgetPackageUploadError("CONTENT_TYPE_MISMATCH", `Upload session expected artifact "${artifact.path}" to use content type "${upload.contentType}", but local artifact uses "${artifact.contentType}".`);
1761
+ let response;
1762
+ try {
1763
+ response = await fetchImpl(upload.url, {
1764
+ method: "PUT",
1765
+ headers: mergeUploadHeaders(upload.headers, upload.contentType),
1766
+ body: artifact.body
1767
+ });
1768
+ } catch (err) {
1769
+ throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": ${formatUnknownError(err)}`);
1770
+ }
1771
+ if (!response.ok) {
1772
+ const body = await response.text().catch(() => "");
1773
+ const details = body.trim() ? `: ${body.trim().slice(0, 500)}` : "";
1774
+ throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": PUT returned HTTP ${response.status}${details}`);
1775
+ }
1776
+ results.push({
1777
+ path: artifact.path,
1778
+ status: response.status,
1779
+ contentType: upload.contentType
1780
+ });
1781
+ }
1782
+ return results;
1783
+ }
1784
+ async function completeWidgetPackageUpload(client, owner, version, packageKey, artifacts) {
1785
+ const payload = buildWidgetPackageCompleteUploadPayload(packageKey, artifacts);
1786
+ try {
1787
+ return normalizeWidgetPackageCompleteResponse(owner.kind === "company" ? await fluidOsV0CompleteWidgetPackageVersionUpload({
1788
+ body: payload,
1789
+ client,
1790
+ path: { version }
1791
+ }) : await client.post({
1792
+ body: payload,
1793
+ responseStyle: "data",
1794
+ url: getWidgetPackageCompleteUploadRoute(owner, version)
1795
+ }), version);
1796
+ } catch (err) {
1797
+ if (err instanceof WidgetPackageUploadError) throw err;
1798
+ throw new WidgetPackageUploadError("COMPLETE_UPLOAD_FAILED", `Failed to complete widget package upload: ${formatUnknownError(err)}`);
1799
+ }
1800
+ }
1801
+ async function publishWidgetPackageVersion(options) {
1802
+ const sessionPayload = buildWidgetPackageUploadSessionPayload(options.request);
1803
+ if (options.dryRun) return {
1804
+ dryRun: true,
1805
+ sessionPayload
1806
+ };
1807
+ if (!options.client) throw new Error("A FluidOS API client is required when dryRun is false.");
1808
+ const session = await requestWidgetPackageUploadSession(options.client, options.owner, options.request);
1809
+ return {
1810
+ dryRun: false,
1811
+ sessionPayload,
1812
+ session,
1813
+ uploadedArtifacts: await uploadWidgetPackageArtifacts(session, options.request.artifacts, options.fetchImpl),
1814
+ complete: await completeWidgetPackageUpload(options.client, options.owner, session.version, options.request.packageKey, options.request.artifacts)
1815
+ };
1816
+ }
1817
+ function normalizeWidgetPackageUploadSession(response, requestedVersion, requestedArtifacts) {
1818
+ const record = requireRecord(response, "INVALID_SESSION_RESPONSE", "Upload session response must be a JSON object.");
1819
+ requireFiniteNumberProperty(record, "version_id", "INVALID_SESSION_RESPONSE");
1820
+ requireStringProperty(record, "artifact_base_url", "INVALID_SESSION_RESPONSE");
1821
+ const uploadValues = record["uploads"];
1822
+ if (!Array.isArray(uploadValues)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", "Upload session response must contain an \"uploads\" array.");
1823
+ const requestedArtifactsByPath = new Map(requestedArtifacts.map((artifact) => [artifact.path, artifact]));
1824
+ const seenUploadPaths = /* @__PURE__ */ new Set();
1825
+ const uploads = uploadValues.map((value, index) => {
1826
+ const upload = requireRecord(value, "INVALID_SESSION_RESPONSE", `Upload session uploads[${index}] must be a JSON object.`);
1827
+ const path = requireStringProperty(upload, "path", "INVALID_SESSION_RESPONSE");
1828
+ const url = requireStringProperty(upload, "upload_url", "INVALID_SESSION_RESPONSE");
1829
+ requireStringProperty(upload, "public_url", "INVALID_SESSION_RESPONSE");
1830
+ const requestedArtifact = requestedArtifactsByPath.get(path);
1831
+ if (!requestedArtifact) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included unexpected artifact "${path}".`);
1832
+ if (seenUploadPaths.has(path)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included duplicate artifact "${path}".`);
1833
+ seenUploadPaths.add(path);
1834
+ return {
1835
+ path,
1836
+ url,
1837
+ contentType: requestedArtifact.contentType,
1838
+ headers: {}
1839
+ };
1840
+ });
1841
+ const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !seenUploadPaths.has(artifactPath));
1842
+ if (missingUploadPaths.length > 0) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response is missing signed URLs for artifact(s): ${missingUploadPaths.join(", ")}.`);
1843
+ return {
1844
+ version: requestedVersion,
1845
+ uploads,
1846
+ raw: response
1847
+ };
1848
+ }
1849
+ function normalizeWidgetPackageCompleteResponse(response, requestedVersion) {
1850
+ const record = requireRecord(response, "INVALID_COMPLETE_RESPONSE", "Complete upload response must be a JSON object.");
1851
+ const responseVersion = requireStringProperty(record, "version", "INVALID_COMPLETE_RESPONSE");
1852
+ if (responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
1853
+ const packageKey = requireStringProperty(record, "packageId", "INVALID_COMPLETE_RESPONSE");
1854
+ const status = requireStringProperty(record, "status", "INVALID_COMPLETE_RESPONSE");
1855
+ requireStringProperty(record, "scriptUrl", "INVALID_COMPLETE_RESPONSE");
1856
+ requireStringProperty(record, "manifestUrl", "INVALID_COMPLETE_RESPONSE");
1857
+ requireStringArrayProperty(record, "cssUrls", "INVALID_COMPLETE_RESPONSE");
1858
+ return {
1859
+ version: responseVersion,
1860
+ status,
1861
+ packageKey,
1862
+ raw: response
1863
+ };
1864
+ }
1865
+ function normalizeHeaderName(name) {
1866
+ return name.toLowerCase();
1867
+ }
1868
+ function mergeUploadHeaders(headers, contentType) {
1869
+ const merged = {};
1870
+ for (const [key, value] of Object.entries(headers)) {
1871
+ if (normalizeHeaderName(key) === "content-type") continue;
1872
+ merged[key] = value;
1873
+ }
1874
+ merged["Content-Type"] = contentType;
1875
+ return merged;
1876
+ }
1877
+ function requireRecord(value, code, message) {
1878
+ if (!isRecord(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
1879
+ return value;
1880
+ }
1881
+ function isRecord(value) {
1882
+ return typeof value === "object" && value !== null;
1883
+ }
1884
+ function requireStringProperty(record, key, code) {
1885
+ const value = record[key];
1886
+ if (typeof value === "string" && value.trim().length > 0) return value;
1887
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be a non-empty string.`);
1888
+ }
1889
+ function requireFiniteNumberProperty(record, key, code) {
1890
+ const value = record[key];
1891
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1892
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be a finite number.`);
1893
+ }
1894
+ function requireStringArrayProperty(record, key, code) {
1895
+ const value = record[key];
1896
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim().length > 0)) return value;
1897
+ throw new WidgetPackageUploadError(code, `Response field "${key}" must be an array of non-empty strings.`);
1898
+ }
1899
+ function formatUnknownError(err) {
1900
+ const base = err instanceof Error ? err.message : String(err);
1901
+ if (isRecord(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
1902
+ return base;
1903
+ }
1904
+ function safeStringify(value) {
1905
+ const seen = /* @__PURE__ */ new WeakSet();
1906
+ try {
1907
+ return JSON.stringify(value, (_key, item) => {
1908
+ if (typeof item === "bigint") return `${item}n`;
1909
+ if (typeof item !== "object" || item === null) return item;
1910
+ if (seen.has(item)) return "[Circular]";
1911
+ seen.add(item);
1912
+ return item;
1913
+ }) ?? String(value);
1914
+ } catch {
1915
+ return "[unserializable error data]";
1916
+ }
1917
+ }
1918
+ //#endregion
1919
+ //#region ../widget-tooling/src/workflows.ts
1920
+ async function loadAndValidateWidgetPackage(options) {
1921
+ const sourceConfig = await resolvePortalWidgetSourceConfig(options.projectDir);
1922
+ const loadResult = await loadSourceWidgetPackages(options.projectDir, options.nodeRuntime);
1923
+ if (!loadResult.success) throw new WidgetToolingError("CONFIG_LOAD_FAILED", loadResult.error.message, { details: loadResult.error.details });
1924
+ const validation = validateSingleSourceWidgetPackage(loadResult.value, { owner: options.owner });
1925
+ if (!validation.success || !validation.value) throw new WidgetToolingError("VALIDATION_FAILED", validation.errors.map((error) => error.message).join("\n") || "Widget package validation failed.", { diagnostics: validation.errors });
1926
+ return {
1927
+ projectDir: options.projectDir,
1928
+ sourceConfigPath: sourceConfig?.relativePath ?? "src/widgets.config.ts",
1929
+ validated: validation.value
1930
+ };
1931
+ }
1932
+ async function buildWidgetPackage(options) {
1933
+ const result = await buildSharedWidgetPackage({
1934
+ projectDir: options.projectDir,
1935
+ publishDir: options.outDir,
1936
+ owner: options.owner,
1937
+ cliVersion: options.cliVersion,
1938
+ ...options.version ? { versionOverride: options.version } : {},
1939
+ ...options.nodeRuntime ? { nodeRuntime: options.nodeRuntime } : {}
1940
+ });
1941
+ if (result.success) return result.value;
1942
+ throw new WidgetToolingError(result.error.code === "CONFIG_LOAD_FAILED" ? "CONFIG_LOAD_FAILED" : result.error.code === "VALIDATION_FAILED" ? "VALIDATION_FAILED" : result.error.code === "INVALID_PUBLISH_DIR" ? "INVALID_OUTPUT_DIR" : "BUILD_FAILED", result.error.message, { details: result.error.details });
1943
+ }
1944
+ async function publishWidgetPackage(options) {
1945
+ if (options.owner !== options.uploadOwner.kind) throw new WidgetToolingError("OWNER_MISMATCH", `Widget package build owner "${options.owner}" must match upload owner kind "${options.uploadOwner.kind}".`);
1946
+ if (!options.dryRun && !options.client) throw new WidgetToolingError("CLIENT_REQUIRED", "A FluidOS API client is required when dryRun is false.");
1947
+ const build = await buildWidgetPackage(options);
1948
+ let request;
1949
+ try {
1950
+ request = await createWidgetPackagePublishRequest({
1951
+ publishDir: build.publishDir,
1952
+ manifestPath: build.manifestPath,
1953
+ publishManifestPath: build.publishManifestPath,
1954
+ owner: options.owner
1955
+ });
1956
+ } catch (error) {
1957
+ throw new WidgetToolingError("ARTIFACT_VERIFICATION_FAILED", error instanceof Error ? error.message : "Failed to verify widget package artifacts.", { cause: error });
1958
+ }
1959
+ try {
1960
+ const publish = options.dryRun ? await publishWidgetPackageVersion({
1961
+ owner: options.uploadOwner,
1962
+ request,
1963
+ dryRun: true,
1964
+ fetchImpl: options.fetchImpl
1965
+ }) : await publishWidgetPackageVersion({
1966
+ client: options.client,
1967
+ owner: options.uploadOwner,
1968
+ request,
1969
+ fetchImpl: options.fetchImpl
1970
+ });
1971
+ return {
1972
+ build,
1973
+ request,
1974
+ publish
1975
+ };
1976
+ } catch (error) {
1977
+ if (error instanceof WidgetToolingError) throw error;
1978
+ if (error instanceof WidgetPackageUploadError) throw new WidgetToolingError(toPublicUploadErrorCode(error.code), error.message, { cause: error });
1979
+ throw new WidgetToolingError("PUBLISH_FAILED", error instanceof Error ? error.message : "Failed to publish widget package.", { cause: error });
1980
+ }
1981
+ }
1982
+ function toPublicUploadErrorCode(code) {
1983
+ switch (code) {
1984
+ case "SESSION_REQUEST_FAILED":
1985
+ case "INVALID_SESSION_RESPONSE":
1986
+ case "MISSING_SIGNED_URL":
1987
+ case "CONTENT_TYPE_MISMATCH":
1988
+ case "ARTIFACT_UPLOAD_FAILED":
1989
+ case "COMPLETE_UPLOAD_FAILED":
1990
+ case "INVALID_COMPLETE_RESPONSE": return code;
1991
+ default: return "PUBLISH_FAILED";
1992
+ }
1993
+ }
1994
+ //#endregion
1995
+ export { buildRuntimeEntryBootstrapScript as a, fluidWidgetDevPlugin as c, fluidOsV0WidgetPackageGitCredentialCreate as i, WidgetToolingError as l, loadAndValidateWidgetPackage as n, createDevWidgetPackageDescriptors as o, publishWidgetPackage as r, createWidgetDevServerConfig as s, buildWidgetPackage as t };
1996
+
1997
+ //# sourceMappingURL=src-_wfDoBLp.mjs.map