@softr/sdk 0.0.54-alpha.0 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/action.d.ts +1 -31
  2. package/dist/action.js +1 -26
  3. package/dist/auth/isomorphic.d.ts +1 -1
  4. package/dist/auth/isomorphic.js +2 -1
  5. package/dist/boot-error.js +1 -0
  6. package/dist/client.d.ts +1 -2
  7. package/dist/client.js +1 -2
  8. package/dist/default-error-overlay.d.ts +2 -2
  9. package/dist/default-error-overlay.js +86 -2
  10. package/dist/fetch.js +2 -2
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +3 -2
  13. package/dist/mcp.d.ts +2 -2
  14. package/dist/package.js +1 -1
  15. package/dist/pwa.d.ts +54 -0
  16. package/dist/pwa.js +152 -0
  17. package/dist/server.d.ts +1 -2
  18. package/dist/server.js +1 -2
  19. package/package.json +1 -3
  20. package/skills/softr-sdk/SKILL.md +7 -7
  21. package/skills/softr-sdk/auth/SKILL.md +1 -1
  22. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  23. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  24. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  25. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  26. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  27. package/skills/softr-sdk/pwa/SKILL.md +77 -0
  28. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  29. package/skills/softr-sdk/record-queries/SKILL.md +2 -2
  30. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  31. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  32. package/skills/softr-sdk/workflows/SKILL.md +1 -1
  33. package/dist/comments/comments.d.ts +0 -137
  34. package/dist/comments/comments.js +0 -329
  35. package/dist/comments/use-comment-composer.d.ts +0 -37
  36. package/dist/comments/use-comment-composer.js +0 -127
  37. package/skills/softr-sdk/comments/SKILL.md +0 -149
package/dist/action.d.ts CHANGED
@@ -234,35 +234,5 @@ type IntegrationDefinition<TId extends string> = {
234
234
  * const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
235
235
  */
236
236
  declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
237
- type CommentsResource = {
238
- type: "COMMENTS";
239
- datasourceId: SoftrDatasourceId;
240
- };
241
- /** Comment threads on the records of one datasource. It has no `permissions`. */
242
- type CommentsAction<TId extends string = string> = {
243
- id: TId;
244
- resource: CommentsResource;
245
- };
246
- type CommentsDefinition<TId extends string> = {
247
- id: TId;
248
- datasourceId: SoftrDatasourceId;
249
- };
250
- /**
251
- * Declares comment threads on the records of a datasource. Each record has one thread.
252
- *
253
- * Who may read, post and delete comes from the app's comment settings in Softr, not from code. Two
254
- * declarations on the same datasource share the same threads.
255
- *
256
- * Pass the returned object to `comments.*` from `@softr/sdk/server`.
257
- *
258
- * @example
259
- * const orderComments = defineComments({
260
- * id: "orderComments",
261
- * datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
262
- * })
263
- *
264
- * const thread = await comments.list(orderComments, { recordId })
265
- */
266
- declare function defineComments<const TId extends string>(definition: CommentsDefinition<TId>): CommentsAction<TId>;
267
237
  //#endregion
268
- export { ActionData, ActionPermissions, ActionRecord, ActionShape, CommentsAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow };
238
+ export { ActionData, ActionPermissions, ActionRecord, ActionShape, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
package/dist/action.js CHANGED
@@ -50,31 +50,6 @@ function defineIntegration(definition) {
50
50
  permissions
51
51
  };
52
52
  }
53
- /**
54
- * Declares comment threads on the records of a datasource. Each record has one thread.
55
- *
56
- * Who may read, post and delete comes from the app's comment settings in Softr, not from code. Two
57
- * declarations on the same datasource share the same threads.
58
- *
59
- * Pass the returned object to `comments.*` from `@softr/sdk/server`.
60
- *
61
- * @example
62
- * const orderComments = defineComments({
63
- * id: "orderComments",
64
- * datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
65
- * })
66
- *
67
- * const thread = await comments.list(orderComments, { recordId })
68
- */
69
- function defineComments(definition) {
70
- return {
71
- id: definition.id,
72
- resource: {
73
- type: "COMMENTS",
74
- datasourceId: definition.datasourceId
75
- }
76
- };
77
- }
78
53
  function isDatasourceAction(value) {
79
54
  if (typeof value !== "object" || value === null) return false;
80
55
  const candidate = value;
@@ -83,4 +58,4 @@ function isDatasourceAction(value) {
83
58
  }
84
59
 
85
60
  //#endregion
86
- export { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
61
+ export { defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
@@ -17,7 +17,7 @@ import * as _tanstack_react_start0 from "@tanstack/react-start";
17
17
  * requestMiddleware: [softrAuthMiddleware()],
18
18
  * }))
19
19
  */
20
- declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
20
+ declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
21
21
  user: User | null;
22
22
  permissions: Permissions;
23
23
  }>;
