@colixsystems/widget-sdk 0.38.0 → 0.39.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/dist/hooks.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // four-client mapping is obvious at a glance:
13
13
  // - CORE — WidgetContext + non-data hooks (no domain client)
14
14
  // - DATASTORE — ctx.datastore (@colixsystems/datastore-client)
15
- // - FILES — ctx.files (@colixsystems/files-client)
15
+ // - ASSETS — ctx.assets (@colixsystems/assets-client)
16
16
  // - DIRECTORY — ctx.directory (@colixsystems/directory-client)
17
17
  // - PAYMENTS — ctx.payments (@colixsystems/payments-client)
18
18
  //
@@ -1057,59 +1057,60 @@ export function useDatastoreSubscription(table, handlers, options) {
1057
1057
  }
1058
1058
 
1059
1059
  /* ============================================================================
1060
- * FILES CLIENT — ctx.files (@colixsystems/files-client)
1060
+ * ASSETS CLIENT — ctx.assets (@colixsystems/assets-client)
1061
1061
  *
1062
- * Files, folders, shares. Covers: useFile.
1062
+ * The Asset Manager: resolve a tenant asset (image / audio / video / doc) by
1063
+ * id to a browser-usable URL. Covers: useAsset.
1063
1064
  * ==========================================================================*/
1064
1065
 
1065
1066
  /**
1066
- * Stateful file-asset resolver hook. Returns { url, file, loading, error,
1067
+ * Stateful asset resolver hook. Returns { url, asset, loading, error,
1067
1068
  * refetch }.
1068
1069
  *
1069
- * The host injects a `@colixsystems/files-client` instance at `ctx.files`,
1070
- * FLATTENED so file ops are top-level. We call `ctx.files.get(fileId)`,
1071
- * which resolves to `{ url, ...meta }` — the returned file already carries
1070
+ * The host injects a `@colixsystems/assets-client` instance at `ctx.assets`,
1071
+ * FLATTENED so asset ops are top-level. We call `ctx.assets.get(assetId)`,
1072
+ * which resolves to `{ url, ...meta }` — the returned asset already carries
1072
1073
  * an absolute URL so the widget can drop it straight into an `<Image
1073
1074
  * source>` without knowing where the API lives. A missing/soft-deleted
1074
1075
  * asset surfaces as `{ url: null, error: <DatastoreError NOT_FOUND> }`.
1075
1076
  *
1076
- * When `fileId` is falsy the hook collapses to { url: null, file: null,
1077
+ * When `assetId` is falsy the hook collapses to { url: null, asset: null,
1077
1078
  * loading: false, error: null, refetch } without a network round-trip,
1078
1079
  * so a widget rendering before the author has bound an asset stays
1079
1080
  * loop-free.
1080
1081
  */
