@fluid-app/fluid-cli-portal 0.1.53 → 0.1.55

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