@fluid-app/fluid-cli-widget 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/index.d.mts +185 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1405 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/templates/default/.oxlintrc.json +9 -0
- package/templates/default/AGENTS.md +10 -0
- package/templates/default/README.md +38 -0
- package/templates/default/fluid.widget.config.ts +9 -0
- package/templates/default/index.html +12 -0
- package/templates/default/manifest.ts +72 -0
- package/templates/default/package.json.template +37 -0
- package/templates/default/src/builder-preview.tsx +63 -0
- package/templates/default/src/index.ts +109 -0
- package/templates/default/src/preview-entry.tsx +60 -0
- package/templates/default/src/vite-env.d.ts +1 -0
- package/templates/default/src/widget-preview.tsx +17 -0
- package/templates/default/src/widgets/review-carousel/ReviewCarousel.tsx +95 -0
- package/templates/default/styles.css +173 -0
- package/templates/default/tsconfig.json +21 -0
- package/templates/default/vite.config.ts +296 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { buildSharedWidgetPackage, createAuthenticatedWidgetPackageClient, loadSourceWidgetPackages, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, publishWidgetRuntimeArtifacts, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir } from "@fluid-app/fluid-cli-portal";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { mkdir, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import prompts from "prompts";
|
|
11
|
+
//#region src/utils/config.ts
|
|
12
|
+
const WIDGET_CONFIG_FILE = "fluid.widget.config.ts";
|
|
13
|
+
var WidgetConfigParseError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "WidgetConfigParseError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function resolveWidgetConfigPath(projectDir) {
|
|
20
|
+
return path.join(projectDir, WIDGET_CONFIG_FILE);
|
|
21
|
+
}
|
|
22
|
+
async function loadWidgetConfig(projectDir) {
|
|
23
|
+
const configPath = resolveWidgetConfigPath(projectDir);
|
|
24
|
+
let source;
|
|
25
|
+
try {
|
|
26
|
+
source = await readFile(configPath, "utf-8");
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (isNodeError$1(err) && err.code === "ENOENT") return {
|
|
29
|
+
path: configPath,
|
|
30
|
+
config: {}
|
|
31
|
+
};
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
throw new Error(`Unable to read ${WIDGET_CONFIG_FILE}: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
path: configPath,
|
|
37
|
+
config: parseWidgetConfigSource(source)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function writeWidgetConfig(projectDir, config) {
|
|
41
|
+
await writeFile(resolveWidgetConfigPath(projectDir), formatWidgetConfig(config), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
function formatWidgetConfig(config) {
|
|
44
|
+
const lines = [
|
|
45
|
+
"import type { FluidWidgetConfig } from \"@fluid-app/fluid-cli-widget\";",
|
|
46
|
+
"",
|
|
47
|
+
"const config = {"
|
|
48
|
+
];
|
|
49
|
+
if (config.droplet) lines.push(` droplet: ${JSON.stringify(config.droplet)},`);
|
|
50
|
+
lines.push("} satisfies FluidWidgetConfig;", "", "const widgetConfig: FluidWidgetConfig = config;", "", "export default config;", "export const droplet = widgetConfig.droplet;", "export { widgetPackage, widgetPackages } from \"./manifest\";", "");
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
function parseWidgetConfigSource(source) {
|
|
54
|
+
return parseConfigObjectProperties(extractConfigObjectLiteral(source));
|
|
55
|
+
}
|
|
56
|
+
function mergeWidgetConfig(current, updates) {
|
|
57
|
+
const droplet = normalizeOptionalString(updates.droplet) ?? normalizeOptionalString(current.droplet);
|
|
58
|
+
return droplet ? { droplet } : {};
|
|
59
|
+
}
|
|
60
|
+
function validateDropletUuid(dropletUuid) {
|
|
61
|
+
if (!dropletUuid) return "Droplet UUID is required.";
|
|
62
|
+
if (dropletUuid.length > MAX_DROPLET_UUID_LENGTH) return `Droplet UUID must be ${MAX_DROPLET_UUID_LENGTH} characters or fewer.`;
|
|
63
|
+
if (!URL_SAFE_IDENTIFIER_PATTERN.test(dropletUuid)) return "Droplet UUID must be URL-safe text: letters, numbers, '_', '~', and '-' only.";
|
|
64
|
+
}
|
|
65
|
+
function extractConfigObjectLiteral(source) {
|
|
66
|
+
const configStart = /\bconst\s+config\s*=\s*\{/.exec(source);
|
|
67
|
+
if (!configStart) throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} must declare \`const config = { ... } satisfies FluidWidgetConfig\`.`);
|
|
68
|
+
const openBraceIndex = source.indexOf("{", configStart.index);
|
|
69
|
+
const closeBraceIndex = findMatchingBrace(source, openBraceIndex);
|
|
70
|
+
const suffix = stripComments(source.slice(closeBraceIndex + 1));
|
|
71
|
+
if (!/^\s*(?:as\s+const\s+)?satisfies\s+FluidWidgetConfig\s*;/.test(suffix)) throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} config object must use \`satisfies FluidWidgetConfig\` or \`as const satisfies FluidWidgetConfig\`.`);
|
|
72
|
+
return stripComments(source.slice(openBraceIndex + 1, closeBraceIndex));
|
|
73
|
+
}
|
|
74
|
+
function parseConfigObjectProperties(objectSource) {
|
|
75
|
+
const config = {};
|
|
76
|
+
for (const member of splitTopLevelMembers(objectSource)) {
|
|
77
|
+
const trimmedMember = member.trim();
|
|
78
|
+
if (!trimmedMember) continue;
|
|
79
|
+
const propertyMatch = /^(droplet)\s*:\s*([\s\S]+)$/.exec(trimmedMember);
|
|
80
|
+
if (!propertyMatch) {
|
|
81
|
+
const propertyName = /^([A-Za-z_$][\w$]*)\s*:/.exec(trimmedMember)?.[1];
|
|
82
|
+
throw new WidgetConfigParseError(propertyName ? `${WIDGET_CONFIG_FILE} does not support config field ${propertyName}. Supported field is droplet.` : `${WIDGET_CONFIG_FILE} config fields must use optional droplet: "..." string literal.`);
|
|
83
|
+
}
|
|
84
|
+
const propertyName = propertyMatch[1];
|
|
85
|
+
if (propertyName !== "droplet") throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} config field could not be parsed.`);
|
|
86
|
+
const value = parseStringLiteralValue(propertyMatch[2]?.trim() ?? "", propertyName);
|
|
87
|
+
if (!value.trim()) throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} field ${propertyName} must be a non-empty string literal.`);
|
|
88
|
+
if (config.droplet !== void 0) throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} field droplet must only be declared once.`);
|
|
89
|
+
config.droplet = value;
|
|
90
|
+
}
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
function parseStringLiteralValue(valueSource, propertyName) {
|
|
94
|
+
if (valueSource.startsWith("\"") && valueSource.endsWith("\"")) try {
|
|
95
|
+
const parsed = JSON.parse(valueSource);
|
|
96
|
+
if (typeof parsed === "string") return parsed;
|
|
97
|
+
} catch {}
|
|
98
|
+
if (valueSource.startsWith("'") && valueSource.endsWith("'")) return parseSingleQuotedStringLiteral(valueSource);
|
|
99
|
+
throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} field ${propertyName} must be a string literal (for example ${propertyName}: "value").`);
|
|
100
|
+
}
|
|
101
|
+
function parseSingleQuotedStringLiteral(valueSource) {
|
|
102
|
+
let value = "";
|
|
103
|
+
for (let index = 1; index < valueSource.length - 1; index += 1) {
|
|
104
|
+
const char = valueSource[index];
|
|
105
|
+
if (char !== "\\") {
|
|
106
|
+
value += char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
index += 1;
|
|
110
|
+
const escaped = valueSource[index];
|
|
111
|
+
switch (escaped) {
|
|
112
|
+
case "\\":
|
|
113
|
+
case "'":
|
|
114
|
+
case "\"":
|
|
115
|
+
value += escaped;
|
|
116
|
+
break;
|
|
117
|
+
case "n":
|
|
118
|
+
value += "\n";
|
|
119
|
+
break;
|
|
120
|
+
case "r":
|
|
121
|
+
value += "\r";
|
|
122
|
+
break;
|
|
123
|
+
case "t":
|
|
124
|
+
value += " ";
|
|
125
|
+
break;
|
|
126
|
+
case "b":
|
|
127
|
+
value += "\b";
|
|
128
|
+
break;
|
|
129
|
+
case "f":
|
|
130
|
+
value += "\f";
|
|
131
|
+
break;
|
|
132
|
+
case "v":
|
|
133
|
+
value += "\v";
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
value += escaped ?? "";
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function stripComments(source) {
|
|
143
|
+
let result = "";
|
|
144
|
+
let stringQuote;
|
|
145
|
+
let escaped = false;
|
|
146
|
+
let lineComment = false;
|
|
147
|
+
let blockComment = false;
|
|
148
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
149
|
+
const char = source[index];
|
|
150
|
+
const nextChar = source[index + 1];
|
|
151
|
+
if (lineComment) {
|
|
152
|
+
if (char === "\n" || char === "\r") {
|
|
153
|
+
lineComment = false;
|
|
154
|
+
result += char;
|
|
155
|
+
} else result += " ";
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (blockComment) {
|
|
159
|
+
if (char === "*" && nextChar === "/") {
|
|
160
|
+
blockComment = false;
|
|
161
|
+
result += " ";
|
|
162
|
+
index += 1;
|
|
163
|
+
} else if (char === "\n" || char === "\r") result += char;
|
|
164
|
+
else result += " ";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (stringQuote) {
|
|
168
|
+
result += char;
|
|
169
|
+
if (escaped) escaped = false;
|
|
170
|
+
else if (char === "\\") escaped = true;
|
|
171
|
+
else if (char === stringQuote) stringQuote = void 0;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (char === "/" && nextChar === "/") {
|
|
175
|
+
lineComment = true;
|
|
176
|
+
result += " ";
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (char === "/" && nextChar === "*") {
|
|
181
|
+
blockComment = true;
|
|
182
|
+
result += " ";
|
|
183
|
+
index += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char === "\"" || char === "'" || char === "`") stringQuote = char;
|
|
187
|
+
result += char;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function splitTopLevelMembers(objectSource) {
|
|
192
|
+
const members = [];
|
|
193
|
+
let memberStart = 0;
|
|
194
|
+
let stringQuote;
|
|
195
|
+
let escaped = false;
|
|
196
|
+
let nestedDepth = 0;
|
|
197
|
+
for (let index = 0; index < objectSource.length; index += 1) {
|
|
198
|
+
const char = objectSource[index];
|
|
199
|
+
if (stringQuote) {
|
|
200
|
+
if (escaped) escaped = false;
|
|
201
|
+
else if (char === "\\") escaped = true;
|
|
202
|
+
else if (char === stringQuote) stringQuote = void 0;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
206
|
+
stringQuote = char;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
210
|
+
nestedDepth += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
214
|
+
nestedDepth -= 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (char === "," && nestedDepth === 0) {
|
|
218
|
+
members.push(objectSource.slice(memberStart, index));
|
|
219
|
+
memberStart = index + 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
members.push(objectSource.slice(memberStart));
|
|
223
|
+
return members;
|
|
224
|
+
}
|
|
225
|
+
function findMatchingBrace(source, openBraceIndex) {
|
|
226
|
+
let depth = 0;
|
|
227
|
+
let stringQuote;
|
|
228
|
+
let escaped = false;
|
|
229
|
+
let lineComment = false;
|
|
230
|
+
let blockComment = false;
|
|
231
|
+
for (let index = openBraceIndex; index < source.length; index += 1) {
|
|
232
|
+
const char = source[index];
|
|
233
|
+
const nextChar = source[index + 1];
|
|
234
|
+
if (lineComment) {
|
|
235
|
+
if (char === "\n" || char === "\r") lineComment = false;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (blockComment) {
|
|
239
|
+
if (char === "*" && nextChar === "/") {
|
|
240
|
+
blockComment = false;
|
|
241
|
+
index += 1;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (stringQuote) {
|
|
246
|
+
if (escaped) escaped = false;
|
|
247
|
+
else if (char === "\\") escaped = true;
|
|
248
|
+
else if (char === stringQuote) stringQuote = void 0;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (char === "/" && nextChar === "/") {
|
|
252
|
+
lineComment = true;
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (char === "/" && nextChar === "*") {
|
|
257
|
+
blockComment = true;
|
|
258
|
+
index += 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
262
|
+
stringQuote = char;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === "{") depth += 1;
|
|
266
|
+
if (char === "}") {
|
|
267
|
+
depth -= 1;
|
|
268
|
+
if (depth === 0) return index;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw new WidgetConfigParseError(`${WIDGET_CONFIG_FILE} config object is missing a closing brace.`);
|
|
272
|
+
}
|
|
273
|
+
function normalizeOptionalString(value) {
|
|
274
|
+
const trimmed = value?.trim();
|
|
275
|
+
return trimmed ? trimmed : void 0;
|
|
276
|
+
}
|
|
277
|
+
function isNodeError$1(error) {
|
|
278
|
+
return error instanceof Error && "code" in error;
|
|
279
|
+
}
|
|
280
|
+
const MAX_DROPLET_UUID_LENGTH = 256;
|
|
281
|
+
const URL_SAFE_IDENTIFIER_PATTERN = new RegExp(`^[A-Za-z0-9][A-Za-z0-9_~-]*$`);
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/utils/validation.ts
|
|
284
|
+
function formatValidationErrors(errors) {
|
|
285
|
+
if (errors.length === 0) return "Validation failed.";
|
|
286
|
+
return ["Validation failed:", ...errors.map((error) => {
|
|
287
|
+
return ` - ${error.path ? `${error.path}: ` : ""}${error.message}`;
|
|
288
|
+
})].join("\n");
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/utils/source-package.ts
|
|
292
|
+
const DEFAULT_WIDGET_OUT_DIR = ".fluid/widget-dist";
|
|
293
|
+
const ROOT_WIDGET_CONFIG_FILE = WIDGET_CONFIG_FILE;
|
|
294
|
+
const SOURCE_CONFIG_CANDIDATES = [
|
|
295
|
+
"src/widgets.config.ts",
|
|
296
|
+
"src/portal.config.ts",
|
|
297
|
+
"portal.config.ts"
|
|
298
|
+
];
|
|
299
|
+
var WidgetValidationError = class extends Error {
|
|
300
|
+
errors;
|
|
301
|
+
constructor(errors) {
|
|
302
|
+
super(formatValidationErrors(errors));
|
|
303
|
+
this.name = "WidgetValidationError";
|
|
304
|
+
this.errors = errors;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
async function validateDropletWidgetProject(projectDir, packageKeyOverride) {
|
|
308
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
309
|
+
const sourceConfig = resolveWidgetSourceConfig(resolvedProjectDir);
|
|
310
|
+
if (!sourceConfig) throw new WidgetValidationError([{
|
|
311
|
+
code: "NO_SOURCE_PACKAGE",
|
|
312
|
+
path: ROOT_WIDGET_CONFIG_FILE,
|
|
313
|
+
message: `No widget package config found. Create ${ROOT_WIDGET_CONFIG_FILE} or src/widgets.config.ts that exports defineWidgetPackage(...).`
|
|
314
|
+
}]);
|
|
315
|
+
const configErrors = await validateRootWidgetConfig(resolvedProjectDir, packageKeyOverride);
|
|
316
|
+
if (configErrors.length > 0) throw new WidgetValidationError(configErrors);
|
|
317
|
+
const sourcePackages = await withRootWidgetConfigBridge(resolvedProjectDir, async () => {
|
|
318
|
+
const loadResult = await loadSourceWidgetPackages(resolvedProjectDir);
|
|
319
|
+
if (!loadResult.success) throw new WidgetValidationError([{
|
|
320
|
+
code: "INVALID_SOURCE_PACKAGE",
|
|
321
|
+
path: sourceConfig.relativePath,
|
|
322
|
+
message: appendDetails(loadResult.error.message, loadResult.error.details)
|
|
323
|
+
}]);
|
|
324
|
+
return loadResult.value;
|
|
325
|
+
}, packageKeyOverride);
|
|
326
|
+
const validation = validateSingleSourceWidgetPackage(sourcePackages, { owner: "droplet" });
|
|
327
|
+
const errors = [...validation.errors, ...validateDropletMetadata(sourcePackages)];
|
|
328
|
+
if (!validation.success || !validation.value || errors.length > 0) throw new WidgetValidationError(errors);
|
|
329
|
+
return {
|
|
330
|
+
projectDir: resolvedProjectDir,
|
|
331
|
+
sourceConfigPath: sourceConfig.relativePath,
|
|
332
|
+
validated: validation.value
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function withRootWidgetConfigBridge(projectDir, callback, packageKeyOverride) {
|
|
336
|
+
if (!existsSync(path.join(projectDir, ROOT_WIDGET_CONFIG_FILE)) || resolvePortalSourceConfig(projectDir)) return callback();
|
|
337
|
+
const packageKey = packageKeyOverride?.trim() || (await loadWidgetConfig(projectDir)).config.droplet;
|
|
338
|
+
if (!packageKey) throw new Error("fluid.widget.config.ts must set droplet before validating, building, or publishing.");
|
|
339
|
+
const srcDir = path.join(projectDir, "src");
|
|
340
|
+
const bridgePath = path.join(srcDir, "widgets.config.ts");
|
|
341
|
+
const srcDirExisted = existsSync(srcDir);
|
|
342
|
+
await mkdir(srcDir, { recursive: true });
|
|
343
|
+
try {
|
|
344
|
+
await writeFile(bridgePath, createRootWidgetConfigBridgeSource(packageKey), {
|
|
345
|
+
encoding: "utf-8",
|
|
346
|
+
flag: "wx"
|
|
347
|
+
});
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (isNodeError(err) && err.code === "EEXIST") throw new Error(`${path.relative(projectDir, bridgePath)} already exists. Remove this stale Fluid widget CLI bridge file and try again.`);
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
return await callback();
|
|
354
|
+
} finally {
|
|
355
|
+
await rm(bridgePath, { force: true }).catch(() => {});
|
|
356
|
+
if (!srcDirExisted) await rmdir(srcDir).catch(() => {});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function createRootWidgetConfigBridgeSource(packageKey) {
|
|
360
|
+
return `import { widgetPackage as sourceWidgetPackage } from "../manifest";
|
|
361
|
+
|
|
362
|
+
const packageKey = ${JSON.stringify(packageKey)};
|
|
363
|
+
const packageId = sourceWidgetPackage.scope + "." + packageKey;
|
|
364
|
+
|
|
365
|
+
export const widgetPackage = {
|
|
366
|
+
...sourceWidgetPackage,
|
|
367
|
+
packageStableId: packageKey,
|
|
368
|
+
packageId,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export const widgetPackages = [widgetPackage] as const;
|
|
372
|
+
export default widgetPackage;
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
function resolveWidgetSourceConfig(projectDir) {
|
|
376
|
+
const rootConfig = path.join(projectDir, ROOT_WIDGET_CONFIG_FILE);
|
|
377
|
+
if (existsSync(rootConfig)) return {
|
|
378
|
+
path: rootConfig,
|
|
379
|
+
relativePath: ROOT_WIDGET_CONFIG_FILE
|
|
380
|
+
};
|
|
381
|
+
return resolvePortalSourceConfig(projectDir);
|
|
382
|
+
}
|
|
383
|
+
function formatValidationSuccess(result) {
|
|
384
|
+
return [
|
|
385
|
+
"Widget package is valid.",
|
|
386
|
+
`Config: ${result.sourceConfigPath ?? "unknown"}`,
|
|
387
|
+
`Package: ${result.validated.packageId}`,
|
|
388
|
+
`Version: ${result.validated.version}`,
|
|
389
|
+
`Widgets: ${result.validated.widgets.length}`
|
|
390
|
+
].join("\n");
|
|
391
|
+
}
|
|
392
|
+
async function validateRootWidgetConfig(projectDir, packageKeyOverride) {
|
|
393
|
+
if (!existsSync(path.join(projectDir, ROOT_WIDGET_CONFIG_FILE))) return [];
|
|
394
|
+
try {
|
|
395
|
+
const loaded = await loadWidgetConfig(projectDir);
|
|
396
|
+
const packageKeyError = validateDropletUuid(packageKeyOverride ?? loaded.config.droplet ?? "");
|
|
397
|
+
if (!packageKeyError) return [];
|
|
398
|
+
return [{
|
|
399
|
+
code: "INVALID_PACKAGE_KEY",
|
|
400
|
+
path: ROOT_WIDGET_CONFIG_FILE,
|
|
401
|
+
message: packageKeyError
|
|
402
|
+
}];
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return [{
|
|
405
|
+
code: "INVALID_SOURCE_PACKAGE",
|
|
406
|
+
path: ROOT_WIDGET_CONFIG_FILE,
|
|
407
|
+
message: err instanceof Error ? err.message : String(err)
|
|
408
|
+
}];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function validateDropletMetadata(sourcePackages) {
|
|
412
|
+
if (sourcePackages.length !== 1) return [];
|
|
413
|
+
const sourcePackage = sourcePackages[0];
|
|
414
|
+
if (!isRecord$5(sourcePackage)) return [];
|
|
415
|
+
const errors = [];
|
|
416
|
+
if (sourcePackage["packageType"] !== "droplet") errors.push({
|
|
417
|
+
code: "UNSUPPORTED_OWNER",
|
|
418
|
+
path: "packageType",
|
|
419
|
+
message: "Droplet widget packages must set packageType: \"droplet\" in package metadata."
|
|
420
|
+
});
|
|
421
|
+
if (!isNonEmptyString(sourcePackage["packageStableId"])) errors.push({
|
|
422
|
+
code: "INVALID_PACKAGE_KEY",
|
|
423
|
+
path: "packageStableId",
|
|
424
|
+
message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
|
|
425
|
+
});
|
|
426
|
+
const widgets = sourcePackage["widgets"];
|
|
427
|
+
if (Array.isArray(widgets)) widgets.forEach((widget, index) => {
|
|
428
|
+
if (!isRecord$5(widget)) return;
|
|
429
|
+
validateJsonSerializable(widget["propertySchema"], `widgets[${index}].propertySchema`, errors);
|
|
430
|
+
validateJsonSerializable(widget["defaultProps"], `widgets[${index}].defaultProps`, errors);
|
|
431
|
+
});
|
|
432
|
+
return errors;
|
|
433
|
+
}
|
|
434
|
+
function resolvePortalSourceConfig(projectDir) {
|
|
435
|
+
for (const relativePath of SOURCE_CONFIG_CANDIDATES) {
|
|
436
|
+
const candidate = path.join(projectDir, relativePath);
|
|
437
|
+
if (existsSync(candidate)) return {
|
|
438
|
+
path: candidate,
|
|
439
|
+
relativePath
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function validateJsonSerializable(value, pathLabel, errors) {
|
|
444
|
+
if (value === void 0) return;
|
|
445
|
+
try {
|
|
446
|
+
assertJsonSerializable(value, pathLabel);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
errors.push({
|
|
449
|
+
code: "INVALID_WIDGET_METADATA",
|
|
450
|
+
path: pathLabel,
|
|
451
|
+
message: err instanceof Error ? err.message : String(err)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function assertJsonSerializable(value, pathLabel) {
|
|
456
|
+
if (value === null) return;
|
|
457
|
+
const valueType = typeof value;
|
|
458
|
+
if (valueType === "string" || valueType === "boolean") return;
|
|
459
|
+
if (valueType === "number") {
|
|
460
|
+
if (Number.isFinite(value)) return;
|
|
461
|
+
throw new Error(`${pathLabel} must not contain NaN or Infinity.`);
|
|
462
|
+
}
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
value.forEach((item, index) => {
|
|
465
|
+
assertJsonSerializable(item, `${pathLabel}[${index}]`);
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (isRecord$5(value)) {
|
|
470
|
+
for (const [key, item] of Object.entries(value)) {
|
|
471
|
+
if (item === void 0) throw new Error(`${pathLabel}.${key} must not be undefined.`);
|
|
472
|
+
assertJsonSerializable(item, `${pathLabel}.${key}`);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`${pathLabel} must contain only JSON-serializable values.`);
|
|
477
|
+
}
|
|
478
|
+
function appendDetails(message, details) {
|
|
479
|
+
return details ? `${message}\n${details}` : message;
|
|
480
|
+
}
|
|
481
|
+
function isNonEmptyString(value) {
|
|
482
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
483
|
+
}
|
|
484
|
+
function isRecord$5(value) {
|
|
485
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
486
|
+
}
|
|
487
|
+
function isNodeError(error) {
|
|
488
|
+
return error instanceof Error && "code" in error;
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/utils/command.ts
|
|
492
|
+
async function runCliAction(action) {
|
|
493
|
+
try {
|
|
494
|
+
await action();
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error(chalk.red("Error:") + " " + formatUnknownError(err));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function formatUnknownError(err) {
|
|
501
|
+
return err instanceof Error ? err.message : String(err);
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/utils/package-manager.ts
|
|
505
|
+
function getRunCommand(script, cwd = process.cwd()) {
|
|
506
|
+
return `${detectPackageManager(cwd)} run ${script}`;
|
|
507
|
+
}
|
|
508
|
+
function getInstallCommand(cwd = process.cwd()) {
|
|
509
|
+
return `${detectPackageManager(cwd)} install`;
|
|
510
|
+
}
|
|
511
|
+
async function installDependencies(cwd) {
|
|
512
|
+
await runPackageManager(detectPackageManager(cwd), ["install"], cwd);
|
|
513
|
+
}
|
|
514
|
+
function detectPackageManager(cwd) {
|
|
515
|
+
if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
516
|
+
if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
517
|
+
if (existsSync(path.join(cwd, "bun.lockb"))) return "bun";
|
|
518
|
+
if (existsSync(path.join(cwd, "package-lock.json"))) return "npm";
|
|
519
|
+
const invokingPackageManager = readInvokingPackageManager();
|
|
520
|
+
if (invokingPackageManager) return invokingPackageManager;
|
|
521
|
+
const declaredPackageManager = readDeclaredPackageManager(cwd);
|
|
522
|
+
if (declaredPackageManager && isCommandAvailable(declaredPackageManager)) return declaredPackageManager;
|
|
523
|
+
return findAvailablePackageManager() ?? "pnpm";
|
|
524
|
+
}
|
|
525
|
+
function runPackageManager(command, args, cwd) {
|
|
526
|
+
return new Promise((resolve, reject) => {
|
|
527
|
+
const child = spawn(command, [...args], {
|
|
528
|
+
cwd,
|
|
529
|
+
stdio: "inherit",
|
|
530
|
+
shell: process.platform === "win32"
|
|
531
|
+
});
|
|
532
|
+
child.once("error", (error) => {
|
|
533
|
+
reject(error);
|
|
534
|
+
});
|
|
535
|
+
child.once("exit", (code) => {
|
|
536
|
+
if (code === 0) {
|
|
537
|
+
resolve();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
function readInvokingPackageManager() {
|
|
545
|
+
const userAgent = process.env["npm_config_user_agent"];
|
|
546
|
+
if (!userAgent) return void 0;
|
|
547
|
+
const name = userAgent.split("/")[0];
|
|
548
|
+
if (isPackageManager(name)) return name;
|
|
549
|
+
}
|
|
550
|
+
function readDeclaredPackageManager(cwd) {
|
|
551
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
552
|
+
if (!existsSync(packageJsonPath)) return void 0;
|
|
553
|
+
try {
|
|
554
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
555
|
+
if (!isRecord$4(packageJson)) return void 0;
|
|
556
|
+
const packageManager = packageJson["packageManager"];
|
|
557
|
+
if (typeof packageManager !== "string") return void 0;
|
|
558
|
+
const name = packageManager.split("@")[0];
|
|
559
|
+
if (isPackageManager(name)) return name;
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
function findAvailablePackageManager() {
|
|
563
|
+
for (const packageManager of [
|
|
564
|
+
"pnpm",
|
|
565
|
+
"yarn",
|
|
566
|
+
"bun",
|
|
567
|
+
"npm"
|
|
568
|
+
]) if (isCommandAvailable(packageManager)) return packageManager;
|
|
569
|
+
}
|
|
570
|
+
function isCommandAvailable(command) {
|
|
571
|
+
return spawnSync(command, ["--version"], {
|
|
572
|
+
shell: process.platform === "win32",
|
|
573
|
+
stdio: "ignore"
|
|
574
|
+
}).status === 0;
|
|
575
|
+
}
|
|
576
|
+
function isPackageManager(value) {
|
|
577
|
+
return value === "pnpm" || value === "yarn" || value === "bun" || value === "npm";
|
|
578
|
+
}
|
|
579
|
+
function isRecord$4(value) {
|
|
580
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/utils/package-scripts.ts
|
|
584
|
+
async function runPackageScriptIfAvailable(options) {
|
|
585
|
+
const packageJsonPath = path.join(options.projectDir, "package.json");
|
|
586
|
+
if (!existsSync(packageJsonPath)) return { ran: false };
|
|
587
|
+
if (!hasPackageScript(await readPackageJson$1(packageJsonPath), options.scriptName)) return { ran: false };
|
|
588
|
+
const packageManager = detectPackageManager(options.projectDir);
|
|
589
|
+
const args = ["run", options.scriptName];
|
|
590
|
+
await runCommand(packageManager, args, options.projectDir);
|
|
591
|
+
return {
|
|
592
|
+
ran: true,
|
|
593
|
+
command: `${packageManager} ${args.join(" ")}`
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function runCommand(command, args, cwd) {
|
|
597
|
+
return new Promise((resolve, reject) => {
|
|
598
|
+
const child = spawn(command, args, {
|
|
599
|
+
cwd,
|
|
600
|
+
stdio: "inherit",
|
|
601
|
+
env: process.env,
|
|
602
|
+
shell: process.platform === "win32"
|
|
603
|
+
});
|
|
604
|
+
child.on("error", reject);
|
|
605
|
+
child.on("exit", (code, signal) => {
|
|
606
|
+
if (code === 0) {
|
|
607
|
+
resolve();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
reject(/* @__PURE__ */ new Error(signal ? `${command} ${args.join(" ")} exited with signal ${signal}.` : `${command} ${args.join(" ")} exited with code ${code ?? "unknown"}.`));
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
async function readPackageJson$1(packageJsonPath) {
|
|
615
|
+
const source = await readFile(packageJsonPath, "utf-8");
|
|
616
|
+
try {
|
|
617
|
+
return JSON.parse(source);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
620
|
+
throw new Error(`Unable to parse package.json: ${message}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function hasPackageScript(value, scriptName) {
|
|
624
|
+
if (!isRecord$3(value)) return false;
|
|
625
|
+
const scripts = value["scripts"];
|
|
626
|
+
if (!isRecord$3(scripts)) return false;
|
|
627
|
+
return typeof scripts[scriptName] === "string";
|
|
628
|
+
}
|
|
629
|
+
function isRecord$3(value) {
|
|
630
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/commands/build.ts
|
|
634
|
+
async function buildWidgetPackage(options) {
|
|
635
|
+
await validateDropletWidgetProject(options.projectDir);
|
|
636
|
+
if (options.skipTypecheck !== true) await runPackageScriptIfAvailable({
|
|
637
|
+
projectDir: options.projectDir,
|
|
638
|
+
scriptName: "typecheck"
|
|
639
|
+
});
|
|
640
|
+
const outDir = validateWidgetPublishOutDir(options.projectDir, options.outDir);
|
|
641
|
+
const buildResult = await withRootWidgetConfigBridge(options.projectDir, () => buildSharedWidgetPackage({
|
|
642
|
+
projectDir: options.projectDir,
|
|
643
|
+
publishDir: outDir,
|
|
644
|
+
owner: "droplet"
|
|
645
|
+
}));
|
|
646
|
+
if (!buildResult.success) {
|
|
647
|
+
const details = buildResult.error.details ? `\n${buildResult.error.details}` : "";
|
|
648
|
+
throw new Error(`${buildResult.error.message}${details}`);
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
outDir: buildResult.value.publishDir,
|
|
652
|
+
packageId: buildResult.value.packageId,
|
|
653
|
+
version: buildResult.value.version
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const buildCommand = new Command("build").description("Validate and build the current droplet widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_WIDGET_OUT_DIR).option("--skip-typecheck", "Skip package typecheck script when present").action((options) => {
|
|
657
|
+
return runCliAction(async () => {
|
|
658
|
+
const outDir = options.outDir ?? ".fluid/widget-dist";
|
|
659
|
+
console.log(chalk.blue.bold("Building Fluid widget package"));
|
|
660
|
+
console.log();
|
|
661
|
+
const spinner = ora("Validating and building widget artifacts...").start();
|
|
662
|
+
const result = await buildWidgetPackage({
|
|
663
|
+
projectDir: process.cwd(),
|
|
664
|
+
outDir,
|
|
665
|
+
skipTypecheck: options.skipTypecheck
|
|
666
|
+
});
|
|
667
|
+
spinner.succeed("Built widget runtime artifacts");
|
|
668
|
+
console.log();
|
|
669
|
+
console.log(chalk.gray("Package: ") + chalk.white(result.packageId));
|
|
670
|
+
console.log(chalk.gray("Version: ") + chalk.white(result.version));
|
|
671
|
+
console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/utils/template.ts
|
|
676
|
+
function getDefaultTemplatePath() {
|
|
677
|
+
return path.join(findPackageRoot(), "templates", "default");
|
|
678
|
+
}
|
|
679
|
+
async function directoryExists(filePath) {
|
|
680
|
+
try {
|
|
681
|
+
return (await stat(filePath)).isDirectory();
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function copyWidgetTemplate(templatePath, targetPath, variables) {
|
|
687
|
+
const files = await getFiles(templatePath);
|
|
688
|
+
for (const file of files) {
|
|
689
|
+
const sourcePath = path.join(templatePath, file);
|
|
690
|
+
const outputFile = getOutputFilename(file);
|
|
691
|
+
const destinationPath = path.join(targetPath, outputFile);
|
|
692
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
693
|
+
await writeFile(destinationPath, renderTemplate(await readFile(sourcePath, "utf-8"), variables), "utf-8");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function getPortalSdkVersion(options = {}) {
|
|
697
|
+
return getWorkspacePackageVersion(["portal", "sdk"], "^0.1.0", options);
|
|
698
|
+
}
|
|
699
|
+
async function getPortalCoreVersion(options = {}) {
|
|
700
|
+
return getWorkspacePackageVersion(["portal", "core"], "^0.1.0", options);
|
|
701
|
+
}
|
|
702
|
+
async function getCliVersion(options = {}) {
|
|
703
|
+
return getWorkspacePackageVersion(["cli", "core"], "^0.1.0", options);
|
|
704
|
+
}
|
|
705
|
+
async function getWidgetCliVersion(options = {}) {
|
|
706
|
+
return getWorkspacePackageVersion(["cli", "widget"], "^0.1.0", options);
|
|
707
|
+
}
|
|
708
|
+
async function getWorkspacePackageVersion(packagePathSegments, fallback, options) {
|
|
709
|
+
try {
|
|
710
|
+
const packageRoot = findPackageRoot();
|
|
711
|
+
const packagesRoot = path.join(packageRoot, "..", "..");
|
|
712
|
+
const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
|
|
713
|
+
if (options.local) return `file:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot) || "."}`;
|
|
714
|
+
const content = await readFile(path.join(targetPackageRoot, "package.json"), "utf-8");
|
|
715
|
+
return `^${JSON.parse(content).version ?? fallback.replace(/^\^/, "")}`;
|
|
716
|
+
} catch {
|
|
717
|
+
return fallback;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function getFiles(dir, baseDir = dir) {
|
|
721
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
722
|
+
const files = [];
|
|
723
|
+
for (const entry of entries) {
|
|
724
|
+
const fullPath = path.join(dir, entry.name);
|
|
725
|
+
if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
|
|
726
|
+
else files.push(path.relative(baseDir, fullPath));
|
|
727
|
+
}
|
|
728
|
+
return files;
|
|
729
|
+
}
|
|
730
|
+
function renderTemplate(content, variables) {
|
|
731
|
+
return content.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
|
|
732
|
+
if (isTemplateVariableKey(key)) return variables[key] ?? "";
|
|
733
|
+
return match;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function isTemplateVariableKey(key) {
|
|
737
|
+
return [
|
|
738
|
+
"projectName",
|
|
739
|
+
"widgetScope",
|
|
740
|
+
"widgetPackageVersion",
|
|
741
|
+
"sdkVersion",
|
|
742
|
+
"coreVersion",
|
|
743
|
+
"fluidCliVersion",
|
|
744
|
+
"widgetCliVersion",
|
|
745
|
+
"droplet"
|
|
746
|
+
].includes(key);
|
|
747
|
+
}
|
|
748
|
+
function getOutputFilename(filename) {
|
|
749
|
+
return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
|
|
750
|
+
}
|
|
751
|
+
function findPackageRoot() {
|
|
752
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
753
|
+
while (!existsSync(path.join(dir, "package.json"))) {
|
|
754
|
+
const parent = path.dirname(dir);
|
|
755
|
+
if (parent === dir) throw new Error("Could not find widget CLI package root");
|
|
756
|
+
dir = parent;
|
|
757
|
+
}
|
|
758
|
+
return dir;
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/commands/create.ts
|
|
762
|
+
const DEFAULT_WIDGET_PACKAGE_VERSION = "0.1.0";
|
|
763
|
+
const DEFAULT_WIDGET_SCOPE = "droplet";
|
|
764
|
+
const createCommand = new Command("create").description("Create a standalone Fluid widget project").argument("<project-name>", "Name of the widget project to create").option("--skip-install", "Skip dependency installation").option("-o, --output-dir <dir>", "Directory to create the project in (defaults to cwd)").option("--scope <scope>", "Widget package scope/namespace", DEFAULT_WIDGET_SCOPE).option("--package-version <version>", "Initial widget package version", DEFAULT_WIDGET_PACKAGE_VERSION).option("--local", "Use local monorepo packages via file: links (for development testing)").option("--droplet <uuid>", "Existing droplet UUID that owns the package").action(async (projectName, options) => {
|
|
765
|
+
try {
|
|
766
|
+
await createWidgetProject(projectName, options);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
console.log();
|
|
769
|
+
console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
770
|
+
console.log();
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
async function createWidgetProject(projectName, options) {
|
|
775
|
+
console.log();
|
|
776
|
+
console.log(chalk.bold("Creating a new Fluid widget project"));
|
|
777
|
+
console.log();
|
|
778
|
+
const projectNameError = validateProjectName(projectName);
|
|
779
|
+
if (projectNameError) throw new Error(projectNameError);
|
|
780
|
+
const droplet = options.droplet?.trim();
|
|
781
|
+
if (droplet) {
|
|
782
|
+
const dropletError = validateDropletUuid(droplet);
|
|
783
|
+
if (dropletError) throw new Error(dropletError);
|
|
784
|
+
}
|
|
785
|
+
const outputDir = path.resolve(options.outputDir ?? process.cwd());
|
|
786
|
+
const targetPath = path.join(outputDir, projectName);
|
|
787
|
+
if (await directoryExists(targetPath)) throw new Error(`Directory already exists: ${targetPath}`);
|
|
788
|
+
const templatePath = getDefaultTemplatePath();
|
|
789
|
+
if (!await directoryExists(templatePath)) throw new Error("Default widget template not found.");
|
|
790
|
+
const config = mergeWidgetConfig({}, { droplet });
|
|
791
|
+
const widgetScope = options.scope ?? DEFAULT_WIDGET_SCOPE;
|
|
792
|
+
const localOptions = {
|
|
793
|
+
local: options.local === true,
|
|
794
|
+
targetPath
|
|
795
|
+
};
|
|
796
|
+
const variables = {
|
|
797
|
+
projectName,
|
|
798
|
+
droplet: config.droplet,
|
|
799
|
+
widgetScope,
|
|
800
|
+
widgetPackageVersion: options.packageVersion ?? DEFAULT_WIDGET_PACKAGE_VERSION,
|
|
801
|
+
sdkVersion: await getPortalSdkVersion(localOptions),
|
|
802
|
+
coreVersion: await getPortalCoreVersion(localOptions),
|
|
803
|
+
fluidCliVersion: await getCliVersion(localOptions),
|
|
804
|
+
widgetCliVersion: await getWidgetCliVersion(localOptions)
|
|
805
|
+
};
|
|
806
|
+
const spinner = ora("Copying widget template...").start();
|
|
807
|
+
try {
|
|
808
|
+
await copyWidgetTemplate(templatePath, targetPath, variables);
|
|
809
|
+
await writeWidgetConfig(targetPath, config);
|
|
810
|
+
spinner.succeed("Copied widget template");
|
|
811
|
+
} catch (err) {
|
|
812
|
+
spinner.fail("Failed to create widget project");
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
const installCommand = getInstallCommand(targetPath);
|
|
816
|
+
if (options.skipInstall !== true) {
|
|
817
|
+
spinner.start(`Installing dependencies with ${installCommand}...`);
|
|
818
|
+
try {
|
|
819
|
+
await installDependencies(targetPath);
|
|
820
|
+
spinner.succeed("Installed dependencies");
|
|
821
|
+
} catch {
|
|
822
|
+
spinner.fail("Failed to install dependencies");
|
|
823
|
+
console.log();
|
|
824
|
+
console.log(chalk.yellow("You can install dependencies manually:"));
|
|
825
|
+
console.log(chalk.cyan(` cd ${formatCdPath(targetPath)}`));
|
|
826
|
+
console.log(chalk.cyan(` ${installCommand}`));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
console.log();
|
|
830
|
+
console.log(chalk.green.bold("Success!") + ` Created ${chalk.cyan(projectName)}`);
|
|
831
|
+
console.log();
|
|
832
|
+
console.log("Next steps:");
|
|
833
|
+
console.log(chalk.cyan(` cd ${formatCdPath(targetPath)}`));
|
|
834
|
+
if (options.skipInstall === true) console.log(chalk.cyan(` ${installCommand}`));
|
|
835
|
+
if (!config.droplet) console.log(chalk.cyan(" fluid widget link"));
|
|
836
|
+
console.log(chalk.cyan(` ${getRunCommand("dev", targetPath)}`));
|
|
837
|
+
console.log();
|
|
838
|
+
console.log("Edit " + chalk.cyan("manifest.ts") + " to customize your widget package.");
|
|
839
|
+
console.log(chalk.dim("This command only creates local project files; it does not create a droplet."));
|
|
840
|
+
console.log();
|
|
841
|
+
}
|
|
842
|
+
function validateProjectName(projectName) {
|
|
843
|
+
if (!projectName) return "Project name is required.";
|
|
844
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(projectName)) return "Project name must contain only lowercase letters, numbers, and hyphens.";
|
|
845
|
+
}
|
|
846
|
+
function formatCdPath(targetPath) {
|
|
847
|
+
return path.relative(process.cwd(), targetPath) || ".";
|
|
848
|
+
}
|
|
849
|
+
//#endregion
|
|
850
|
+
//#region src/commands/dev.ts
|
|
851
|
+
const DEFAULT_WIDGET_DEV_PORT = 5174;
|
|
852
|
+
const WIDGET_DEV_ROUTES = [
|
|
853
|
+
"/",
|
|
854
|
+
"/builder-preview",
|
|
855
|
+
"/__manifests__",
|
|
856
|
+
"/__preview__",
|
|
857
|
+
"/__runtime-entry__"
|
|
858
|
+
];
|
|
859
|
+
const devCommand = new Command("dev").description("Start the local Fluid widget preview development server").option("-p, --port <port>", "Port to run the widget dev server on", String(DEFAULT_WIDGET_DEV_PORT)).option("--host [host]", "Expose the dev server to the network").option("--mode <mode>", "Vite mode to run", "development").action((options) => {
|
|
860
|
+
return runCliAction(async () => {
|
|
861
|
+
const projectDir = process.cwd();
|
|
862
|
+
printWidgetDevSummary(await resolveWidgetDevProject(projectDir, options));
|
|
863
|
+
await runViteDevServer(projectDir, options);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
async function resolveWidgetDevProject(projectDir, options = {}) {
|
|
867
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
868
|
+
const packageJsonPath = path.join(resolvedProjectDir, "package.json");
|
|
869
|
+
if (!existsSync(packageJsonPath)) throw new Error("No package.json found. Run this command from a Fluid widget project directory.");
|
|
870
|
+
const packageName = readPackageName(await readPackageJson(packageJsonPath)) ?? path.basename(projectDir);
|
|
871
|
+
const viteConfigPath = path.join(resolvedProjectDir, "vite.config.ts");
|
|
872
|
+
if (!existsSync(viteConfigPath)) throw new Error("No vite.config.ts found. Run this command from a Fluid widget project directory.");
|
|
873
|
+
const port = parsePort(options.port ?? String(5174));
|
|
874
|
+
const host = normalizeHostOption(options.host);
|
|
875
|
+
const mode = normalizeMode(options.mode);
|
|
876
|
+
return {
|
|
877
|
+
projectDir: resolvedProjectDir,
|
|
878
|
+
packageName,
|
|
879
|
+
viteConfigPath,
|
|
880
|
+
localUrl: formatLocalUrl(port, host),
|
|
881
|
+
target: packageName,
|
|
882
|
+
mode,
|
|
883
|
+
routes: WIDGET_DEV_ROUTES
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function buildViteDevArgs(options = {}) {
|
|
887
|
+
const port = parsePort(options.port ?? String(5174));
|
|
888
|
+
const host = normalizeHostOption(options.host);
|
|
889
|
+
const mode = normalizeMode(options.mode);
|
|
890
|
+
const args = [
|
|
891
|
+
"--port",
|
|
892
|
+
String(port),
|
|
893
|
+
"--mode",
|
|
894
|
+
mode
|
|
895
|
+
];
|
|
896
|
+
if (host) args.push("--host", host);
|
|
897
|
+
return args;
|
|
898
|
+
}
|
|
899
|
+
function printWidgetDevSummary(summary) {
|
|
900
|
+
console.log();
|
|
901
|
+
console.log(chalk.blue.bold("Fluid Widget Dev"));
|
|
902
|
+
console.log(chalk.gray("Local: ") + chalk.cyan(summary.localUrl));
|
|
903
|
+
console.log(chalk.gray("Target: ") + chalk.white(summary.target));
|
|
904
|
+
console.log(chalk.gray("Mode: ") + chalk.white(summary.mode));
|
|
905
|
+
console.log(chalk.gray("Routes:"));
|
|
906
|
+
for (const route of summary.routes) console.log(chalk.gray(" - ") + chalk.cyan(route));
|
|
907
|
+
console.log();
|
|
908
|
+
}
|
|
909
|
+
async function runViteDevServer(projectDir, options = {}) {
|
|
910
|
+
const viteCliPath = resolveViteCliPath(projectDir);
|
|
911
|
+
if (!viteCliPath) throw new Error("Unable to find Vite in node_modules. Run your package manager install command before starting widget dev.");
|
|
912
|
+
await runNodeScript(viteCliPath, buildViteDevArgs(options), projectDir);
|
|
913
|
+
}
|
|
914
|
+
function resolveViteCliPath(projectDir) {
|
|
915
|
+
const viteCliPath = path.join(projectDir, "node_modules", "vite", "bin", "vite.js");
|
|
916
|
+
return existsSync(viteCliPath) ? viteCliPath : void 0;
|
|
917
|
+
}
|
|
918
|
+
async function readPackageJson(packageJsonPath) {
|
|
919
|
+
try {
|
|
920
|
+
return JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
921
|
+
} catch (err) {
|
|
922
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
923
|
+
throw new Error(`Unable to read package.json: ${message}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function readPackageName(packageJson) {
|
|
927
|
+
if (!isRecord$2(packageJson)) return void 0;
|
|
928
|
+
const name = packageJson["name"];
|
|
929
|
+
if (typeof name !== "string") return void 0;
|
|
930
|
+
const trimmed = name.trim();
|
|
931
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
932
|
+
}
|
|
933
|
+
function parsePort(value) {
|
|
934
|
+
const port = Number(value);
|
|
935
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Port must be an integer between 1 and 65535.");
|
|
936
|
+
return port;
|
|
937
|
+
}
|
|
938
|
+
function normalizeHostOption(host) {
|
|
939
|
+
if (host === void 0 || host === false) return void 0;
|
|
940
|
+
if (host === true) return "0.0.0.0";
|
|
941
|
+
const trimmed = host.trim();
|
|
942
|
+
return trimmed.length > 0 ? trimmed : "0.0.0.0";
|
|
943
|
+
}
|
|
944
|
+
function normalizeMode(mode) {
|
|
945
|
+
const trimmed = mode?.trim();
|
|
946
|
+
return trimmed && trimmed.length > 0 ? trimmed : "development";
|
|
947
|
+
}
|
|
948
|
+
function formatLocalUrl(port, host) {
|
|
949
|
+
return `http://${host && host !== "0.0.0.0" ? host : "localhost"}:${port}`;
|
|
950
|
+
}
|
|
951
|
+
function runNodeScript(scriptPath, args, cwd) {
|
|
952
|
+
return new Promise((resolve, reject) => {
|
|
953
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
954
|
+
cwd,
|
|
955
|
+
stdio: "inherit",
|
|
956
|
+
shell: process.platform === "win32"
|
|
957
|
+
});
|
|
958
|
+
child.on("error", reject);
|
|
959
|
+
child.on("exit", (code, signal) => {
|
|
960
|
+
if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") {
|
|
961
|
+
resolve();
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
reject(/* @__PURE__ */ new Error(signal ? `Vite dev server exited with signal ${signal}.` : `Vite dev server exited with code ${code ?? "unknown"}.`));
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function isRecord$2(value) {
|
|
969
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/utils/auth-client.ts
|
|
973
|
+
function createAuthenticatedClient() {
|
|
974
|
+
return { client: createAuthenticatedWidgetPackageClient() };
|
|
975
|
+
}
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/utils/droplets.ts
|
|
978
|
+
async function promptForDroplet(client) {
|
|
979
|
+
const droplets = await listDroplets(client);
|
|
980
|
+
if (droplets.length === 0) throw new Error("No droplets were found for this account. Create a droplet in Fluid first, then run this command again.");
|
|
981
|
+
return (await prompts({
|
|
982
|
+
type: "autocomplete",
|
|
983
|
+
name: "droplet",
|
|
984
|
+
message: "Select the droplet that owns this widget package",
|
|
985
|
+
choices: droplets.map((droplet) => ({
|
|
986
|
+
title: `${droplet.name} (${droplet.uuid})`,
|
|
987
|
+
value: droplet.uuid
|
|
988
|
+
})),
|
|
989
|
+
suggest: (input, choices) => Promise.resolve(input ? choices.filter((choice) => choice.title.toLowerCase().includes(input.toLowerCase())) : choices)
|
|
990
|
+
}))["droplet"];
|
|
991
|
+
}
|
|
992
|
+
async function listDroplets(client) {
|
|
993
|
+
try {
|
|
994
|
+
return normalizeDropletsResponse(await client.get("/api/droplets"));
|
|
995
|
+
} catch (err) {
|
|
996
|
+
if (isApiLikeError(err)) {
|
|
997
|
+
if (err.status === 401 || err.status === 403) throw new Error("Your Fluid session has expired. Run `fluid login` and try again.");
|
|
998
|
+
throw new Error(`Could not fetch droplets from Fluid (HTTP ${err.status}). ${err.message}`);
|
|
999
|
+
}
|
|
1000
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1001
|
+
throw new Error(`Could not fetch droplets from Fluid. ${message}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function normalizeDropletsResponse(response) {
|
|
1005
|
+
return extractDropletArray(response).map(normalizeDroplet).filter((droplet) => droplet !== void 0);
|
|
1006
|
+
}
|
|
1007
|
+
function extractDropletArray(response) {
|
|
1008
|
+
if (Array.isArray(response)) return response;
|
|
1009
|
+
if (!isRecord$1(response)) return [];
|
|
1010
|
+
const directDroplets = response["droplets"];
|
|
1011
|
+
if (Array.isArray(directDroplets)) return directDroplets;
|
|
1012
|
+
const data = response["data"];
|
|
1013
|
+
if (Array.isArray(data)) return data;
|
|
1014
|
+
if (isRecord$1(data) && Array.isArray(data["droplets"])) return data["droplets"];
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
function normalizeDroplet(value) {
|
|
1018
|
+
if (!isRecord$1(value)) return void 0;
|
|
1019
|
+
const uuid = readString(value, "uuid") ?? readString(value, "id");
|
|
1020
|
+
if (!uuid) return void 0;
|
|
1021
|
+
return {
|
|
1022
|
+
uuid,
|
|
1023
|
+
name: readString(value, "name") ?? readString(value, "title") ?? readString(value, "slug") ?? uuid
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function readString(record, key) {
|
|
1027
|
+
const value = record[key];
|
|
1028
|
+
if (typeof value !== "string") return void 0;
|
|
1029
|
+
const trimmed = value.trim();
|
|
1030
|
+
return trimmed ? trimmed : void 0;
|
|
1031
|
+
}
|
|
1032
|
+
function isApiLikeError(error) {
|
|
1033
|
+
return error instanceof Error && "status" in error && typeof error.status === "number";
|
|
1034
|
+
}
|
|
1035
|
+
function isRecord$1(value) {
|
|
1036
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/commands/link.ts
|
|
1040
|
+
const linkCommand = new Command("link").description("Link this widget project to an existing Fluid droplet").option("--droplet <uuid>", "Droplet UUID that owns the widget package").action(async (options) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const projectDir = process.cwd();
|
|
1043
|
+
const loaded = await loadWidgetConfig(projectDir);
|
|
1044
|
+
let droplet = options.droplet?.trim() || void 0;
|
|
1045
|
+
if (!droplet) {
|
|
1046
|
+
const spinner = ora("Fetching droplets...").start();
|
|
1047
|
+
try {
|
|
1048
|
+
const { client } = createAuthenticatedClient();
|
|
1049
|
+
spinner.stop();
|
|
1050
|
+
droplet = await promptForDroplet(client);
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
spinner.fail("Unable to load droplets");
|
|
1053
|
+
throw err;
|
|
1054
|
+
}
|
|
1055
|
+
if (!droplet) {
|
|
1056
|
+
console.log(chalk.dim("Cancelled."));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const nextConfig = mergeWidgetConfig(loaded.config, { droplet });
|
|
1061
|
+
const nextDropletError = validateDropletUuid(nextConfig.droplet ?? "");
|
|
1062
|
+
if (nextDropletError) throw new Error(nextDropletError);
|
|
1063
|
+
await writeWidgetConfig(projectDir, nextConfig);
|
|
1064
|
+
console.log();
|
|
1065
|
+
console.log(chalk.green("Linked widget project."));
|
|
1066
|
+
console.log(chalk.gray("Config: ") + chalk.cyan(path.relative(projectDir, loaded.path) || "fluid.widget.config.ts"));
|
|
1067
|
+
console.log(chalk.gray("Droplet: ") + chalk.white(nextConfig.droplet));
|
|
1068
|
+
console.log(chalk.gray("Package key: ") + chalk.white(`${nextConfig.droplet} (derived from droplet)`));
|
|
1069
|
+
console.log();
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
console.log();
|
|
1072
|
+
console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
1073
|
+
console.log();
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/utils/droplet-api.ts
|
|
1079
|
+
async function fetchDropletWidgetStatus(options) {
|
|
1080
|
+
return extractArray(await options.client.get(`/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions`)).map(normalizeVersionSummary);
|
|
1081
|
+
}
|
|
1082
|
+
async function fetchDropletWidgetLogs(options) {
|
|
1083
|
+
return extractArray(await options.client.get(`/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions/logs`, options.limit ? { limit: options.limit } : void 0)).map(normalizeLogEntry);
|
|
1084
|
+
}
|
|
1085
|
+
function formatStatusRows(versions) {
|
|
1086
|
+
if (versions.length === 0) return "No widget package versions found.";
|
|
1087
|
+
return versions.map((version) => {
|
|
1088
|
+
const identity = version.packageId ?? version.packageKey ?? "unknown-package";
|
|
1089
|
+
const release = version.version ?? "unknown-version";
|
|
1090
|
+
const status = version.status ?? "unknown";
|
|
1091
|
+
const updated = version.updatedAt ?? version.createdAt ?? "unknown time";
|
|
1092
|
+
return `${identity}@${release} — ${status} — ${version.artifactCount === void 0 ? "unknown artifacts" : `${version.artifactCount} artifact${version.artifactCount === 1 ? "" : "s"}`} — ${updated}`;
|
|
1093
|
+
}).join("\n");
|
|
1094
|
+
}
|
|
1095
|
+
function formatLogRows(logs) {
|
|
1096
|
+
if (logs.length === 0) return "No widget package logs found.";
|
|
1097
|
+
return logs.map((entry) => {
|
|
1098
|
+
return `[${entry.timestamp ?? "unknown time"}] ${entry.level ? entry.level.toUpperCase() : "INFO"} ${entry.message}`;
|
|
1099
|
+
}).join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
function normalizeVersionSummary(value) {
|
|
1102
|
+
const record = unwrapKnownNestedRecord(value);
|
|
1103
|
+
const artifacts = readArray(record, "artifacts");
|
|
1104
|
+
return {
|
|
1105
|
+
packageKey: readFirstString(record, ["package_key", "packageKey"]),
|
|
1106
|
+
packageId: readFirstString(record, ["package_id", "packageId"]),
|
|
1107
|
+
version: readFirstString(record, ["version", "name"]),
|
|
1108
|
+
status: readFirstString(record, ["status", "state"]),
|
|
1109
|
+
createdAt: readFirstString(record, ["created_at", "createdAt"]),
|
|
1110
|
+
updatedAt: readFirstString(record, ["updated_at", "updatedAt"]),
|
|
1111
|
+
...artifacts ? { artifactCount: artifacts.length } : {},
|
|
1112
|
+
raw: value
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeLogEntry(value) {
|
|
1116
|
+
const record = unwrapKnownNestedRecord(value);
|
|
1117
|
+
return {
|
|
1118
|
+
timestamp: readFirstString(record, [
|
|
1119
|
+
"timestamp",
|
|
1120
|
+
"created_at",
|
|
1121
|
+
"createdAt"
|
|
1122
|
+
]),
|
|
1123
|
+
level: readFirstString(record, ["level", "severity"]),
|
|
1124
|
+
message: readFirstString(record, [
|
|
1125
|
+
"message",
|
|
1126
|
+
"event",
|
|
1127
|
+
"status",
|
|
1128
|
+
"state"
|
|
1129
|
+
]) ?? JSON.stringify(value),
|
|
1130
|
+
raw: value
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function extractArray(value) {
|
|
1134
|
+
if (Array.isArray(value)) return value;
|
|
1135
|
+
if (!isRecord(value)) return [];
|
|
1136
|
+
for (const key of [
|
|
1137
|
+
"widget_package_versions",
|
|
1138
|
+
"widgetPackageVersions",
|
|
1139
|
+
"versions",
|
|
1140
|
+
"logs",
|
|
1141
|
+
"events",
|
|
1142
|
+
"data"
|
|
1143
|
+
]) {
|
|
1144
|
+
const nested = value[key];
|
|
1145
|
+
if (Array.isArray(nested)) return nested;
|
|
1146
|
+
}
|
|
1147
|
+
return [value];
|
|
1148
|
+
}
|
|
1149
|
+
function unwrapKnownNestedRecord(value) {
|
|
1150
|
+
if (!isRecord(value)) return {};
|
|
1151
|
+
for (const key of [
|
|
1152
|
+
"widget_package_version",
|
|
1153
|
+
"widgetPackageVersion",
|
|
1154
|
+
"version",
|
|
1155
|
+
"log",
|
|
1156
|
+
"event"
|
|
1157
|
+
]) {
|
|
1158
|
+
const nested = value[key];
|
|
1159
|
+
if (isRecord(nested)) return nested;
|
|
1160
|
+
}
|
|
1161
|
+
return value;
|
|
1162
|
+
}
|
|
1163
|
+
function readArray(record, key) {
|
|
1164
|
+
const value = record[key];
|
|
1165
|
+
return Array.isArray(value) ? value : void 0;
|
|
1166
|
+
}
|
|
1167
|
+
function readFirstString(record, keys) {
|
|
1168
|
+
for (const key of keys) {
|
|
1169
|
+
const value = record[key];
|
|
1170
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
1171
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function isRecord(value) {
|
|
1175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1176
|
+
}
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/commands/logs.ts
|
|
1179
|
+
const logsCommand = new Command("logs").description("Show recent widget package publish logs for a Droplet").option("--droplet <uuid>", "Droplet UUID that owns the widget package").option("--limit <number>", "Maximum number of log entries to request", "50").option("--json", "Print raw log response as JSON").action((options) => {
|
|
1180
|
+
return runCliAction(async () => {
|
|
1181
|
+
const droplet = await resolveDroplet$1(process.cwd(), options.droplet);
|
|
1182
|
+
const limit = parseLimit(options.limit);
|
|
1183
|
+
const logs = await fetchDropletWidgetLogs({
|
|
1184
|
+
client: createAuthenticatedWidgetPackageClient(),
|
|
1185
|
+
droplet,
|
|
1186
|
+
limit
|
|
1187
|
+
});
|
|
1188
|
+
if (options.json === true) {
|
|
1189
|
+
console.log(JSON.stringify(logs.map((entry) => entry.raw), null, 2));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
console.log(chalk.blue.bold(`Widget logs for droplet ${droplet}`));
|
|
1193
|
+
console.log();
|
|
1194
|
+
console.log(formatLogRows(logs));
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
async function resolveDroplet$1(projectDir, override) {
|
|
1198
|
+
const droplet = override?.trim() || (await loadWidgetConfig(projectDir)).config.droplet;
|
|
1199
|
+
if (droplet?.trim()) {
|
|
1200
|
+
const trimmedDroplet = droplet.trim();
|
|
1201
|
+
const dropletError = validateDropletUuid(trimmedDroplet);
|
|
1202
|
+
if (dropletError) throw new Error(dropletError);
|
|
1203
|
+
return trimmedDroplet;
|
|
1204
|
+
}
|
|
1205
|
+
throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
|
|
1206
|
+
}
|
|
1207
|
+
function parseLimit(value) {
|
|
1208
|
+
if (!value) return void 0;
|
|
1209
|
+
const parsed = Number(value);
|
|
1210
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 500) return parsed;
|
|
1211
|
+
throw new Error("--limit must be an integer between 1 and 500.");
|
|
1212
|
+
}
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region src/commands/publish.ts
|
|
1215
|
+
async function publishWidgetPackage(options, dependencies) {
|
|
1216
|
+
if (dependencies) {
|
|
1217
|
+
await dependencies.buildWidgetPackage({
|
|
1218
|
+
projectDir: options.projectDir,
|
|
1219
|
+
outDir: options.outDir
|
|
1220
|
+
});
|
|
1221
|
+
const dryRun = options.dryRun === true;
|
|
1222
|
+
if (dryRun) return {
|
|
1223
|
+
dryRun,
|
|
1224
|
+
uploaded: false,
|
|
1225
|
+
outDir: options.outDir
|
|
1226
|
+
};
|
|
1227
|
+
await dependencies.uploadWidgetPackage({
|
|
1228
|
+
outDir: options.outDir,
|
|
1229
|
+
droplet: options.droplet,
|
|
1230
|
+
client: options.client
|
|
1231
|
+
});
|
|
1232
|
+
return {
|
|
1233
|
+
dryRun,
|
|
1234
|
+
uploaded: true,
|
|
1235
|
+
outDir: options.outDir
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const droplet = await resolvePublishDroplet(options.projectDir, options.droplet);
|
|
1239
|
+
await validateDropletWidgetProject(options.projectDir, droplet);
|
|
1240
|
+
if (options.skipTypecheck !== true) await runPackageScriptIfAvailable({
|
|
1241
|
+
projectDir: options.projectDir,
|
|
1242
|
+
scriptName: "typecheck"
|
|
1243
|
+
});
|
|
1244
|
+
const dryRun = options.dryRun === true;
|
|
1245
|
+
await withRootWidgetConfigBridge(options.projectDir, () => publishWidgetRuntimeArtifacts({
|
|
1246
|
+
projectDir: options.projectDir,
|
|
1247
|
+
outDir: options.outDir,
|
|
1248
|
+
buildOwner: "droplet",
|
|
1249
|
+
uploadOwner: {
|
|
1250
|
+
kind: "droplet",
|
|
1251
|
+
uuid: droplet
|
|
1252
|
+
},
|
|
1253
|
+
dryRun,
|
|
1254
|
+
...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
|
|
1255
|
+
}), droplet);
|
|
1256
|
+
return {
|
|
1257
|
+
dryRun,
|
|
1258
|
+
uploaded: !dryRun,
|
|
1259
|
+
outDir: options.outDir,
|
|
1260
|
+
droplet
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const publishCommand = new Command("publish").description("Publish a Fluid widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_WIDGET_OUT_DIR).option("--droplet <uuid>", "Droplet UUID that owns the widget package").option("--dry-run", "Build and validate without uploading").option("--skip-typecheck", "Skip package typecheck script when present").action((options) => {
|
|
1264
|
+
return runCliAction(async () => {
|
|
1265
|
+
const projectDir = process.cwd();
|
|
1266
|
+
const outDir = options.outDir ?? ".fluid/widget-dist";
|
|
1267
|
+
const droplet = await resolvePublishDroplet(projectDir, options.droplet);
|
|
1268
|
+
const dryRun = options.dryRun === true;
|
|
1269
|
+
console.log();
|
|
1270
|
+
console.log(chalk.blue.bold("Fluid Widget Publish"));
|
|
1271
|
+
console.log();
|
|
1272
|
+
printRuntimeOnlyNotice();
|
|
1273
|
+
console.log(chalk.gray("Droplet: ") + chalk.white(droplet));
|
|
1274
|
+
console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
|
|
1275
|
+
if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
|
|
1276
|
+
console.log();
|
|
1277
|
+
const validateSpinner = ora("Validating widget package...").start();
|
|
1278
|
+
await validateDropletWidgetProject(projectDir, droplet);
|
|
1279
|
+
validateSpinner.succeed("Validated widget package");
|
|
1280
|
+
if (options.skipTypecheck !== true) {
|
|
1281
|
+
const typecheckSpinner = ora("Running package typecheck...").start();
|
|
1282
|
+
if ((await runPackageScriptIfAvailable({
|
|
1283
|
+
projectDir,
|
|
1284
|
+
scriptName: "typecheck"
|
|
1285
|
+
})).ran) typecheckSpinner.succeed("Package typecheck passed");
|
|
1286
|
+
else typecheckSpinner.info("No typecheck script found; skipping");
|
|
1287
|
+
}
|
|
1288
|
+
const publishSpinner = ora(dryRun ? "Building and preparing dry-run upload payload..." : "Building and publishing widget artifacts...").start();
|
|
1289
|
+
const result = await withRootWidgetConfigBridge(projectDir, () => publishWidgetRuntimeArtifacts({
|
|
1290
|
+
projectDir,
|
|
1291
|
+
outDir,
|
|
1292
|
+
buildOwner: "droplet",
|
|
1293
|
+
uploadOwner: {
|
|
1294
|
+
kind: "droplet",
|
|
1295
|
+
uuid: droplet
|
|
1296
|
+
},
|
|
1297
|
+
dryRun,
|
|
1298
|
+
...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
|
|
1299
|
+
}), droplet);
|
|
1300
|
+
publishSpinner.succeed(dryRun ? "Prepared dry-run upload payload" : "Published widget package");
|
|
1301
|
+
if (dryRun) {
|
|
1302
|
+
console.log(chalk.yellow("Dry run complete — upload session was not requested."));
|
|
1303
|
+
console.log();
|
|
1304
|
+
printDryRunSessionPayload(result);
|
|
1305
|
+
}
|
|
1306
|
+
printWidgetPublishSummary({
|
|
1307
|
+
title: dryRun ? "Widget publish dry run" : "Widget publish",
|
|
1308
|
+
ownerLabel: `droplet ${droplet}`,
|
|
1309
|
+
outDir,
|
|
1310
|
+
result
|
|
1311
|
+
});
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
async function resolvePublishDroplet(projectDir, override) {
|
|
1315
|
+
const droplet = override?.trim() || (await loadWidgetConfig(projectDir)).config.droplet;
|
|
1316
|
+
if (droplet?.trim()) {
|
|
1317
|
+
const trimmedDroplet = droplet.trim();
|
|
1318
|
+
const dropletError = validateDropletUuid(trimmedDroplet);
|
|
1319
|
+
if (dropletError) throw new Error(dropletError);
|
|
1320
|
+
return trimmedDroplet;
|
|
1321
|
+
}
|
|
1322
|
+
throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
|
|
1323
|
+
}
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region src/commands/status.ts
|
|
1326
|
+
const statusCommand = new Command("status").description("Show published widget package status for a Droplet").option("--droplet <uuid>", "Droplet UUID that owns the widget package").option("--json", "Print raw status response as JSON").action((options) => {
|
|
1327
|
+
return runCliAction(async () => {
|
|
1328
|
+
const droplet = await resolveDroplet(process.cwd(), options.droplet);
|
|
1329
|
+
const versions = await fetchDropletWidgetStatus({
|
|
1330
|
+
client: createAuthenticatedWidgetPackageClient(),
|
|
1331
|
+
droplet
|
|
1332
|
+
});
|
|
1333
|
+
if (options.json === true) {
|
|
1334
|
+
console.log(JSON.stringify(versions.map((version) => version.raw), null, 2));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
console.log(chalk.blue.bold(`Widget status for droplet ${droplet}`));
|
|
1338
|
+
console.log();
|
|
1339
|
+
console.log(formatStatusRows(versions));
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
async function resolveDroplet(projectDir, override) {
|
|
1343
|
+
const droplet = override?.trim() || (await loadWidgetConfig(projectDir)).config.droplet;
|
|
1344
|
+
if (droplet?.trim()) {
|
|
1345
|
+
const trimmedDroplet = droplet.trim();
|
|
1346
|
+
const dropletError = validateDropletUuid(trimmedDroplet);
|
|
1347
|
+
if (dropletError) throw new Error(dropletError);
|
|
1348
|
+
return trimmedDroplet;
|
|
1349
|
+
}
|
|
1350
|
+
throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
|
|
1351
|
+
}
|
|
1352
|
+
//#endregion
|
|
1353
|
+
//#region src/commands/validate.ts
|
|
1354
|
+
const validateCommand = new Command("validate").description("Validate the current droplet widget package").option("--json", "Print machine-readable validation output").action((options) => {
|
|
1355
|
+
return runCliAction(async () => {
|
|
1356
|
+
const result = await validateDropletWidgetProject(process.cwd());
|
|
1357
|
+
if (options.json === true) {
|
|
1358
|
+
console.log(JSON.stringify({
|
|
1359
|
+
valid: true,
|
|
1360
|
+
config: result.sourceConfigPath,
|
|
1361
|
+
packageId: result.validated.packageId,
|
|
1362
|
+
version: result.validated.version,
|
|
1363
|
+
widgets: result.validated.widgets.map((widget) => ({
|
|
1364
|
+
name: widget.name,
|
|
1365
|
+
type: widget.type
|
|
1366
|
+
}))
|
|
1367
|
+
}, null, 2));
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
console.log(chalk.green(formatValidationSuccess(result)));
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/index.ts
|
|
1375
|
+
const widgetCommand = new Command("widget").description("Create, preview, link, validate, build, and publish standalone Fluid widgets").addCommand(createCommand).addCommand(devCommand).addCommand(linkCommand).addCommand(validateCommand).addCommand(buildCommand).addCommand(publishCommand).addCommand(statusCommand).addCommand(logsCommand);
|
|
1376
|
+
const plugin = {
|
|
1377
|
+
name: "@fluid-app/fluid-cli-widget",
|
|
1378
|
+
version: "0.1.0",
|
|
1379
|
+
register(ctx) {
|
|
1380
|
+
const existingWidgetCommand = findCommand(ctx.program, "widget");
|
|
1381
|
+
if (existingWidgetCommand) {
|
|
1382
|
+
addCommandIfMissing(existingWidgetCommand, createCommand);
|
|
1383
|
+
addCommandIfMissing(existingWidgetCommand, devCommand);
|
|
1384
|
+
addCommandIfMissing(existingWidgetCommand, linkCommand);
|
|
1385
|
+
addCommandIfMissing(existingWidgetCommand, validateCommand);
|
|
1386
|
+
addCommandIfMissing(existingWidgetCommand, buildCommand);
|
|
1387
|
+
addCommandIfMissing(existingWidgetCommand, publishCommand);
|
|
1388
|
+
addCommandIfMissing(existingWidgetCommand, statusCommand);
|
|
1389
|
+
addCommandIfMissing(existingWidgetCommand, logsCommand);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
ctx.program.addCommand(widgetCommand);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
function addCommandIfMissing(parent, child) {
|
|
1396
|
+
if (findCommand(parent, child.name())) return;
|
|
1397
|
+
parent.addCommand(child);
|
|
1398
|
+
}
|
|
1399
|
+
function findCommand(parent, name) {
|
|
1400
|
+
return parent.commands?.find((command) => command.name() === name);
|
|
1401
|
+
}
|
|
1402
|
+
//#endregion
|
|
1403
|
+
export { DEFAULT_WIDGET_DEV_PORT, DEFAULT_WIDGET_OUT_DIR, ROOT_WIDGET_CONFIG_FILE, WIDGET_CONFIG_FILE, WIDGET_DEV_ROUTES, WidgetValidationError, buildCommand, buildViteDevArgs, buildWidgetPackage, createCommand, createWidgetProject, plugin as default, devCommand, formatValidationErrors, formatValidationSuccess, formatWidgetConfig, linkCommand, loadWidgetConfig, logsCommand, mergeWidgetConfig, parseWidgetConfigSource, printWidgetDevSummary, publishCommand, publishWidgetPackage, resolveWidgetConfigPath, resolveWidgetDevProject, resolveWidgetSourceConfig, runViteDevServer, statusCommand, validateCommand, validateDropletUuid, validateDropletWidgetProject, widgetCommand, withRootWidgetConfigBridge, writeWidgetConfig };
|
|
1404
|
+
|
|
1405
|
+
//# sourceMappingURL=index.mjs.map
|