1081
- export function useFile(fileId) {
1082
- const ctx = useWidgetContextOrThrow("useFile");
1083
- if (!ctx.files || typeof ctx.files.get !== "function") {
1084
- throw new Error("useFile: host did not inject a files client");
1082
+ export function useAsset(assetId) {
1083
+ const ctx = useWidgetContextOrThrow("useAsset");
1084
+ if (!ctx.assets || typeof ctx.assets.get !== "function") {
1085
+ throw new Error("useAsset: host did not inject an assets client");
1085
1086
  }
1086
- const ready = Boolean(fileId);
1087
- const [file, setFile] = useState(null);
1087
+ const ready = Boolean(assetId);
1088
+ const [asset, setAsset] = useState(null);
1088
1089
  const [loading, setLoading] = useState(ready);
1089
1090
  const [error, setError] = useState(null);
1090
1091
 
1091
- const fileIdRef = useRef(fileId);
1092
- const getRef = useRef(ctx.files.get);
1093
- fileIdRef.current = fileId;
1094
- getRef.current = ctx.files.get;
1092
+ const assetIdRef = useRef(assetId);
1093
+ const getRef = useRef(ctx.assets.get);
1094
+ assetIdRef.current = assetId;
1095
+ getRef.current = ctx.assets.get;
1095
1096
 
1096
1097
  const runRef = useRef(0);
1097
1098
 
1098
1099
  const doFetch = useCallback(async () => {
1099
1100
  const myRun = ++runRef.current;
1100
- const id = fileIdRef.current;
1101
+ const id = assetIdRef.current;
1101
1102
  if (!id) {
1102
1103
  setLoading(false);
1103
1104
  setError(null);
1104
- setFile(null);
1105
+ setAsset(null);
1105
1106
  return;
1106
1107
  }
1107
1108
  setLoading(true);
1108
1109
  setError(null);
1109
1110
  try {
1110
- const f = await getRef.current(id);
1111
+ const a = await getRef.current(id);
1111
1112
  if (runRef.current !== myRun) return;
1112
- setFile(f || null);
1113
+ setAsset(a || null);
1113
1114
  setLoading(false);
1114
1115
  } catch (err) {
1115
1116
  if (runRef.current !== myRun) return;
@@ -1121,17 +1122,17 @@ export function useFile(fileId) {
1121
1122
  useEffect(() => {
1122
1123
  doFetch();
1123
1124
  // eslint-disable-next-line react-hooks/exhaustive-deps
1124
- }, [fileId]);
1125
+ }, [assetId]);
1125
1126
 
1126
1127
  const refetch = useCallback(async () => {
1127
1128
  await doFetch();
1128
1129
  }, [doFetch]);
1129
1130
 
1130
1131
  const url =
1131
- file && typeof file.url === "string" && file.url.length > 0
1132
- ? file.url
1132
+ asset && typeof asset.url === "string" && asset.url.length > 0
1133
+ ? asset.url
1133
1134
  : null;
1134
- return { url, file, loading, error, refetch };
1135
+ return { url, asset, loading, error, refetch };
1135
1136
  }
1136
1137
 
1137
1138
  /* ============================================================================
package/dist/index.d.ts CHANGED
@@ -375,13 +375,13 @@ export interface DirectoryClient {
375
375
  }
376
376
 
377
377
  /**
378
- * Structural shape of the injected Asset-Manager files client (`ctx.files`).
378
+ * Structural shape of the injected Asset-Manager client (`ctx.assets`).
379
379
  * Slimmed to the three top-level ops the host injects — `get` / `list` /
380
- * `upload`. Backs `useFile`, which calls `ctx.files.get(id)`. The returned
381
- * file carries an absolute `url` safe to drop into `<Image source>`.
380
+ * `upload`. Backs `useAsset`, which calls `ctx.assets.get(id)`. The returned
381
+ * asset carries an absolute `url` safe to drop into `<Image source>`.
382
382
  */
383
- export interface FilesClient {
384
- get(fileId: string): Promise<unknown>;
383
+ export interface AssetsClient {
384
+ get(assetId: string): Promise<unknown>;
385
385
  list(query?: Record<string, unknown>): Promise<ListEnvelope>;
386
386
  upload(formData: unknown): Promise<unknown>;
387
387
  }
@@ -431,8 +431,8 @@ export interface WidgetContext<TProps = unknown> {
431
431
  datastore: DatastoreClient;
432
432
  /** Injected @colixsystems/directory-client (users + groups + invites). */
433
433
  directory: DirectoryClient;
434
- /** Injected Asset-Manager files client: { get, list, upload }. */
435
- files: FilesClient;
434
+ /** Injected @colixsystems/assets-client: { get, list, upload }. */
435
+ assets: AssetsClient;
436
436
  /** Injected @colixsystems/payments-client. */
437
437
  payments: PaymentsClient;
438
438
  /** Host child-node renderer; backs WidgetTree / useChildRenderer. */
@@ -457,7 +457,7 @@ export interface WidgetContext<TProps = unknown> {
457
457
  /**
458
458
  * The widget's component receives the author-authored props **as React
459
459
  * props** — the same shape and calling convention every built-in widget
460
- * uses (`<MetricWidget tableId={...} sumField={...} />`). Everything
460
+ * uses (`<ChartWidget tableId={...} groupByField={...} />`). Everything
461
461
  * else from `WidgetContext` (datastore, user, workspace, navigation,
462
462
  * i18n, …) is reachable via the SDK hooks (`useDatastoreMutation`,
463
463
  * `useDatastoreQuery`, `useTheme`, `useI18n`, `useWidgetEvent`), which
@@ -742,9 +742,9 @@ export function useChildRenderer(): {
742
742
  */
743
743
  export const WidgetTree: (props: { node: unknown }) => unknown;
744
744
 
745
- export function useFile(fileId: string | null | undefined): {
745
+ export function useAsset(assetId: string | null | undefined): {
746
746
  url: string | null;
747
- file: {
747
+ asset: {
748
748
  id: string;
749
749
  url: string;
750
750
  stored_filename?: string;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export {
14
14
  useDatastoreQuery,
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
- useFile,
17
+ useAsset,
18
18
  useFilestoreFiles,
19
19
  useFilestoreFolders,
20
20
  useFileSignature,
@@ -14,7 +14,7 @@ export {
14
14
  useDatastoreQuery,
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
- useFile,
17
+ useAsset,
18
18
  useFilestoreFiles,
19
19
  useFilestoreFolders,
20
20
  useFileSignature,
@@ -0,0 +1,125 @@
1
+ // REQ-WSDK-PLATFORM bundle (sc-1064): the SINGLE web-entry bundler shared by the
2
+ // publish packer (`scripts/pack-first-party-widget.mjs`) and the dev server
3
+ // (`devserver.js`). Both paths feed a widget's WEB entry through this so what
4
+ // you `dev` is byte-shape-identical to what ships (CLAUDE.md §3, one source).
5
+ //
6
+ // Why a bundler at all (vs. the sucrase transpile the native entry still uses):
7
+ // the Studio's runtime loader resolves bare specifiers only for the host's OWN
8
+ // shimmed set (`dev-shims.hostExternalSpecifiers()` — react family, react-dom,
9
+ // the SDK, and a few vetted packages). A transpile-only web entry leaves
10
+ // `import … from "react-leaflet"` / `"leaflet"` / `"leaflet/dist/leaflet.css"`
11
+ // / the marker PNGs bare, and the loader's `findUnresolvedBareImports` guard
12
+ // rejects the bundle ("unresolvable bare imports: react-leaflet, leaflet, …").
13
+ // esbuild inlines all of those while leaving the host-resolved set external.
14
+ //
15
+ // What's externalised vs. inlined:
16
+ // * external = `hostExternalSpecifiers()` — the host injects its OWN React,
17
+ // react-dom, SDK, and vetted packages at runtime; inlining a second copy of
18
+ // React/react-dom would break the shared-instance invariant (mismatched
19
+ // renderer). These stay as bare `import`s the loader rewrites to blob shims.
20
+ // * inlined = everything else: `react-leaflet`, `leaflet`, the marker PNGs
21
+ // (→ data URLs), the `leaflet/dist/leaflet.css` side-effect import (→ a JS
22
+ // module that injects a <style> at runtime).
23
+ //
24
+ // esbuild is a LAZY, optional dependency — imported only when this module's
25
+ // `bundleWebEntry` runs (the `dev`/pack paths), never by the published SDK
26
+ // runtime. Same pattern as `loadTransform()`'s `sucrase` import in devserver.js.
27
+
28
+ import { hostExternalSpecifiers } from "./dev-shims.js";
29
+
30
+ /**
31
+ * Lazily resolve esbuild's `build`. Kept out of the module's static imports so
32
+ * the published SDK runtime never forces the dependency — only the dev/pack
33
+ * bundle paths reach it. Throws a friendly install hint when it's absent
34
+ * (mirrors `loadTransform`'s sucrase handling).
35
+ *
36
+ * @returns {Promise<typeof import("esbuild")>}
37
+ */
38
+ export async function loadEsbuild() {
39
+ try {
40
+ return await import("esbuild");
41
+ } catch {
42
+ throw new Error(
43
+ "Bundling a widget's web entry needs the optional `esbuild` dependency.\n" +
44
+ "Install it in your widget project:\n" +
45
+ " npm install --save-dev esbuild",
46
+ );
47
+ }
48
+ }
49
+
50
+ // esbuild plugin: resolve `import "….css"` (a side-effect import) to a tiny JS
51
+ // module that injects the CSS through a runtime <style> element. The output
52
+ // bundle then carries NO `.css` import, and the styling still applies the
53
+ // moment the module is evaluated in the browser. Guarded by
54
+ // `typeof document !== "undefined"` so the same module is a no-op under SSR /
55
+ // the native runtime (which never loads the web entry, but belt-and-braces).
56
+ function cssInjectPlugin() {
57
+ return {
58
+ name: "appstudio-css-inject",
59
+ setup(build) {
60
+ build.onLoad({ filter: /\.css$/ }, async (args) => {
61
+ const { readFile } = await import("node:fs/promises");
62
+ const css = await readFile(args.path, "utf8");
63
+ const contents =
64
+ "if (typeof document !== \"undefined\") {\n" +
65
+ " const __s = document.createElement(\"style\");\n" +
66
+ " __s.setAttribute(\"data-appstudio-widget-css\", \"1\");\n" +
67
+ ` __s.textContent = ${JSON.stringify(css)};\n` +
68
+ " document.head.appendChild(__s);\n" +
69
+ "}\n" +
70
+ "export default undefined;\n";
71
+ return { contents, loader: "js" };
72
+ });
73
+ },
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Bundle a widget's WEB entry into a single ESM string with the host-resolved
79
+ * specifiers left external. JSX is transformed with the automatic runtime (so
80
+ * the loader's react/jsx-runtime shims resolve it), `.png` assets become data
81
+ * URLs, and `.css` side-effect imports become runtime <style> injectors.
82
+ *
83
+ * @param {object} opts
84
+ * @param {string} opts.entryPath absolute path to the web entry (e.g. the
85
+ * resolved `MapWidget.web.jsx`).
86
+ * @param {string[]} [opts.external] bare specifiers to leave external. Defaults
87
+ * to `hostExternalSpecifiers()` — the canonical host-resolved set.
88
+ * @param {string[]} [opts.nodePaths] extra `node_modules` roots esbuild should
89
+ * search when resolving the inlined deps (`react-leaflet`, `leaflet`, …). The
90
+ * packer passes the frontend's `node_modules` (where the vetted web packages
91
+ * are installed per the adding-vetted-packages skill) so the bundle resolves
92
+ * them no matter the invoking CWD.
93
+ * @returns {Promise<string>} the bundled ESM source.
94
+ */
95
+ export async function bundleWebEntry({
96
+ entryPath,
97
+ external = hostExternalSpecifiers(),
98
+ nodePaths = [],
99
+ }) {
100
+ const esbuild = await loadEsbuild();
101
+ const result = await esbuild.build({
102
+ entryPoints: [entryPath],
103
+ bundle: true,
104
+ write: false,
105
+ format: "esm",
106
+ platform: "browser",
107
+ target: ["es2020"],
108
+ jsx: "automatic",
109
+ // Leave the host-resolved set as bare `import`s; the Studio loader rewrites
110
+ // them to blob shims at runtime (and Metro resolves them in the export).
111
+ external,
112
+ loader: { ".png": "dataurl", ".jpg": "dataurl", ".gif": "dataurl" },
113
+ plugins: [cssInjectPlugin()],
114
+ // esbuild walks up from the entry for node_modules by default; `nodePaths`
115
+ // adds extra roots so a packer invoked from the repo root still resolves the
116
+ // vetted web packages installed under frontend/node_modules.
117
+ nodePaths,
118
+ logLevel: "silent",
119
+ });
120
+ const out = result.outputFiles?.[0];
121
+ if (!out) {
122
+ throw new Error(`esbuild produced no output for ${entryPath}`);
123
+ }
124
+ return out.text;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,10 @@
18
18
  "import": "./dist/linter.js",
19
19
  "default": "./dist/linter.js"
20
20
  },
21
+ "./dev-shims": {
22
+ "import": "./dist/dev-shims.js",
23
+ "default": "./dist/dev-shims.js"
24
+ },
21
25
  "./contract": {
22
26
  "types": "./dist/index.d.ts",
23
27
  "require": "./dist/contract.cjs",
@@ -35,7 +39,7 @@
35
39
  ],
36
40
  "scripts": {
37
41
  "build": "node scripts/build.js",
38
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
42
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
39
43
  },
40
44
  "engines": {
41
45
  "node": ">=18"
@@ -66,6 +70,7 @@
66
70
  }
67
71
  },
68
72
  "optionalDependencies": {
73
+ "esbuild": "^0.28.0",
69
74
  "sucrase": "^3.35.0"
70
75
  },
71
76
  "keywords": [