@fluid-app/fluid-cli 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/fluid.mjs +22 -20
- package/dist/bin/fluid.mjs.map +1 -1
- package/dist/index.d.mts +13 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +45 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{token-D4DJFSdm.mjs → token-Bdv5tSmm.mjs} +45 -8
- package/dist/token-Bdv5tSmm.mjs.map +1 -0
- package/package.json +1 -1
- package/template-skills/fluid-widget-authoring/SKILL.md +58 -249
- package/template-skills/fluid-widget-authoring/references/package-authoring.md +28 -0
- package/template-skills/fluid-widget-authoring/references/portal-functions.md +23 -0
- package/template-skills/fluid-widget-authoring/references/property-fields.md +36 -0
- package/template-skills/fluid-widget-authoring/references/runtime-and-styling.md +44 -0
- package/template-skills/fluid-widget-authoring/references/workflows.md +50 -0
- package/dist/token-D4DJFSdm.mjs.map +0 -1
|
@@ -330,10 +330,10 @@ function readConfig() {
|
|
|
330
330
|
} catch {
|
|
331
331
|
return createDefaultConfig();
|
|
332
332
|
}
|
|
333
|
-
return {
|
|
333
|
+
return withDurableWorkspaceAliases({
|
|
334
334
|
...createDefaultConfig(),
|
|
335
335
|
...parsed
|
|
336
|
-
};
|
|
336
|
+
});
|
|
337
337
|
}
|
|
338
338
|
function writeConfig(config) {
|
|
339
339
|
const configPath = getConfigFilePath();
|
|
@@ -344,7 +344,7 @@ function writeConfig(config) {
|
|
|
344
344
|
});
|
|
345
345
|
const tmpPath = join(dir, `.fluid-config-${randomBytes(6).toString("hex")}.tmp`);
|
|
346
346
|
try {
|
|
347
|
-
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
|
|
347
|
+
writeFileSync(tmpPath, JSON.stringify(withDurableWorkspaceAliases(config), null, 2) + "\n", {
|
|
348
348
|
encoding: "utf-8",
|
|
349
349
|
mode: 384
|
|
350
350
|
});
|
|
@@ -356,6 +356,38 @@ function writeConfig(config) {
|
|
|
356
356
|
throw err;
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
+
function workspaceSlug(value) {
|
|
360
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
361
|
+
}
|
|
362
|
+
function storefrontSlug(value) {
|
|
363
|
+
return workspaceSlug((value.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/?#]/, 1)[0]?.replace(/:\d+$/, "") ?? "").replace(/\.fluid\.app$/i, ""));
|
|
364
|
+
}
|
|
365
|
+
/** Preserve rename history even when `fluid logout` clears the profiles that
|
|
366
|
+
* originally carried it. Derivation on read migrates existing config files;
|
|
367
|
+
* normalization on every write makes the migration durable before profiles
|
|
368
|
+
* are removed. */
|
|
369
|
+
function withDurableWorkspaceAliases(config) {
|
|
370
|
+
const aliases = {};
|
|
371
|
+
const add = (storefront, candidates) => {
|
|
372
|
+
const key = storefrontSlug(storefront);
|
|
373
|
+
if (!key) return;
|
|
374
|
+
const values = new Set(aliases[key] ?? []);
|
|
375
|
+
for (const candidate of candidates) {
|
|
376
|
+
const slug = workspaceSlug(candidate);
|
|
377
|
+
if (slug) values.add(slug);
|
|
378
|
+
}
|
|
379
|
+
if (values.size > 0) aliases[key] = [...values];
|
|
380
|
+
};
|
|
381
|
+
for (const [storefront, candidates] of Object.entries(config.workspaceAliasesByFluidShop ?? {})) {
|
|
382
|
+
if (!Array.isArray(candidates)) continue;
|
|
383
|
+
add(storefront, candidates.filter((candidate) => typeof candidate === "string"));
|
|
384
|
+
}
|
|
385
|
+
for (const profile of Object.values(config.profiles)) if (profile.fluidShop) add(profile.fluidShop, [profile.name, profile.companyName]);
|
|
386
|
+
return Object.keys(aliases).length > 0 || config.workspaceAliasesByFluidShop !== void 0 ? {
|
|
387
|
+
...config,
|
|
388
|
+
workspaceAliasesByFluidShop: aliases
|
|
389
|
+
} : config;
|
|
390
|
+
}
|
|
359
391
|
function updateConfig(updater) {
|
|
360
392
|
const updated = updater(readConfig());
|
|
361
393
|
writeConfig(updated);
|
|
@@ -428,16 +460,21 @@ function findProjectConfig(startDir) {
|
|
|
428
460
|
/**
|
|
429
461
|
* Get the auth token for a named profile, or the active profile when omitted.
|
|
430
462
|
*
|
|
431
|
-
* Resolution order (when `profileName` is omitted
|
|
432
|
-
*
|
|
433
|
-
*
|
|
463
|
+
* Resolution order (when `profileName` is omitted — mirrors `resolveToken`
|
|
464
|
+
* in domain/context.ts):
|
|
465
|
+
* 1. `FLUID_TOKEN` / `FLUID_API_TOKEN` environment variables
|
|
466
|
+
* 2. Project-level `.fluidrc` profile (walks upward from cwd)
|
|
467
|
+
* 3. Global active profile from `~/.fluid/config.json`
|
|
434
468
|
*
|
|
435
469
|
* When `profileName` is provided explicitly (e.g. via `--profile` flag),
|
|
436
|
-
* it takes precedence over
|
|
470
|
+
* it takes precedence over the environment variables, `.fluidrc`, and the
|
|
471
|
+
* active profile.
|
|
437
472
|
*/
|
|
438
473
|
function getAuthToken(profileName) {
|
|
439
474
|
const config = readConfig();
|
|
440
475
|
if (profileName) return config.profiles[profileName]?.token ?? null;
|
|
476
|
+
const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"];
|
|
477
|
+
if (envToken) return envToken;
|
|
441
478
|
const projectConfig = findProjectConfig(process.cwd());
|
|
442
479
|
if (projectConfig?.profile) {
|
|
443
480
|
const profile = config.profiles[projectConfig.profile];
|
|
@@ -464,4 +501,4 @@ function listProfileNames() {
|
|
|
464
501
|
//#endregion
|
|
465
502
|
export { unwrapOr as A, isSuccess as C, tryCatch as D, success as E, tryCatchAsync as O, isNodeError as S, mapResult as T, validateToken as _, readConfig as a, isError as b, getConfigDir as c, FLUID_API_ERROR as d, confirmMfa as f, switchCompany as g, sendMfa as h, findProjectConfig as i, unwrap as k, getConfigFilePath as l, getFluidApiBase as m, getAuthToken as n, updateConfig as o, fetchUserCompanies as p, listProfileNames as r, writeConfig as s, getActiveProfile as t, createDefaultConfig as u, failure as v, mapError as w, isFailure as x, getErrorMessage as y };
|
|
466
503
|
|
|
467
|
-
//# sourceMappingURL=token-
|
|
504
|
+
//# sourceMappingURL=token-Bdv5tSmm.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-Bdv5tSmm.mjs","names":[],"sources":["../src/utils/result.ts","../src/auth/fluid-api.ts","../src/config/types.ts","../src/config/paths.ts","../src/config/config.ts","../src/config/project-config.ts","../src/auth/token.ts"],"sourcesContent":["/**\n * Result type utilities for type-safe error handling\n *\n * The Result<T, E> pattern provides a discriminated union for fallible operations,\n * enabling exhaustive handling without try/catch blocks.\n */\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Result type - discriminated union for success/failure\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface Success<T> {\n readonly success: true;\n readonly value: T;\n}\n\nexport interface Failure<E> {\n readonly success: false;\n readonly error: E;\n}\n\nexport type Result<T, E = Error> = Success<T> | Failure<E>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constructor functions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function success<T>(value: T): Success<T> {\n return { success: true, value };\n}\n\nexport function failure<E>(error: E): Failure<E> {\n return { success: false, error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Type guards\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {\n return result.success === true;\n}\n\nexport function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {\n return result.success === false;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Utility functions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function tryCatch<T>(fn: () => T): Result<T, Error> {\n try {\n return success(fn());\n } catch (error) {\n return failure(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\nexport async function tryCatchAsync<T>(\n fn: () => Promise<T>,\n): Promise<Result<T, Error>> {\n try {\n return success(await fn());\n } catch (error) {\n return failure(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\nexport function unwrap<T, E>(result: Result<T, E>): T {\n if (isSuccess(result)) return result.value;\n // Always throw an Error instance so V8 attaches a stack trace and\n // `instanceof Error` checks in catch blocks work as expected.\n if (result.error instanceof Error) throw result.error;\n throw new Error(\n typeof result.error === \"object\" && result.error !== null\n ? JSON.stringify(result.error)\n : String(result.error),\n );\n}\n\nexport function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {\n if (isSuccess(result)) return result.value;\n return defaultValue;\n}\n\nexport function mapResult<T, U, E>(\n result: Result<T, E>,\n fn: (value: T) => U,\n): Result<U, E> {\n if (isSuccess(result)) return success(fn(result.value));\n return result;\n}\n\nexport function mapError<T, E, F>(\n result: Result<T, E>,\n fn: (error: E) => F,\n): Result<T, F> {\n if (isFailure(result)) return failure(fn(result.error));\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error narrowing utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function isError(value: unknown): value is Error {\n return value instanceof Error;\n}\n\nexport function isNodeError(value: unknown): value is NodeJS.ErrnoException {\n return value instanceof Error && \"code\" in value;\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return String(error);\n}\n","/**\n * Fluid API client for authentication operations\n */\n\nimport type { CliError } from \"../utils/errors.js\";\nimport { type Result, success, failure } from \"../utils/result.js\";\n\nconst API_TIMEOUT_MS = 10_000;\n\nexport function getFluidApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nfunction makeSignal(): { signal: AbortSignal; cleanup: () => void } {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);\n return { signal: controller.signal, cleanup: () => clearTimeout(timer) };\n}\n\nexport interface FluidApiError extends CliError {\n readonly code: string;\n readonly message: string;\n readonly details?: string;\n}\n\nexport const FLUID_API_ERROR = {\n INVALID_TOKEN: {\n code: \"INVALID_TOKEN\",\n message: \"Token is invalid or expired\",\n },\n API_UNREACHABLE: {\n code: \"API_UNREACHABLE\",\n message: \"Could not reach the Fluid API\",\n },\n UUID_NOT_FOUND: {\n code: \"UUID_NOT_FOUND\",\n message: \"Verification session not found\",\n },\n CODE_EXPIRED: {\n code: \"CODE_EXPIRED\",\n message: \"Verification code has expired — please request a new one\",\n },\n INVALID_CODE: {\n code: \"INVALID_CODE\",\n message: \"Invalid verification code\",\n },\n COMPANY_NOT_FOUND: {\n code: \"COMPANY_NOT_FOUND\",\n message: \"Company not found\",\n },\n SWITCH_FAILED: {\n code: \"SWITCH_FAILED\",\n message: \"Failed to switch company\",\n },\n} as const;\n\nfunction createApiError(\n template: { readonly code: string; readonly message: string },\n details?: string,\n): FluidApiError {\n return { code: template.code, message: template.message, details };\n}\n\nexport interface CompanyInfo {\n readonly name: string;\n}\n\nexport interface UserCompany {\n readonly id: number;\n readonly name: string;\n}\n\nexport interface SwitchCompanyResult {\n readonly companyId: number;\n readonly companyName: string;\n readonly fluidShop: string;\n readonly jwt: string;\n}\n\nexport interface MfaResponse {\n readonly uuid: string;\n readonly expiresAt: string;\n}\n\nexport interface CompanyChoice {\n readonly id: number;\n readonly name: string;\n readonly shopName: string;\n readonly jwt: string;\n}\n\nexport interface ConfirmMfaResponse {\n readonly authType: string;\n readonly companies: CompanyChoice[];\n}\n\n/**\n * Validate a token against the Fluid API and return company info\n */\nexport async function validateToken(\n token: string,\n): Promise<Result<CompanyInfo, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/company/v1/companies/me`,\n {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n },\n signal,\n },\n );\n\n if (response.ok) {\n const data = (await response.json()) as {\n data?: { company?: { name?: string } };\n };\n const name = data?.data?.company?.name;\n if (!name) {\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n \"Unexpected response shape from /companies/me\",\n ),\n );\n }\n return success({ name });\n }\n\n if (response.status === 401 || response.status === 403) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_TOKEN,\n `HTTP ${response.status}: Check that your token is valid and non-expired.`,\n ),\n );\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Send a multi-factor authentication code to the given email address.\n * Always returns 201 from the API (anti-enumeration).\n */\nexport async function sendMfa(\n email: string,\n): Promise<Result<MfaResponse, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/authentication/send_mfa`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email }),\n signal,\n },\n );\n\n if (response.status === 201) {\n const data = (await response.json()) as {\n multi_factor_authentication?: {\n uuid?: string;\n verification_code_expires_at?: string;\n };\n };\n const uuid = data.multi_factor_authentication?.uuid;\n const expiresAt =\n data.multi_factor_authentication?.verification_code_expires_at;\n if (!uuid || !expiresAt) {\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n \"Unexpected response shape from /send_mfa\",\n ),\n );\n }\n return success({ uuid, expiresAt });\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Confirm a multi-factor authentication code and retrieve company JWTs.\n */\nexport async function confirmMfa(\n uuid: string,\n code: string,\n): Promise<Result<ConfirmMfaResponse, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/authentication/confirm_mfa`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n uuid,\n multi_factor_authentication: { verification_code: code },\n }),\n signal,\n },\n );\n\n if (response.ok) {\n const data = (await response.json()) as {\n authenticated?: boolean;\n auth_type?: string;\n companies?:\n | {\n id: number;\n name: string;\n shop_name: string;\n jwt: string;\n }[]\n | null;\n };\n if (!data.authenticated) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_CODE,\n \"Authentication was not confirmed by the server\",\n ),\n );\n }\n const companies = Array.isArray(data.companies) ? data.companies : [];\n return success({\n authType: data.auth_type ?? \"\",\n companies: companies.map((c) => ({\n id: c.id,\n name: c.name,\n shopName: c.shop_name,\n jwt: c.jwt,\n })),\n });\n }\n\n if (response.status === 404) {\n return failure(createApiError(FLUID_API_ERROR.UUID_NOT_FOUND));\n }\n if (response.status === 410) {\n return failure(createApiError(FLUID_API_ERROR.CODE_EXPIRED));\n }\n if (response.status === 422) {\n return failure(createApiError(FLUID_API_ERROR.INVALID_CODE));\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Fetch all companies the authenticated user has access to.\n */\nexport async function fetchUserCompanies(\n token: string,\n baseUrl?: string,\n): Promise<Result<UserCompany[], FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n const apiBase = baseUrl ?? getFluidApiBase();\n try {\n const response = await fetch(`${apiBase}/api/me`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${token}` },\n signal,\n });\n\n if (response.ok) {\n const data = (await response.json()) as {\n companies?: { id: number; name: string }[];\n };\n const companies = Array.isArray(data.companies) ? data.companies : [];\n return success(companies.map((c) => ({ id: c.id, name: c.name })));\n }\n\n if (response.status === 401 || response.status === 403) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_TOKEN,\n `HTTP ${response.status}`,\n ),\n );\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Switch to a different company and receive a new JWT.\n *\n * Response shape (from @fluid-app/auth SwitchCompanyResponse):\n * { company: { id, name, fluid_shop, jwt }, meta: { request_id, timestamp } }\n */\nexport async function switchCompany(\n token: string,\n companyId: number,\n baseUrl?: string,\n): Promise<Result<SwitchCompanyResult, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n const apiBase = baseUrl ?? getFluidApiBase();\n try {\n const response = await fetch(\n `${apiBase}/api/authentication/company/${companyId}/switch`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n signal,\n },\n );\n\n if (response.ok) {\n const data = (await response.json()) as {\n company?: {\n id: number;\n name: string;\n fluid_shop: string;\n jwt: string;\n };\n };\n const company = data.company;\n if (!company?.jwt) {\n return failure(\n createApiError(\n FLUID_API_ERROR.SWITCH_FAILED,\n \"Unexpected response shape from /switch\",\n ),\n );\n }\n return success({\n companyId: company.id,\n companyName: company.name,\n fluidShop: company.fluid_shop,\n jwt: company.jwt,\n });\n }\n\n if (response.status === 404) {\n return failure(\n createApiError(\n FLUID_API_ERROR.COMPANY_NOT_FOUND,\n `Company ${companyId} not found`,\n ),\n );\n }\n\n if (response.status === 401 || response.status === 403) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_TOKEN,\n `HTTP ${response.status}`,\n ),\n );\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.SWITCH_FAILED,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n","/**\n * Configuration types for the Fluid CLI\n */\n\nexport interface FluidProfile {\n readonly name: string;\n readonly token: string;\n readonly companyName: string;\n readonly storedAt: string; // ISO-8601\n readonly baseUrl?: string;\n /** Stable storefront identity written by Mist and newer auth endpoints. */\n readonly fluidShop?: string;\n}\n\nexport interface FluidConfig {\n activeProfile: string | null;\n profiles: Record<string, FluidProfile>;\n plugins: Record<string, unknown>;\n /**\n * Allow-list of plugin package names.\n *\n * - `null` (default) — auto-discover and load all `@fluid-app/fluid-cli-*`\n * and `*-cli-commands` packages found in `node_modules` or the pnpm\n * workspace. Only `@fluid-app`-scoped packages matching the naming\n * convention are eligible; no third-party code is loaded.\n * - `string[]` — only load plugins whose names appear in the array.\n * Set to `[]` to disable all plugins.\n */\n enabledPlugins: string[] | null;\n /** Historical local workspace slugs, keyed by stable storefront slug.\n * Optional because older CLI versions and hand-written test configs do not\n * know about Mist's local workspace compatibility metadata. */\n workspaceAliasesByFluidShop?: Record<string, string[]>;\n}\n\nexport function createDefaultConfig(): FluidConfig {\n return {\n activeProfile: null,\n profiles: {},\n plugins: {},\n enabledPlugins: null, // auto-discover all plugins\n };\n}\n","/**\n * XDG-compliant config directory resolution\n *\n * Priority:\n * 1. FLUID_CONFIG_DIR env var (explicit override)\n * 2. ~/.fluid/ on macOS (convention for CLI tools)\n * 3. %APPDATA%/fluid/ on Windows\n * 4. $XDG_CONFIG_HOME/fluid/ on Linux (XDG spec)\n * 5. ~/.config/fluid/ fallback on Linux\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function getConfigDir(): string {\n const envOverride = process.env[\"FLUID_CONFIG_DIR\"];\n if (envOverride) return envOverride;\n\n if (process.platform === \"darwin\") {\n return join(homedir(), \".fluid\");\n }\n\n if (process.platform === \"win32\") {\n const appData =\n process.env[\"APPDATA\"] ?? join(homedir(), \"AppData\", \"Roaming\");\n return join(appData, \"fluid\");\n }\n\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return join(xdgConfig, \"fluid\");\n\n return join(homedir(), \".config\", \"fluid\");\n}\n\nexport function getConfigFilePath(): string {\n return join(getConfigDir(), \"config.json\");\n}\n","/**\n * Read/write config.json with atomic writes and safe defaults\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\nimport { type FluidConfig, createDefaultConfig } from \"./types.js\";\nimport { getConfigFilePath } from \"./paths.js\";\n\nexport function readConfig(): FluidConfig {\n const configPath = getConfigFilePath();\n\n if (!existsSync(configPath)) {\n return createDefaultConfig();\n }\n\n const raw = readFileSync(configPath, \"utf-8\");\n\n let parsed: Partial<FluidConfig>;\n try {\n parsed = JSON.parse(raw) as Partial<FluidConfig>;\n } catch {\n // Corrupted config — fall back to defaults rather than crashing\n return createDefaultConfig();\n }\n\n // Merge with defaults to handle schema evolution\n return withDurableWorkspaceAliases({\n ...createDefaultConfig(),\n ...parsed,\n });\n}\n\nexport function writeConfig(config: FluidConfig): void {\n const configPath = getConfigFilePath();\n const dir = dirname(configPath);\n\n // 0o700: owner-only access so other users cannot enumerate the config dir\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n\n // Atomic write: write to a temp file in the same directory, then rename.\n // rename() is a POSIX atomic operation — a crash mid-write will never leave\n // a partially-written config.json. The temp file must be on the same\n // filesystem as the destination for rename() to succeed.\n const suffix = randomBytes(6).toString(\"hex\");\n const tmpPath = join(dir, `.fluid-config-${suffix}.tmp`);\n\n try {\n writeFileSync(\n tmpPath,\n JSON.stringify(withDurableWorkspaceAliases(config), null, 2) + \"\\n\",\n {\n encoding: \"utf-8\",\n // NOTE: mode: 0o600 restricts to owner read/write on POSIX (macOS, Linux).\n // On Windows, Node.js ignores this option — NTFS permissions are not\n // set via the mode parameter. Windows file protection would require\n // icacls or Windows security descriptor APIs.\n mode: 0o600,\n },\n );\n renameSync(tmpPath, configPath);\n } catch (err) {\n // Clean up the temp file if something went wrong before the rename\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore cleanup errors — the temp file may not exist yet\n }\n throw err;\n }\n}\n\nfunction workspaceSlug(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nfunction storefrontSlug(value: string): string {\n const withoutScheme = value.trim().replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\");\n const host = withoutScheme.split(/[/?#]/, 1)[0]?.replace(/:\\d+$/, \"\") ?? \"\";\n return workspaceSlug(host.replace(/\\.fluid\\.app$/i, \"\"));\n}\n\n/** Preserve rename history even when `fluid logout` clears the profiles that\n * originally carried it. Derivation on read migrates existing config files;\n * normalization on every write makes the migration durable before profiles\n * are removed. */\nfunction withDurableWorkspaceAliases(config: FluidConfig): FluidConfig {\n const aliases: Record<string, string[]> = {};\n const add = (storefront: string, candidates: readonly string[]): void => {\n const key = storefrontSlug(storefront);\n if (!key) return;\n const values = new Set(aliases[key] ?? []);\n for (const candidate of candidates) {\n const slug = workspaceSlug(candidate);\n if (slug) values.add(slug);\n }\n if (values.size > 0) aliases[key] = [...values];\n };\n\n for (const [storefront, candidates] of Object.entries(\n config.workspaceAliasesByFluidShop ?? {},\n )) {\n if (!Array.isArray(candidates)) continue;\n add(\n storefront,\n candidates.filter(\n (candidate): candidate is string => typeof candidate === \"string\",\n ),\n );\n }\n for (const profile of Object.values(config.profiles)) {\n if (profile.fluidShop) {\n add(profile.fluidShop, [profile.name, profile.companyName]);\n }\n }\n\n return Object.keys(aliases).length > 0 ||\n config.workspaceAliasesByFluidShop !== undefined\n ? { ...config, workspaceAliasesByFluidShop: aliases }\n : config;\n}\n\nexport function updateConfig(\n updater: (config: FluidConfig) => FluidConfig,\n): FluidConfig {\n const config = readConfig();\n const updated = updater(config);\n writeConfig(updated);\n return updated;\n}\n","/**\n * Project-level config (.fluidrc) reader.\n *\n * Walks upward from a starting directory to find a `.fluidrc` file.\n * The file is JSON with a `profile` field that maps to a profile name\n * in `~/.fluid/config.json`.\n *\n * This allows portal repos to pin themselves to a specific CLI profile\n * so that `fluid portal pull` / `push` always use the correct company\n * credentials, regardless of which profile is globally active.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nexport interface ProjectRcConfig {\n readonly profile: string;\n}\n\nconst RC_FILENAME = \".fluidrc\";\n\n/** Cached result keyed by the starting directory. */\nlet cachedResult: { dir: string; config: ProjectRcConfig | null } | undefined;\n\n/**\n * Walk upward from `startDir` looking for a `.fluidrc` file.\n *\n * Returns the parsed config when found, or `null` if no valid `.fluidrc`\n * exists in any ancestor directory.\n *\n * The result is cached per starting directory for the lifetime of the\n * process — CLI commands are short-lived so stale-cache is not a concern.\n */\nexport function findProjectConfig(startDir: string): ProjectRcConfig | null {\n if (cachedResult && cachedResult.dir === startDir) {\n return cachedResult.config;\n }\n\n let dir = startDir;\n while (true) {\n const rcPath = join(dir, RC_FILENAME);\n if (existsSync(rcPath)) {\n try {\n const raw = readFileSync(rcPath, \"utf-8\");\n const parsed = JSON.parse(raw) as Partial<ProjectRcConfig>;\n if (typeof parsed.profile === \"string\" && parsed.profile.length > 0) {\n const config: ProjectRcConfig = { profile: parsed.profile };\n cachedResult = { dir: startDir, config };\n return config;\n }\n } catch {\n // Malformed .fluidrc — treat as absent\n }\n\n // Found a .fluidrc but it was invalid or empty — stop searching\n // upward to avoid confusing inheritance from a parent directory.\n cachedResult = { dir: startDir, config: null };\n return null;\n }\n\n const parent = dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n\n cachedResult = { dir: startDir, config: null };\n return null;\n}\n\n/** Reset the cache. Exposed for tests only. */\nexport function _resetProjectConfigCache(): void {\n cachedResult = undefined;\n}\n","/**\n * Token storage and retrieval from config\n */\n\nimport { readConfig } from \"../config/config.js\";\nimport { findProjectConfig } from \"../config/project-config.js\";\nimport type { FluidProfile } from \"../config/types.js\";\n\n/**\n * Get the auth token for a named profile, or the active profile when omitted.\n *\n * Resolution order (when `profileName` is omitted — mirrors `resolveToken`\n * in domain/context.ts):\n * 1. `FLUID_TOKEN` / `FLUID_API_TOKEN` environment variables\n * 2. Project-level `.fluidrc` profile (walks upward from cwd)\n * 3. Global active profile from `~/.fluid/config.json`\n *\n * When `profileName` is provided explicitly (e.g. via `--profile` flag),\n * it takes precedence over the environment variables, `.fluidrc`, and the\n * active profile.\n */\nexport function getAuthToken(profileName?: string): string | null {\n const config = readConfig();\n\n // Explicit profile name takes precedence\n if (profileName) {\n const profile = config.profiles[profileName];\n return profile?.token ?? null;\n }\n\n // Environment variable override (before any stored profile)\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n\n // Project-level .fluidrc profile\n const projectConfig = findProjectConfig(process.cwd());\n if (projectConfig?.profile) {\n const profile = config.profiles[projectConfig.profile];\n if (profile) return profile.token;\n }\n\n // Fall back to global active profile\n if (!config.activeProfile) return null;\n const profile = config.profiles[config.activeProfile];\n return profile?.token ?? null;\n}\n\n/**\n * Get the active profile, or null if not logged in\n */\nexport function getActiveProfile(): FluidProfile | null {\n const config = readConfig();\n if (!config.activeProfile) return null;\n\n return config.profiles[config.activeProfile] ?? null;\n}\n\n/**\n * List all stored profile names\n */\nexport function listProfileNames(): string[] {\n const config = readConfig();\n return Object.keys(config.profiles);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,253 +1,62 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fluid-widget-authoring
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when authoring, validating, building, or publishing company or Droplet Fluid Remote DOM widget packages.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Fluid
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- `dataSourceTargetProps`: prop keys that data sources may populate.
|
|
64
|
-
- `itemConfigSchema`: optional per-item fields for custom data-source item settings.
|
|
65
|
-
- Avoid `validate` in standalone published metadata because metadata must be serializable.
|
|
66
|
-
|
|
67
|
-
Base field keys:
|
|
68
|
-
|
|
69
|
-
- `key`, `label`, and `type` are required.
|
|
70
|
-
- Optional: `description`, `defaultValue`, `tab`, `group`, `advanced`, `requiresKeyValue`.
|
|
71
|
-
- `advanced: true` is for lower-frequency theme/style overrides.
|
|
72
|
-
- `requiresKeyValue` can be one condition or an array of AND conditions.
|
|
73
|
-
|
|
74
|
-
Supported field types:
|
|
75
|
-
|
|
76
|
-
- `text`: single-line string; supports `placeholder`, `maxLength`, `tokenSuggestions`.
|
|
77
|
-
- `textarea`: multi-line string; supports `placeholder`, `rows`, `maxLength`.
|
|
78
|
-
- `number`: numeric input; supports `min`, `max`, `step`.
|
|
79
|
-
- `boolean`: toggle.
|
|
80
|
-
- `select`: dropdown; requires `options` with `label` and `value`.
|
|
81
|
-
- `color`: basic color value.
|
|
82
|
-
- `range`: slider; requires `min`, `max`; optional `step`.
|
|
83
|
-
- `dataSource`: data-source configuration.
|
|
84
|
-
- `resource`: resource picker; optional `allowedTypes`.
|
|
85
|
-
- `image`: media picker; optional `accept` as `image`, `video`, or `any`.
|
|
86
|
-
- `alignment`: alignment picker; requires vertical/horizontal enabled flags.
|
|
87
|
-
- `slider`: numeric slider; supports `min`, `max`, `step`, `unit`.
|
|
88
|
-
- `colorPicker`: color picker; supports `swatches`.
|
|
89
|
-
- `sectionHeader`: visual header; supports `subtitle`.
|
|
90
|
-
- `separator`: visual divider.
|
|
91
|
-
- `buttonGroup`: segmented control; requires options with `value` and optional labels/icons.
|
|
92
|
-
- `colorSelect`: semantic theme color selector; supports `excludeColors`.
|
|
93
|
-
- `sectionLayoutSelect`: visual layout selector.
|
|
94
|
-
- `background`: combined background resource/color control.
|
|
95
|
-
- `contentPosition`: 3-by-3 position picker.
|
|
96
|
-
- `textSizeSelect`: theme text size selector.
|
|
97
|
-
- `cssUnit`: number plus unit; supports unit allowlist/default and per-unit min/max/step maps.
|
|
98
|
-
- `fontPicker`: Google font picker; supports `placeholder`.
|
|
99
|
-
- `stringArray`: editable list of strings; supports `placeholder`.
|
|
100
|
-
- `borderRadius`: four-corner radius editor; requires `keys.topLeft`, `keys.topRight`, `keys.bottomLeft`, `keys.bottomRight`.
|
|
101
|
-
- `screenPicker`: portal screen picker; supports `includeSystemItems`.
|
|
102
|
-
|
|
103
|
-
## Data-source-ready props
|
|
104
|
-
|
|
105
|
-
- Add bindable prop names to `dataSourceTargetProps`.
|
|
106
|
-
- Make components resilient to missing, empty, or partial prop data.
|
|
107
|
-
- Render empty states for empty arrays and invalid items.
|
|
108
|
-
- Keep defaults in `defaultProps` so manual use works before a data source is connected.
|
|
109
|
-
- Use `itemConfigSchema` for per-selected-item settings.
|
|
110
|
-
- Do not fetch directly when props/data sources can provide data.
|
|
111
|
-
|
|
112
|
-
## Component quality and accessibility
|
|
113
|
-
|
|
114
|
-
- Use typed props and defaults.
|
|
115
|
-
- Guard unsafe array/object access.
|
|
116
|
-
- Keep render deterministic and side-effect free.
|
|
117
|
-
- Clean up effects.
|
|
118
|
-
- Keep bundles small and avoid unnecessary large dependencies.
|
|
119
|
-
- Never put secrets or tenant credentials in widget source, props, or defaults.
|
|
120
|
-
- Use semantic HTML.
|
|
121
|
-
- Ensure keyboard access and visible focus states.
|
|
122
|
-
- Add labels or accessible names to controls.
|
|
123
|
-
- Add alt text for meaningful media.
|
|
124
|
-
- Keep heading order logical.
|
|
125
|
-
- Respect reduced motion.
|
|
126
|
-
- Use theme foreground/background pairs for contrast.
|
|
127
|
-
|
|
128
|
-
## Theme and CSS
|
|
129
|
-
|
|
130
|
-
Prefer Fluid semantic CSS variables:
|
|
131
|
-
|
|
132
|
-
- Surface and text: `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--popover-foreground`.
|
|
133
|
-
- Brand/actions: `--primary`, `--primary-foreground`.
|
|
134
|
-
- Supporting UI: `--secondary`, `--secondary-foreground`, `--muted`, `--muted-foreground`, `--accent`, `--accent-foreground`.
|
|
135
|
-
- Status/chrome: `--destructive`, `--destructive-foreground`, `--border`, `--input`, `--ring`.
|
|
136
|
-
- Charts: `--chart-1` through `--chart-5`.
|
|
137
|
-
- Radius: `--radius`, `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl`.
|
|
138
|
-
- Fonts and sizes may be available as `--font-header`, `--font-body`, and `--font-size-*` theme aliases.
|
|
139
|
-
|
|
140
|
-
Tailwind equivalents, when Tailwind is available, are semantic utilities such as `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-primary`, `text-primary-foreground`, `border-border`, `ring-ring`, and `rounded-lg`. Plain CSS is the most portable runtime default for this template.
|
|
141
|
-
|
|
142
|
-
Light/dark mode is controlled by host theme variables and may use `data-theme-mode="dark"`. Prefer tokens so light and dark work automatically. If a mode-specific rule is necessary, scope it to the dark-mode attribute and still use tokens.
|
|
143
|
-
|
|
144
|
-
CSS must be reachable from `src/index.ts` or another worker module. Prefix selectors with the widget name and do not rely on global body styles for published runtime output.
|
|
145
|
-
|
|
146
|
-
## Remote DOM runtime
|
|
147
|
-
|
|
148
|
-
- In a company portal project, `src/widgets.config.ts` is authoring input; the portal CLI generates the worker artifact. Do not add a hand-written worker bootstrap.
|
|
149
|
-
- In a standalone widget project, `src/index.ts` calls `startWidgetPackage(widgetPackage)` exactly once.
|
|
150
|
-
- Generated portals read company packages from the authenticated `/api/app/widget-packages` catalog.
|
|
151
|
-
- Standalone widget dev exposes `/__widget-packages__` as the local canonical descriptor endpoint and `/__runtime-entry__` as the worker entry. It does not bundle a second host or iframe preview application.
|
|
152
|
-
- Do not add global widget registries, classic script loaders, or direct React host previews.
|
|
153
|
-
- Call typed portal functions from `@fluid-app/portal-sdk/widgets/worker` directly and list each function in `defineWidget({ uses: [...] })`.
|
|
154
|
-
|
|
155
|
-
## Typed portal functions
|
|
156
|
-
|
|
157
|
-
Built-in worker functions are `getUserAccount`, `getStore`, `getPortalApp`,
|
|
158
|
-
`getPortalProfile`, `getNavigationState`, `buildPortalHref`, `navigateTo`,
|
|
159
|
-
`getFullscreenState`, `requestFullscreen`, and `exitFullscreen`. Getters await
|
|
160
|
-
and return resolved data; they do not return query-status snapshots.
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
import {
|
|
164
|
-
defineWidget,
|
|
165
|
-
getUserAccount,
|
|
166
|
-
navigateTo,
|
|
167
|
-
} from "@fluid-app/portal-sdk/widgets/worker";
|
|
168
|
-
|
|
169
|
-
async function openProfile(): Promise<void> {
|
|
170
|
-
const account = await getUserAccount();
|
|
171
|
-
await navigateTo(account.slug);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export const profileButton = defineWidget({
|
|
175
|
-
name: "ProfileButton",
|
|
176
|
-
component: ProfileButton,
|
|
177
|
-
uses: [getUserAccount, navigateTo],
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
`defineWidget()` derives and deduplicates descriptor declarations from `uses`.
|
|
182
|
-
The worker and host both enforce those declarations. If a call fails with
|
|
183
|
-
`PortalFunctionError` code `NOT_DECLARED`, add the called function to that
|
|
184
|
-
widget's `uses` array. Do not author a raw `capabilities` array.
|
|
185
|
-
|
|
186
|
-
The account result is intentionally allowlisted. It may include email, but it
|
|
187
|
-
does not expose phone numbers, addresses, payment data, credentials,
|
|
188
|
-
government or tax identifiers, dates of birth, raw metadata, or unknown future
|
|
189
|
-
fields. There is no `tenantClient` worker API.
|
|
190
|
-
|
|
191
|
-
For a company-specific operation, create one typed function definition shared
|
|
192
|
-
by worker and host:
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
export const getQuote = definePortalFunction<QuoteResult, QuoteRequest>({
|
|
196
|
-
capability: "company.example.quotes",
|
|
197
|
-
version: "1",
|
|
198
|
-
method: "get",
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Call `getQuote(input)` directly in the worker and include `getQuote` in `uses`.
|
|
203
|
-
The portal host installs the implementation with
|
|
204
|
-
`implementPortalFunction(getQuote, handler)` in `remoteWidgets.functions`.
|
|
205
|
-
Built-ins cannot be overridden and duplicate custom implementations are
|
|
206
|
-
rejected.
|
|
207
|
-
|
|
208
|
-
Custom inputs and outputs are JSON-only: null, booleans, finite numbers,
|
|
209
|
-
strings, arrays, and objects made from those values. Do not pass `undefined`,
|
|
210
|
-
functions, Dates, Maps, Sets, class instances, `NaN`, or `Infinity`. Types
|
|
211
|
-
describe the contract and runtime validation guards the protocol boundary.
|
|
212
|
-
|
|
213
|
-
## Company portal widget preflight
|
|
214
|
-
|
|
215
|
-
In a generated portal project, validate and dry-run the company package with:
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
pnpm typecheck
|
|
219
|
-
pnpm build
|
|
220
|
-
pnpm exec fluid portal deploy --dry-run
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Publish company-owned runtime artifacts with `fluid portal deploy`. This does
|
|
224
|
-
not push `portal/` JSON or deploy the hosted portal shell.
|
|
225
|
-
|
|
226
|
-
## Standalone droplet widget preflight
|
|
227
|
-
|
|
228
|
-
In a generated standalone widget project, run:
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
pnpm typecheck
|
|
232
|
-
pnpm validate
|
|
233
|
-
pnpm build
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
For standalone publish readiness, also run:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
pnpm run widget:publish --dry-run
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Publish the linked droplet-owned package with `fluid widget publish`. Do not use
|
|
243
|
-
`fluid portal deploy` from a standalone project.
|
|
244
|
-
|
|
245
|
-
Common failures:
|
|
246
|
-
|
|
247
|
-
- Missing droplet UUID: run `pnpm run widget:link` or pass a droplet option.
|
|
248
|
-
- No source package found: ensure `fluid.widget.config.ts` exports the package from `manifest.ts`.
|
|
249
|
-
- Invalid package type: keep `packageType: "droplet"`.
|
|
250
|
-
- Invalid name/key/version: use URL-safe identifiers and SemVer without build metadata.
|
|
251
|
-
- Non-serializable metadata: remove functions, undefined, Dates, NaN, Infinity, Maps, Sets, and class instances.
|
|
252
|
-
- Missing published CSS: keep the guarded CSS import reachable from the worker build graph.
|
|
253
|
-
- Local development only succeeds: remove local-only URLs and guard browser APIs.
|
|
6
|
+
# Fluid widget authoring
|
|
7
|
+
|
|
8
|
+
## Choose the workflow
|
|
9
|
+
|
|
10
|
+
- Standalone Droplet package: use `fluid widget` commands and the package's
|
|
11
|
+
`manifest.ts` plus `fluid.widget.config.ts`.
|
|
12
|
+
- Company package inside a portal project: use `fluid portal widget create`,
|
|
13
|
+
register the package in `src/widgets.config.ts`, and publish it with
|
|
14
|
+
`fluid portal deploy`.
|
|
15
|
+
|
|
16
|
+
Do not copy configuration between these package types. Their owners and
|
|
17
|
+
publication endpoints differ.
|
|
18
|
+
|
|
19
|
+
## Authoring workflow
|
|
20
|
+
|
|
21
|
+
1. Inspect the project layout and preserve existing work.
|
|
22
|
+
2. Confirm whether the package is company-owned in a portal project or linked
|
|
23
|
+
to a Droplet in a standalone widget project.
|
|
24
|
+
3. Inspect the current manifest and installed API reference before changing
|
|
25
|
+
package metadata. Use the installed reference for the exact fields, values,
|
|
26
|
+
portal functions, and capabilities supported by this SDK version.
|
|
27
|
+
4. Implement the component for missing, partial, and malformed JSON inputs.
|
|
28
|
+
5. Make the widget comply with the portal theme. Use semantic CSS variables and
|
|
29
|
+
`colorSelect` fields. The legacy `color` field is deprecated. Add custom
|
|
30
|
+
colors or styling properties only when the theme engine cannot represent an
|
|
31
|
+
explicit product requirement.
|
|
32
|
+
6. Keep runtime CSS in the project-root `styles.css` and scope it to the
|
|
33
|
+
widget. Company widget modules must not import CSS; preserve the standalone
|
|
34
|
+
template's guarded worker import.
|
|
35
|
+
7. Typecheck, validate, build, and inspect the result in a portal host. Use a
|
|
36
|
+
dry run before an authorized publication.
|
|
37
|
+
|
|
38
|
+
## Completion checks
|
|
39
|
+
|
|
40
|
+
- Defaults satisfy the property schema and render a useful initial state.
|
|
41
|
+
- Missing and malformed JSON do not crash rendering.
|
|
42
|
+
- Every portal function and capability in the component appears in `uses`.
|
|
43
|
+
- Keyboard, focus, labels, contrast, and reduced motion work.
|
|
44
|
+
- Light and dark themes work without literal color overrides.
|
|
45
|
+
- Generated artifacts were inspected but not edited or committed.
|
|
46
|
+
- The package version changed if an immutable published package changed.
|
|
47
|
+
|
|
48
|
+
## References
|
|
49
|
+
|
|
50
|
+
- [Package authoring](references/package-authoring.md)
|
|
51
|
+
- [Property fields](references/property-fields.md)
|
|
52
|
+
- [Portal functions](references/portal-functions.md)
|
|
53
|
+
- [Runtime, styling, accessibility, and security](references/runtime-and-styling.md)
|
|
54
|
+
- [Development and publication](references/workflows.md)
|
|
55
|
+
- [Installed widget API](../../../node_modules/@fluid-app/portal-sdk/authoring/widget-api/api.md)
|
|
56
|
+
- [Installed widget commands](../../../node_modules/@fluid-app/fluid-cli-widget/authoring/commands.md)
|
|
57
|
+
|
|
58
|
+
If an LSP is unavailable, use the installed API reference. The installed
|
|
59
|
+
reference and declarations match the SDK version in this project.
|
|
60
|
+
|
|
61
|
+
Use the project scripts and installed command reference to select checks for
|
|
62
|
+
the current project type.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Package authoring
|
|
2
|
+
|
|
3
|
+
Use `@fluid-app/portal-sdk/widgets/worker` for worker APIs. Read the installed
|
|
4
|
+
widget API before changing package or widget metadata. It is the authoritative
|
|
5
|
+
contract for the installed SDK version; this guide does not repeat its fields
|
|
6
|
+
or accepted values.
|
|
7
|
+
|
|
8
|
+
Keep package identity and widget names stable after publication. Publish a new
|
|
9
|
+
semantic version for each release. Do not author runtime artifact URLs; the
|
|
10
|
+
build and publication flow creates them.
|
|
11
|
+
|
|
12
|
+
Keep widget registration, defaults, property-schema metadata, and declared
|
|
13
|
+
uses together in the package manifest. Confirm that defaults satisfy the
|
|
14
|
+
schema and that every function or capability used at runtime is declared.
|
|
15
|
+
|
|
16
|
+
Company widget packages are registered in a portal project's
|
|
17
|
+
`src/widgets.config.ts`. Standalone widget packages are associated with an
|
|
18
|
+
existing Droplet through `fluid.widget.config.ts`. That file resolves and
|
|
19
|
+
exports the package from `manifest.ts`; it does not own package identity,
|
|
20
|
+
version, or widgets.
|
|
21
|
+
|
|
22
|
+
Use the portal JSON Schema for the exact third-party widget type and node
|
|
23
|
+
shape. Do not copy its pattern into project guidance or infer a type from an
|
|
24
|
+
empty catalog response.
|
|
25
|
+
|
|
26
|
+
All metadata that crosses the worker boundary must satisfy the installed
|
|
27
|
+
JSON-value types. Use those types instead of maintaining a separate list of
|
|
28
|
+
allowed or prohibited JavaScript values.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Portal-function guidance
|
|
2
|
+
|
|
3
|
+
Use the installed widget API for the complete set of portal functions, their
|
|
4
|
+
arguments, return types, errors, and capability declarations. Do not maintain
|
|
5
|
+
a function or capability list in this skill.
|
|
6
|
+
|
|
7
|
+
Call portal functions directly and put the same function values in the
|
|
8
|
+
widget's `uses` array. Do not write a parallel capability-name list. The worker
|
|
9
|
+
and host enforce those declarations. If a call reports that it was not
|
|
10
|
+
declared, add the called function value to `uses` and rebuild the package.
|
|
11
|
+
|
|
12
|
+
Treat every returned value as the documented allowlisted view, not as a raw
|
|
13
|
+
tenant API response. Worker code has no tenant API client. Handle unavailable
|
|
14
|
+
or partial data without crashing the widget.
|
|
15
|
+
|
|
16
|
+
For a company-specific operation, define one typed portal function in a module
|
|
17
|
+
shared by the worker package and portal host. Register its host implementation
|
|
18
|
+
through the portal's remote-widget configuration. Use the installed worker and
|
|
19
|
+
host API references for the exact definition, implementation, and error
|
|
20
|
+
contracts.
|
|
21
|
+
|
|
22
|
+
Do not override a built-in function or register the same custom function more
|
|
23
|
+
than once. Inputs and outputs must satisfy the installed JSON-value types.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Property fields
|
|
2
|
+
|
|
3
|
+
Property fields define the builder editor and generated portal JSON Schema.
|
|
4
|
+
Put the prop description on the field that produces that prop. Do not create a
|
|
5
|
+
separate prop-description section.
|
|
6
|
+
|
|
7
|
+
Every prop-producing field needs a stable `key`, clear `label`, precise
|
|
8
|
+
`description`, and a valid default when the field supports one. Use the
|
|
9
|
+
installed API reference for the current field types, options, and required
|
|
10
|
+
members.
|
|
11
|
+
|
|
12
|
+
Section headers and separators organize the editor and do not produce props.
|
|
13
|
+
|
|
14
|
+
## Theme-aware fields
|
|
15
|
+
|
|
16
|
+
Use `colorSelect` when an author must choose a color. Its value comes from the
|
|
17
|
+
theme engine's semantic color-token contract. The legacy `color` field is
|
|
18
|
+
deprecated. Do not add new uses. Avoid arbitrary color controls unless an
|
|
19
|
+
explicit product requirement cannot use the theme engine.
|
|
20
|
+
|
|
21
|
+
Apply the same rule to other visual properties. Prefer theme typography,
|
|
22
|
+
spacing, radii, borders, and shadows over widget-owned values. If a necessary
|
|
23
|
+
custom style can override a theme-derived value, mark it `advanced`, provide a
|
|
24
|
+
theme-compatible default, and describe the compatibility cost.
|
|
25
|
+
|
|
26
|
+
## Data sources
|
|
27
|
+
|
|
28
|
+
If a prop exists only as a data-source result, document it on the canonical
|
|
29
|
+
data-source field rather than in a parallel prop list. Keep each target prop's
|
|
30
|
+
description with that mapping and keep the schema's target-prop declaration in
|
|
31
|
+
sync. Use the installed property-field API for the exact mapping shape and
|
|
32
|
+
per-item configuration contract.
|
|
33
|
+
|
|
34
|
+
Components must accept missing, empty, partial, or malformed data and render a
|
|
35
|
+
useful empty state. Prefer props and host data sources to direct network
|
|
36
|
+
requests.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Runtime, styling, accessibility, and security
|
|
2
|
+
|
|
3
|
+
## Component behavior
|
|
4
|
+
|
|
5
|
+
Use semantic HTML, accessible names, keyboard operation, visible focus, useful
|
|
6
|
+
alt text, logical heading order, and reduced-motion handling. Guard unsafe
|
|
7
|
+
array and object access, clean up effects, and keep rendering deterministic.
|
|
8
|
+
|
|
9
|
+
## Theme and CSS
|
|
10
|
+
|
|
11
|
+
Use the theme contract supplied by the host. Inspect the active theme and the
|
|
12
|
+
installed theme API for the exact semantic variables; do not copy a partial
|
|
13
|
+
token list into widget guidance.
|
|
14
|
+
|
|
15
|
+
Theme compliance is the default, not an optional polish step. A widget should
|
|
16
|
+
inherit the active theme for colors, typography, spacing, radii, borders,
|
|
17
|
+
focus, and charts. Do not hardcode a visual value or expose a custom styling
|
|
18
|
+
property when a semantic token represents the same intent. Use the
|
|
19
|
+
`colorSelect` property field for author-selectable semantic colors. The legacy
|
|
20
|
+
`color` field is deprecated. Use arbitrary colors only for a product-specific
|
|
21
|
+
value that cannot be semantic, and verify contrast in every supported theme.
|
|
22
|
+
|
|
23
|
+
Keep styles in the generated project's root `styles.css`. The standalone
|
|
24
|
+
template keeps a guarded import in its worker entry, and both widget build
|
|
25
|
+
flows include the root stylesheet in their output. Scope selectors to the
|
|
26
|
+
widget and do not depend on global `body` styles.
|
|
27
|
+
|
|
28
|
+
## Security and network access
|
|
29
|
+
|
|
30
|
+
Follow the worker isolation boundary in the installed runtime contract. Do not
|
|
31
|
+
assume that browser or host application facilities are available. Declare every
|
|
32
|
+
portal function and capability in `uses`.
|
|
33
|
+
|
|
34
|
+
Add `networkAccess` only for public HTTP APIs after package review and
|
|
35
|
+
placement consent. The grant is bound to the package ID, package version, and
|
|
36
|
+
capability version. Fluid does not add credentials, cookies, tokens, or
|
|
37
|
+
headers. The widget's `RequestInit`, including credential mode, remains in
|
|
38
|
+
effect.
|
|
39
|
+
|
|
40
|
+
Treat worker network access as approved public HTTP fetch, not as general
|
|
41
|
+
browser or server access. The runtime reference owns the exact destination and
|
|
42
|
+
ambient-API policy. Native redirects and DNS results are not revalidated.
|
|
43
|
+
Never embed secrets, credentials, private endpoints, or tenant tokens. Use a
|
|
44
|
+
typed host portal function or server application instead.
|