@@ -4,6 +4,7 @@ import { sessionConfig } from "./config.js";
4
4
  import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
5
5
  import { makePlatform } from "./platform.js";
6
6
  import { bootErrorMiddleware } from "../boot-error.js";
7
+ import { pwaMiddleware } from "../pwa.js";
7
8
  import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
8
9
  import { rootRouteId, useRouteContext } from "@tanstack/react-router";
9
10
 
@@ -21,7 +22,7 @@ import { rootRouteId, useRouteContext } from "@tanstack/react-router";
21
22
  * }))
22
23
  */
23
24
  function softrAuthMiddleware() {
24
- return createMiddleware({ type: "request" }).middleware([bootErrorMiddleware()]).server(async ({ next }) => {
25
+ return createMiddleware({ type: "request" }).middleware([bootErrorMiddleware(), pwaMiddleware()]).server(async ({ next }) => {
25
26
  async function getSession() {
26
27
  const token = readSessionToken();
27
28
  const cached = readSessionData(token);
@@ -16,6 +16,7 @@ function bootErrorMiddleware() {
16
16
  return await next();
17
17
  } catch (error) {
18
18
  if (handlerType === "serverFn" || error instanceof Response || isRedirect(error) || isNotFound(error)) throw error;
19
+ console.error("[@softr/sdk] boot error:", error);
19
20
  return new Response(errorPage(error), {
20
21
  status: 500,
21
22
  headers: { "content-type": "text/html; charset=utf-8" }
package/dist/client.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
2
- import { useCommentComposer } from "./comments/use-comment-composer.js";
3
2
  import "@tanstack/react-start/client-only";
4
- export { type FileItem, useCommentComposer, useFileUpload };
3
+ export { type FileItem, useFileUpload };
package/dist/client.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { useFileUpload } from "./core/use-file-upload.js";
2
- import { useCommentComposer } from "./comments/use-comment-composer.js";
3
2
  import "@tanstack/react-start/client-only";
4
3
 
5
- export { useCommentComposer, useFileUpload };
4
+ export { useFileUpload };
@@ -1,4 +1,4 @@
1
- import * as react0 from "react";
1
+ import * as react1 from "react";
2
2
 
3
3
  //#region src/default-error-overlay.d.ts
4
4
  type Props = {
@@ -8,6 +8,6 @@ type Props = {
8
8
  declare function DefaultErrorOverlay({
9
9
  error,
10
10
  reset
11
- }: Props): react0.JSX.Element;
11
+ }: Props): react1.JSX.Element;
12
12
  //#endregion
13
13
  export { DefaultErrorOverlay };
@@ -1,10 +1,25 @@
1
- import { useRef, useState } from "react";
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
2
+ import { useRouter } from "@tanstack/react-router";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
 
4
5
  //#region src/default-error-overlay.tsx
6
+ /** Set by Studio's canvas engine, which ships apart from the SDK each app pins, so renaming it breaks older apps. */
7
+ const AGENT_TURN_ATTRIBUTE = "data-agent-turn";
8
+ function subscribeToAgentTurn(listener) {
9
+ const observer = new MutationObserver(listener);
10
+ observer.observe(document.documentElement, {
11
+ attributes: true,
12
+ attributeFilter: [AGENT_TURN_ATTRIBUTE]
13
+ });
14
+ return () => observer.disconnect();
15
+ }
16
+ function isAgentTurnRunning() {
17
+ return document.documentElement.getAttribute(AGENT_TURN_ATTRIBUTE) === "running";
18
+ }
5
19
  function DefaultErrorOverlay({ error, reset }) {
6
20
  if (process.env.NODE_ENV === "production") return /* @__PURE__ */ jsx("div", {
7
21
  style: s.overlay,
22
+ "data-softr-error-overlay": "error",
8
23
  children: /* @__PURE__ */ jsxs("div", {
9
24
  style: s.card,
10
25
  children: [
@@ -26,8 +41,33 @@ function DefaultErrorOverlay({ error, reset }) {
26
41
  });
27
42
  }
28
43
  function DevView({ error, reset }) {
44
+ const running = useSyncExternalStore(subscribeToAgentTurn, isAgentTurnRunning, () => false);
45
+ const [retryPending, setRetryPending] = useState(running);
46
+ const router = useRouter();
47
+ useEffect(() => {
48
+ if (running) {
49
+ setRetryPending(true);
50
+ return;
51
+ }
52
+ if (!retryPending) return;
53
+ const showError = () => setRetryPending(false);
54
+ const timer = setTimeout(() => {
55
+ router.invalidate().then(() => {
56
+ reset?.();
57
+ showError();
58
+ }, showError);
59
+ }, 1e3);
60
+ return () => clearTimeout(timer);
61
+ }, [
62
+ running,
63
+ retryPending,
64
+ reset,
65
+ router
66
+ ]);
67
+ if (running || retryPending) return /* @__PURE__ */ jsx(UpdatingView, {});
29
68
  return /* @__PURE__ */ jsx("div", {
30
69
  style: s.overlay,
70
+ "data-softr-error-overlay": "error",
31
71
  children: /* @__PURE__ */ jsxs("div", {
32
72
  style: s.card,
33
73
  children: [
@@ -56,6 +96,39 @@ function DevView({ error, reset }) {
56
96
  })
57
97
  });
58
98
  }
99
+ function UpdatingView() {
100
+ return /* @__PURE__ */ jsx("div", {
101
+ style: s.overlay,
102
+ "data-softr-error-overlay": "updating",
103
+ children: /* @__PURE__ */ jsxs("div", {
104
+ style: s.card,
105
+ children: [/* @__PURE__ */ jsx("div", {
106
+ style: s.pending,
107
+ children: /* @__PURE__ */ jsxs("svg", {
108
+ style: icon.md,
109
+ viewBox: "0 0 24 24",
110
+ fill: "none",
111
+ stroke: "currentColor",
112
+ strokeWidth: "2",
113
+ strokeLinecap: "round",
114
+ strokeLinejoin: "round",
115
+ children: [
116
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
117
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" }),
118
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
119
+ /* @__PURE__ */ jsx("path", { d: "M8 16H3v5" })
120
+ ]
121
+ })
122
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
123
+ style: s.heading,
124
+ children: "Updating the app…"
125
+ }), /* @__PURE__ */ jsx("p", {
126
+ style: s.body,
127
+ children: "The agent is still working on this change. The preview will recover once it is complete."
128
+ })] })]
129
+ })
130
+ });
131
+ }
59
132
  function cleanStack(stack) {
60
133
  return stack.split("\n").slice(1).filter((line) => line.trim().startsWith("at ") && !line.includes("/node_modules/")).map((line) => line.replace(/https?:\/\/[^/]+\//, "").replace(/\?[^):]+/g, "")).join("\n");
61
134
  }
@@ -228,6 +301,17 @@ const s = {
228
301
  color: "var(--destructive, oklch(0.577 0.245 27.325))",
229
302
  flexShrink: 0
230
303
  },
304
+ pending: {
305
+ display: "flex",
306
+ alignItems: "center",
307
+ justifyContent: "center",
308
+ width: "2.5rem",
309
+ height: "2.5rem",
310
+ borderRadius: "var(--radius, 0.625rem)",
311
+ background: "var(--muted, oklch(0.97 0 0))",
312
+ color: "var(--muted-foreground, oklch(0.556 0 0))",
313
+ flexShrink: 0
314
+ },
231
315
  heading: {
232
316
  margin: 0,
233
317
  fontSize: "1rem",
@@ -267,7 +351,7 @@ const s = {
267
351
  border: "1px solid var(--input, oklch(0.922 0 0))",
268
352
  borderRadius: "var(--radius, 0.625rem)",
269
353
  background: "var(--input-fill, oklch(1 0 0))",
270
- color: "var(--primary-foreground, oklch(0.985 0 0))",
354
+ color: "var(--foreground, oklch(0.145 0 0))",
271
355
  fontFamily: "inherit",
272
356
  fontSize: "0.875rem",
273
357
  fontWeight: 500,
package/dist/fetch.js CHANGED
@@ -8,11 +8,11 @@ const fetch = async (input, init) => {
8
8
  const started = Date.now();
9
9
  try {
10
10
  const response = await globalThis.fetch(input, init);
11
- logger.info(`${method} ${response.status} ${url} ${Date.now() - started}ms`);
11
+ if (!response.ok) logger.warn(`${method} ${response.status} ${url} ${Date.now() - started}ms`);
12
12
  return response;
13
13
  } catch (error) {
14
14
  const label = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
15
- logger.info(`${method} ERR ${url} ${label} in ${Date.now() - started}ms`);
15
+ logger.error(`${method} ERR ${url} ${label} in ${Date.now() - started}ms`);
16
16
  throw error;
17
17
  }
18
18
  };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
2
- import { ActionPermissions, SoftrDatasources, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { FileAttachment } from "./core/file-upload.js";
4
4
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
5
- import { CommentMention, CommentThread, CommentView } from "./comments/comments.js";
6
5
  import { SoftrContext } from "./auth/context.js";
7
6
  import { checkAccess, defineVisibility, isAuthorized } from "./visibility/visibility.js";
8
7
  import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
9
8
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
10
9
  import { softrMcpMiddleware } from "./mcp.js";
11
10
  import { useUserTimezone } from "./timezone.js";
11
+ import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
12
12
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
13
13
  import { Audience } from "./visibility/audience.js";
14
- export { type ActionPermissions, Audience, type CommentMention, type CommentThread, type CommentView, DefaultErrorOverlay, type FileAttachment, type FileItem, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, useUserTimezone };
14
+ export { type ActionPermissions, Audience, DefaultErrorOverlay, type FileAttachment, type FileItem, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, usePwaInstall, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { useFileUpload } from "./core/use-file-upload.js";
2
- import { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { checkAccess, defineVisibility, isAuthorized } from "./visibility/visibility.js";
4
4
  import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
5
+ import { SoftrPwaHead, usePwaInstall } from "./pwa.js";
5
6
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
6
7
  import { softrMcpMiddleware } from "./mcp.js";
7
8
  import { useUserTimezone } from "./timezone.js";
8
9
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
9
10
  import { Audience } from "./visibility/audience.js";
10
11
 
11
- export { Audience, DefaultErrorOverlay, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, useUserTimezone };
12
+ export { Audience, DefaultErrorOverlay, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, usePwaInstall, useUserTimezone };
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatasourceAction } from "./action.js";
2
- import * as _tanstack_react_start2 from "@tanstack/react-start";
2
+ import * as _tanstack_react_start0 from "@tanstack/react-start";
3
3
 
4
4
  //#region src/mcp.d.ts
5
5
 
@@ -37,6 +37,6 @@ type SoftrMcpOptions = {
37
37
  * ],
38
38
  * }))
39
39
  */
40
- declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start2.RequestMiddlewareAfterServer<{}, undefined, undefined>;
40
+ declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
41
41
  //#endregion
42
42
  export { softrMcpMiddleware };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.54-alpha.0";
2
+ var version = "0.0.55";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import * as react0 from "react";
2
+ import "@tanstack/react-start";
3
+
4
+ //#region src/pwa.d.ts
5
+
6
+ /**
7
+ * The links that make the app installable: its manifest and its home screen
8
+ * icon. Render it once, in `<head>` in the root route's `RootDocument`.
9
+ *
10
+ * @example
11
+ * function RootDocument({ children }: { children: React.ReactNode }) {
12
+ * return (
13
+ * <html lang="en">
14
+ * <head>
15
+ * <HeadContent />
16
+ * <SoftrPwaHead />
17
+ * </head>
18
+ * <body>
19
+ * {children}
20
+ * <Scripts />
21
+ * </body>
22
+ * </html>
23
+ * )
24
+ * }
25
+ */
26
+ declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
27
+ type PwaPlatform = "ios" | "android" | "desktop";
28
+ type PwaInstall = {
29
+ available: false;
30
+ } | {
31
+ available: true;
32
+ /** Which steps install the app on this device. */
33
+ platform: PwaPlatform;
34
+ /** The name the home screen will show. */
35
+ name: string;
36
+ /** The icon the home screen will show. */
37
+ iconUrl: string;
38
+ };
39
+ /**
40
+ * Whether to offer installing the app, and on which platform. Render an
41
+ * install button only while this says `available`: it is unavailable while
42
+ * loading, when the builder has not turned the PWA on, when Softr has switched
43
+ * it off, and when the app already runs installed.
44
+ *
45
+ * @example
46
+ * function InstallButton() {
47
+ * const install = usePwaInstall()
48
+ * if (!install.available) return null
49
+ * return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
50
+ * }
51
+ */
52
+ declare function usePwaInstall(): PwaInstall;
53
+ //#endregion
54
+ export { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall };
package/dist/pwa.js ADDED
@@ -0,0 +1,152 @@
1
+ import { logger } from "./logger.js";
2
+ import { fetch } from "./fetch.js";
3
+ import { sessionConfig } from "./auth/config.js";
4
+ import { platformApiUrl } from "./auth/platform.js";
5
+ import { Fragment, createElement, useEffect, useState } from "react";
6
+ import { createMiddleware } from "@tanstack/react-start";
7
+ import { z } from "zod";
8
+
9
+ //#region src/pwa.ts
10
+ const MANIFEST_PATH = "/manifest.webmanifest";
11
+ const ICON_PATH = "/apple-touch-icon.png";
12
+ const INSTALL_PATH = "/_softr/pwa.json";
13
+ const TTL_MS = 6e4;
14
+ const CACHE_CONTROL = "public, max-age=60";
15
+ const pwaSchema = z.object({
16
+ manifest: z.record(z.string(), z.unknown()),
17
+ install: z.object({
18
+ name: z.string(),
19
+ iconUrl: z.string()
20
+ })
21
+ });
22
+ let cached;
23
+ /**
24
+ * The app's PWA, or `null` when it is not installable right now. Never
25
+ * rejects: a failure reads as not installable and is not remembered, so the
26
+ * next request tries again.
27
+ */
28
+ function readPwa() {
29
+ const now = Date.now();
30
+ if (cached !== void 0 && cached.expires > now) return cached.pwa;
31
+ const entry = {
32
+ expires: now + TTL_MS,
33
+ pwa: fetchPwa().catch((error) => {
34
+ logger.warn("Could not read the PWA settings:", error);
35
+ if (cached === entry) cached = void 0;
36
+ return null;
37
+ })
38
+ };
39
+ cached = entry;
40
+ return entry.pwa;
41
+ }
42
+ async function fetchPwa() {
43
+ const { applicationId } = sessionConfig();
44
+ const response = await fetch(`${await platformApiUrl()}/v1/vibe/applications/${applicationId}/pwa`, { headers: { accept: "application/json" } });
45
+ if (response.status === 404) return null;
46
+ if (!response.ok) throw new Error(`The platform answered ${response.status}`);
47
+ return pwaSchema.parse(await response.json());
48
+ }
49
+ /**
50
+ * Serves the app's web app manifest, its home screen icon, and what an install
51
+ * button needs to know, from the app's own origin. Every other request passes
52
+ * through untouched.
53
+ *
54
+ * Answers 404, never an error page, when the app is not installable: a browser
55
+ * silently drops a manifest it cannot parse, so a 500 there would be invisible.
56
+ */
57
+ function pwaMiddleware() {
58
+ return createMiddleware({ type: "request" }).server(async ({ next, pathname }) => {
59
+ if (pathname !== MANIFEST_PATH && pathname !== ICON_PATH && pathname !== INSTALL_PATH) return next();
60
+ const pwa = await readPwa();
61
+ if (pwa === null) return new Response(null, { status: 404 });
62
+ if (pathname === MANIFEST_PATH) return new Response(JSON.stringify(pwa.manifest), { headers: {
63
+ "content-type": "application/manifest+json",
64
+ "cache-control": CACHE_CONTROL
65
+ } });
66
+ if (pathname === INSTALL_PATH) return Response.json(pwa.install, { headers: { "cache-control": CACHE_CONTROL } });
67
+ try {
68
+ const iconResponse = await fetch(pwa.install.iconUrl);
69
+ if (!iconResponse.ok) return new Response(null, { status: 404 });
70
+ return new Response(iconResponse.body, { headers: {
71
+ "content-type": "image/png",
72
+ "cache-control": CACHE_CONTROL
73
+ } });
74
+ } catch (error) {
75
+ logger.warn("Could not fetch the PWA icon:", error);
76
+ return new Response(null, { status: 404 });
77
+ }
78
+ });
79
+ }
80
+ /**
81
+ * The links that make the app installable: its manifest and its home screen
82
+ * icon. Render it once, in `<head>` in the root route's `RootDocument`.
83
+ *
84
+ * @example
85
+ * function RootDocument({ children }: { children: React.ReactNode }) {
86
+ * return (
87
+ * <html lang="en">
88
+ * <head>
89
+ * <HeadContent />
90
+ * <SoftrPwaHead />
91
+ * </head>
92
+ * <body>
93
+ * {children}
94
+ * <Scripts />
95
+ * </body>
96
+ * </html>
97
+ * )
98
+ * }
99
+ */
100
+ function SoftrPwaHead() {
101
+ return createElement(Fragment, null, createElement("link", {
102
+ rel: "manifest",
103
+ href: MANIFEST_PATH
104
+ }), createElement("link", {
105
+ rel: "apple-touch-icon",
106
+ href: ICON_PATH
107
+ }));
108
+ }
109
+ const installSchema = pwaSchema.shape.install;
110
+ /**
111
+ * Whether to offer installing the app, and on which platform. Render an
112
+ * install button only while this says `available`: it is unavailable while
113
+ * loading, when the builder has not turned the PWA on, when Softr has switched
114
+ * it off, and when the app already runs installed.
115
+ *
116
+ * @example
117
+ * function InstallButton() {
118
+ * const install = usePwaInstall()
119
+ * if (!install.available) return null
120
+ * return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
121
+ * }
122
+ */
123
+ function usePwaInstall() {
124
+ const [install, setInstall] = useState({ available: false });
125
+ useEffect(() => {
126
+ if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in navigator && navigator.standalone === true) return;
127
+ let cancelled = false;
128
+ fetch(INSTALL_PATH, { headers: { accept: "application/json" } }).then(async (response) => {
129
+ if (!response.ok) return;
130
+ const parsed = installSchema.safeParse(await response.json());
131
+ if (!parsed.success || cancelled) return;
132
+ setInstall({
133
+ available: true,
134
+ platform: detectPlatform(),
135
+ ...parsed.data
136
+ });
137
+ }).catch(() => {});
138
+ return () => {
139
+ cancelled = true;
140
+ };
141
+ }, []);
142
+ return install;
143
+ }
144
+ function detectPlatform() {
145
+ const userAgent = navigator.userAgent;
146
+ if (/iPhone|iPad|iPod/.test(userAgent) || /Macintosh/.test(userAgent) && navigator.maxTouchPoints > 1) return "ios";
147
+ if (/Android/.test(userAgent)) return "android";
148
+ return "desktop";
149
+ }
150
+
151
+ //#endregion
152
+ export { SoftrPwaHead, pwaMiddleware, usePwaInstall };
package/dist/server.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { upload } from "./core/file-upload.js";
2
- import { comments } from "./comments/comments.js";
3
2
  import { auth } from "./auth/server.js";
4
3
  import { ds } from "./datasource/datasource.js";
5
4
  import { workflow } from "./core/workflow.js";
6
5
  import { integration } from "./core/integration.js";
7
6
  import "@tanstack/react-start/server-only";
8
- export { auth, comments, ds, integration, upload, workflow };
7
+ export { auth, ds, integration, upload, workflow };
package/dist/server.js CHANGED
@@ -3,7 +3,6 @@ import { ds } from "./datasource/datasource.js";
3
3
  import { auth } from "./auth/server.js";
4
4
  import { workflow } from "./core/workflow.js";
5
5
  import { integration } from "./core/integration.js";
6
- import { comments } from "./comments/comments.js";
7
6
  import "@tanstack/react-start/server-only";
8
7
 
9
- export { auth, comments, ds, integration, upload, workflow };
8
+ export { auth, ds, integration, upload, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.54-alpha.0",
3
+ "version": "0.0.55",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -17,8 +17,6 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "marked": "^18.0.14",
21
- "xss": "^1.0.15",
22
20
  "zod": "^4.4.3"
23
21
  },
24
22
  "devDependencies": {
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.52
14
+ library_version: 0.0.55
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -29,9 +29,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
29
29
 
30
30
  | Import | Runs | Holds |
31
31
  | --- | --- | --- |
32
- | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineComments`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
33
- | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `comments` |
34
- | `@softr/sdk/client` | Browser only | `useCommentComposer` |
32
+ | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
33
+ | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
34
+ | `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
35
35
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
36
36
 
37
37
  ## Sub-Skills
@@ -48,7 +48,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
48
48
  | Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
49
49
  | Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
50
50
  | Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
51
- | Add comments to a record | [comments/SKILL.md](./comments/SKILL.md) |
51
+ | Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
52
52
 
53
53
  ## Quick Decision Tree
54
54
 
@@ -83,8 +83,8 @@ Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)
83
83
  Opening the app to an AI assistant over MCP?
84
84
  load softr-sdk/mcp
85
85
 
86
- Letting visitors comment on a record?
87
- load softr-sdk/comments
86
+ Adding an install button, or making the app installable?
87
+ load softr-sdk/pwa
88
88
 
89
89
  Writing a server function, loader, or middleware in general?
90
90
  not a Softr concern. Use the TanStack Start skills.
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.52
19
+ library_version: 0.0.55
20
20
  ---
21
21
 
22
22
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.52
15
+ library_version: 0.0.55
16
16
  ---
17
17
 
18
18
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.52
12
+ library_version: 0.0.55
13
13
  ---
14
14
 
15
15
  # The current user