@tightknitai/block-kitchen 0.9.9 → 0.10.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/index.cjs CHANGED
@@ -2045,6 +2045,7 @@ function computeValidation(blocks, surface) {
2045
2045
  const grouped = groupValidatorErrors(result.errors, blocks);
2046
2046
  return {
2047
2047
  valid: result.valid,
2048
+ errors: result.errors,
2048
2049
  byBlockId: grouped.byBlockId,
2049
2050
  general: grouped.general,
2050
2051
  total: grouped.total
@@ -2255,6 +2256,29 @@ function useBlockKitchenState({
2255
2256
  replaceAll
2256
2257
  };
2257
2258
  }
2259
+ function useDevWarning(condition, message) {
2260
+ const warnedRef = React.useRef(false);
2261
+ React.useEffect(() => {
2262
+ if (condition && !warnedRef.current) {
2263
+ warnedRef.current = true;
2264
+ console.warn(message);
2265
+ }
2266
+ }, [condition, message]);
2267
+ }
2268
+ function useNotifyOnChange(value, toKey, callback) {
2269
+ const lastNotifiedRef = React.useRef(null);
2270
+ React.useEffect(() => {
2271
+ if (!callback) {
2272
+ return;
2273
+ }
2274
+ const key = toKey(value);
2275
+ if (lastNotifiedRef.current === key) {
2276
+ return;
2277
+ }
2278
+ lastNotifiedRef.current = key;
2279
+ callback(value);
2280
+ }, [value, toKey, callback]);
2281
+ }
2258
2282
 
2259
2283
  // src/lib/brand-theme.ts
2260
2284
  var TOKEN_TO_CSS_VAR = {
@@ -3052,7 +3076,7 @@ function LoadMessageDialog({
3052
3076
  loadRecentMessagesRef.current = loadRecentMessages;
3053
3077
  loadChannelsRef.current = loadChannels;
3054
3078
  });
3055
- const hasRecent = !!loadRecentMessages;
3079
+ const hasRecent = !!loadRecentMessages && !!loadChannels;
3056
3080
  React.useEffect(() => {
3057
3081
  if (!open) {
3058
3082
  return;
@@ -3063,7 +3087,10 @@ function LoadMessageDialog({
3063
3087
  setRecent(null);
3064
3088
  setRecentError(null);
3065
3089
  setSelectedRecent(null);
3066
- if (!loadRecentMessagesRef.current) {
3090
+ }, [open]);
3091
+ React.useEffect(() => {
3092
+ const load = loadChannelsRef.current;
3093
+ if (!open || !hasRecent || !load) {
3067
3094
  setChannels([]);
3068
3095
  setChannelsError(null);
3069
3096
  return;
@@ -3071,7 +3098,7 @@ function LoadMessageDialog({
3071
3098
  setChannels(null);
3072
3099
  setChannelsError(null);
3073
3100
  let cancelled = false;
3074
- loadChannelsRef.current().then((list) => {
3101
+ load().then((list) => {
3075
3102
  if (!cancelled) {
3076
3103
  setChannels(list);
3077
3104
  }
@@ -3083,7 +3110,7 @@ function LoadMessageDialog({
3083
3110
  return () => {
3084
3111
  cancelled = true;
3085
3112
  };
3086
- }, [open]);
3113
+ }, [open, hasRecent]);
3087
3114
  React.useEffect(() => {
3088
3115
  if (!open || !loadRecentMessagesRef.current || !channelId) {
3089
3116
  return;
@@ -3562,6 +3589,7 @@ function SendDialog({
3562
3589
  loadChannels,
3563
3590
  loadSendAsUserStatus,
3564
3591
  onSend,
3592
+ renderSendExtras,
3565
3593
  confirmSendLabel = "Send",
3566
3594
  errorCount,
3567
3595
  onShowIssues
@@ -3571,6 +3599,10 @@ function SendDialog({
3571
3599
  const [channelId, setChannelId] = React.useState("");
3572
3600
  const [sendAs, setSendAs] = React.useState("bot");
3573
3601
  const [status, setStatus] = React.useState({ kind: "idle" });
3602
+ const [extras, setExtrasState] = React.useState({});
3603
+ const setExtras = React.useCallback((patch) => {
3604
+ setExtrasState((prev) => ({ ...prev, ...patch }));
3605
+ }, []);
3574
3606
  const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: true });
3575
3607
  const loadChannelsRef = React.useRef(loadChannels);
3576
3608
  React.useEffect(() => {
@@ -3585,6 +3617,7 @@ function SendDialog({
3585
3617
  setChannelsError(null);
3586
3618
  setChannelId("");
3587
3619
  setSendAs("bot");
3620
+ setExtrasState({});
3588
3621
  let cancelled = false;
3589
3622
  loadChannelsRef.current().then((list) => {
3590
3623
  if (cancelled) {
@@ -3612,7 +3645,10 @@ function SendDialog({
3612
3645
  const result = await onSend({
3613
3646
  channelId,
3614
3647
  blocks: toSlackBlocks(blocks),
3615
- sendAsUser: sendAs === "user"
3648
+ sendAsUser: sendAs === "user",
3649
+ // Only introduce the key when the extras slot is wired, so existing
3650
+ // consumers never see a new payload field.
3651
+ ...renderSendExtras ? { extras } : {}
3616
3652
  });
3617
3653
  if (result.ok) {
3618
3654
  setStatus({ kind: "idle" });
@@ -3679,6 +3715,12 @@ function SendDialog({
3679
3715
  }
3680
3716
  )
3681
3717
  ] }),
3718
+ renderSendExtras ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1.5", children: renderSendExtras({
3719
+ channelId: channelId || null,
3720
+ sendAsUser: sendAs === "user",
3721
+ extras,
3722
+ setExtras
3723
+ }) }) : null,
3682
3724
  errorCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
3683
3725
  "button",
3684
3726
  {
@@ -9425,8 +9467,11 @@ function Toolbar2({
9425
9467
  showThemeControl = true,
9426
9468
  docsLink,
9427
9469
  errorCount,
9470
+ showSend = true,
9471
+ primaryAction,
9428
9472
  sendButtonLabel = "Review & send",
9429
- editingEnabled = false,
9473
+ loadEnabled = false,
9474
+ updateEnabled = false,
9430
9475
  editBadge,
9431
9476
  onOpenLoad,
9432
9477
  onOpenUpdate,
@@ -9446,7 +9491,7 @@ function Toolbar2({
9446
9491
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9447
9492
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1 border-b bg-background px-2 py-1.5 sm:px-3 sm:py-2", children: [
9448
9493
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
9449
- editingEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9494
+ loadEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9450
9495
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MailSearch, { className: "h-3.5 w-3.5" }),
9451
9496
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
9452
9497
  ] }) : null,
@@ -9559,9 +9604,29 @@ function Toolbar2({
9559
9604
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
9560
9605
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "View JSON" })
9561
9606
  ] }),
9562
- editBadge ? (
9563
- // A message is loaded: split button. Main action updates it in
9564
- // place; the menu also offers posting the blocks as a new message.
9607
+ !showSend ? primaryAction ? (
9608
+ // Compose-only mode with a host-owned primary action: same
9609
+ // placement and styling as the built-in send button. No icon —
9610
+ // the action's meaning is the host's, so the label always shows
9611
+ // (the send button can collapse to its icon on small screens;
9612
+ // an icon-less button can't).
9613
+ /* @__PURE__ */ jsxRuntime.jsx(
9614
+ Button,
9615
+ {
9616
+ type: "button",
9617
+ size: "sm",
9618
+ onClick: primaryAction.onClick,
9619
+ disabled: primaryAction.disabled,
9620
+ "aria-label": primaryAction.label,
9621
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryAction.label })
9622
+ }
9623
+ )
9624
+ ) : null : editBadge && updateEnabled ? (
9625
+ // A message is loaded and update-in-place is wired: split button.
9626
+ // Main action updates it in place; the menu also offers posting
9627
+ // the blocks as a new message. Without `updateEnabled` (loading
9628
+ // configured but no `editing.onUpdate`), the plain send button
9629
+ // below stays — the loaded message can only be posted as new.
9565
9630
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-stretch", children: [
9566
9631
  /* @__PURE__ */ jsxRuntime.jsxs(
9567
9632
  Button,
@@ -9634,7 +9699,7 @@ function Toolbar2({
9634
9699
  ] })
9635
9700
  ] })
9636
9701
  ] }),
9637
- editingEnabled && editBadge ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9702
+ loadEnabled && editBadge ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9638
9703
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4 shrink-0 text-primary" }),
9639
9704
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 text-sm", children: [
9640
9705
  "References an existing message in ",
@@ -9797,17 +9862,34 @@ function UpdateDialog({
9797
9862
  ] })
9798
9863
  ] }) });
9799
9864
  }
9865
+ function toLoadedMessage({
9866
+ ok: _ok,
9867
+ blocks: _blocks,
9868
+ ...loadedMessage
9869
+ }) {
9870
+ return loadedMessage;
9871
+ }
9872
+ function loadedMessageKey(message) {
9873
+ return message ? JSON.stringify(message) : null;
9874
+ }
9875
+ function validationSummaryKey(summary) {
9876
+ return JSON.stringify([summary.valid, summary.errorCount, ...summary.errors]);
9877
+ }
9800
9878
  function BlockKitchen(props) {
9801
9879
  const {
9802
9880
  workspaceName,
9803
9881
  initialBlocks,
9804
9882
  onChange,
9883
+ onValidationChange,
9805
9884
  previewHooks,
9806
9885
  customEmojis,
9807
9886
  loadChannels,
9808
9887
  loadSendAsUserStatus,
9809
9888
  onSend,
9810
- editing,
9889
+ renderSendExtras,
9890
+ loading,
9891
+ onUpdate: onUpdateProp,
9892
+ primaryAction,
9811
9893
  loadButtonLabel,
9812
9894
  updateButtonLabel,
9813
9895
  confirmUpdateLabel,
@@ -9824,6 +9906,13 @@ function BlockKitchen(props) {
9824
9906
  sendButtonLabel,
9825
9907
  confirmSendLabel,
9826
9908
  theme
9909
+ // Widen the all-or-nothing union so the branch-specific props
9910
+ // destructure as independent optionals: untyped JS consumers can still
9911
+ // pass partial wiring (handled by the runtime guards below), and the
9912
+ // correlated-union narrowing would otherwise flag those guards as
9913
+ // "always true". `primaryAction` is pulled from the compose-only branch
9914
+ // (where it carries a real type) rather than the send branch's
9915
+ // `undefined` pin, which would collapse the intersection to `undefined`.
9827
9916
  } = props;
9828
9917
  const paletteSections = React.useMemo(() => {
9829
9918
  const sections = palette ?? defaultPalette;
@@ -9842,14 +9931,40 @@ function BlockKitchen(props) {
9842
9931
  }, [palette, disabledBlockTypes]);
9843
9932
  const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
9844
9933
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
9845
- const seededBlocks = editing?.initialTarget?.blocks ?? initialBlocks;
9846
- React.useEffect(() => {
9847
- if (editing?.initialTarget && initialBlocks) {
9848
- console.warn(
9849
- "[BlockKitchen] Both `initialBlocks` and `editing.initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9850
- );
9851
- }
9852
- }, []);
9934
+ const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
9935
+ const loadEnabled = Boolean(loading);
9936
+ const recentChannelSource = loading?.loadChannels ?? (sendEnabled ? loadChannels : void 0);
9937
+ const onUpdate = sendEnabled ? onUpdateProp : void 0;
9938
+ const seededBlocks = loading?.initialTarget?.blocks ?? initialBlocks;
9939
+ const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
9940
+ useDevWarning(
9941
+ Boolean(loading?.initialTarget && initialBlocks),
9942
+ "[BlockKitchen] Both `initialBlocks` and an `initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9943
+ );
9944
+ useDevWarning(
9945
+ sendPropsProvided > 0 && sendPropsProvided < 3,
9946
+ "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
9947
+ );
9948
+ useDevWarning(
9949
+ Boolean(onUpdateProp && !sendEnabled),
9950
+ "[BlockKitchen] `onUpdate` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `onUpdate`. To load an existing message without the send integration, use the `loading` prop instead."
9951
+ );
9952
+ useDevWarning(
9953
+ Boolean(sendEnabled && onUpdateProp && !loading),
9954
+ "[BlockKitchen] `onUpdate` needs a loaded message, but no load source is configured \u2014 provide the `loading` prop. The update flow is unreachable."
9955
+ );
9956
+ useDevWarning(
9957
+ Boolean(loading?.loadRecentMessages && !recentChannelSource),
9958
+ "[BlockKitchen] `loading.loadRecentMessages` needs a channel list to scope its lookup \u2014 provide `loading.loadChannels` (compose-only mode has no send integration to reuse). Hiding the recent-messages picker."
9959
+ );
9960
+ useDevWarning(
9961
+ Boolean(renderSendExtras && !sendEnabled),
9962
+ "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
9963
+ );
9964
+ useDevWarning(
9965
+ Boolean(primaryAction && sendEnabled),
9966
+ "[BlockKitchen] `primaryAction` is only available in compose-only mode \u2014 with the send integration wired, the built-in Send/Update flow owns the toolbar\u2019s primary action; ignoring `primaryAction`."
9967
+ );
9853
9968
  const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9854
9969
  const childParentById = React.useMemo(() => {
9855
9970
  const map = /* @__PURE__ */ new Map();
@@ -9866,8 +9981,11 @@ function BlockKitchen(props) {
9866
9981
  const [sendOpen, setSendOpen] = React.useState(false);
9867
9982
  const [updateOpen, setUpdateOpen] = React.useState(false);
9868
9983
  const [loadOpen, setLoadOpen] = React.useState(false);
9869
- const [editTarget, setEditTarget] = React.useState(() => editing?.initialTarget ?? null);
9870
- const activeEditTarget = editing ? editTarget : null;
9984
+ const [editTarget, setEditTarget] = React.useState(
9985
+ () => loading?.initialTarget ? toLoadedMessage(loading.initialTarget) : null
9986
+ );
9987
+ const activeEditTarget = loadEnabled ? editTarget : null;
9988
+ useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
9871
9989
  const [issuesOpen, setIssuesOpen] = React.useState(false);
9872
9990
  const [paletteOpen, setPaletteOpen] = React.useState(false);
9873
9991
  const [openBlockId, setOpenBlockId] = React.useState(null);
@@ -9885,6 +10003,11 @@ function BlockKitchen(props) {
9885
10003
  return { emoji: makeEmojiHook(byName), ...previewHooks };
9886
10004
  }, [customEmojis, previewHooks]);
9887
10005
  const validation = useBlockKitValidation(blocks, "message");
10006
+ const validationSummary = React.useMemo(
10007
+ () => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
10008
+ [validation]
10009
+ );
10010
+ useNotifyOnChange(validationSummary, validationSummaryKey, onValidationChange);
9888
10011
  const sensors = core.useSensors(
9889
10012
  core.useSensor(core.PointerSensor, { activationConstraint: { distance: 4 } }),
9890
10013
  core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
@@ -9988,8 +10111,19 @@ function BlockKitchen(props) {
9988
10111
  allowedSurfaces,
9989
10112
  showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
9990
10113
  docsLink,
10114
+ showSend: sendEnabled,
10115
+ primaryAction: !sendEnabled && primaryAction ? {
10116
+ label: primaryAction.label,
10117
+ onClick: () => primaryAction.onClick({
10118
+ blocks: blockPayloads,
10119
+ validation: validationSummary,
10120
+ loadedMessage: activeEditTarget
10121
+ }),
10122
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10123
+ } : null,
9991
10124
  sendButtonLabel,
9992
- editingEnabled: !!editing,
10125
+ loadEnabled,
10126
+ updateEnabled: Boolean(onUpdate),
9993
10127
  editBadge: activeEditTarget ? {
9994
10128
  channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
9995
10129
  ts: activeEditTarget.ts
@@ -9997,9 +10131,13 @@ function BlockKitchen(props) {
9997
10131
  onOpenLoad: () => setLoadOpen(true),
9998
10132
  onOpenUpdate: () => setUpdateOpen(true),
9999
10133
  onExitEdit: () => {
10134
+ if (sendEnabled) {
10135
+ setEditTarget(null);
10136
+ replaceAll([]);
10137
+ setLoadOpen(true);
10138
+ return;
10139
+ }
10000
10140
  setEditTarget(null);
10001
- replaceAll([]);
10002
- setLoadOpen(true);
10003
10141
  },
10004
10142
  loadButtonLabel,
10005
10143
  updateButtonLabel
@@ -10071,7 +10209,7 @@ function BlockKitchen(props) {
10071
10209
  }
10072
10210
  ) }),
10073
10211
  /* @__PURE__ */ jsxRuntime.jsx(JsonDrawer, { open: jsonOpen, onOpenChange: setJsonOpen, blocks: blockPayloads, onApply: replaceAll }),
10074
- /* @__PURE__ */ jsxRuntime.jsx(
10212
+ loadChannels && loadSendAsUserStatus && onSend ? /* @__PURE__ */ jsxRuntime.jsx(
10075
10213
  SendDialog,
10076
10214
  {
10077
10215
  open: sendOpen,
@@ -10080,6 +10218,7 @@ function BlockKitchen(props) {
10080
10218
  loadChannels,
10081
10219
  loadSendAsUserStatus,
10082
10220
  onSend,
10221
+ renderSendExtras,
10083
10222
  confirmSendLabel,
10084
10223
  errorCount: validation.total,
10085
10224
  onShowIssues: () => {
@@ -10087,8 +10226,8 @@ function BlockKitchen(props) {
10087
10226
  setIssuesOpen(true);
10088
10227
  }
10089
10228
  }
10090
- ),
10091
- activeEditTarget && editing ? /* @__PURE__ */ jsxRuntime.jsx(
10229
+ ) : null,
10230
+ activeEditTarget && onUpdate && loadSendAsUserStatus ? /* @__PURE__ */ jsxRuntime.jsx(
10092
10231
  UpdateDialog,
10093
10232
  {
10094
10233
  open: updateOpen,
@@ -10096,7 +10235,7 @@ function BlockKitchen(props) {
10096
10235
  target: activeEditTarget,
10097
10236
  blocks: blockPayloads,
10098
10237
  loadSendAsUserStatus,
10099
- onUpdate: editing.onUpdate,
10238
+ onUpdate,
10100
10239
  confirmUpdateLabel,
10101
10240
  errorCount: validation.total,
10102
10241
  onShowIssues: () => {
@@ -10105,25 +10244,17 @@ function BlockKitchen(props) {
10105
10244
  }
10106
10245
  }
10107
10246
  ) : null,
10108
- editing ? /* @__PURE__ */ jsxRuntime.jsx(
10247
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(
10109
10248
  LoadMessageDialog,
10110
10249
  {
10111
10250
  open: loadOpen,
10112
10251
  onOpenChange: setLoadOpen,
10113
- onLoadMessage: editing.onLoadMessage,
10114
- loadRecentMessages: editing.loadRecentMessages,
10115
- loadChannels,
10252
+ onLoadMessage: loading.onLoadMessage,
10253
+ loadRecentMessages: loading.loadRecentMessages,
10254
+ loadChannels: recentChannelSource,
10116
10255
  onLoaded: (result) => {
10117
10256
  replaceAll(result.blocks);
10118
- setEditTarget({
10119
- channelId: result.channelId,
10120
- channelName: result.channelName,
10121
- ts: result.ts,
10122
- editableVia: result.editableVia,
10123
- workspaceName: result.workspaceName,
10124
- username: result.username,
10125
- iconUrl: result.iconUrl
10126
- });
10257
+ setEditTarget(toLoadedMessage(result));
10127
10258
  setLoadOpen(false);
10128
10259
  },
10129
10260
  onOpenAsNew: (loadedBlocks) => {
@@ -10306,6 +10437,8 @@ function fromBase64Url(b64url) {
10306
10437
  }
10307
10438
 
10308
10439
  exports.BlockKitchen = BlockKitchen;
10440
+ exports.SendDialog = SendDialog;
10441
+ exports.SlackSignInButton = SlackSignInButton;
10309
10442
  exports.TemplatePicker = TemplatePicker;
10310
10443
  exports.decodeBlocksFromString = decodeBlocksFromString;
10311
10444
  exports.defaultPalette = defaultPalette;
@@ -10313,5 +10446,6 @@ exports.encodeBlocksToString = encodeBlocksToString;
10313
10446
  exports.extraAlertVariant = extraAlertVariant;
10314
10447
  exports.legacyInputVariants = legacyInputVariants;
10315
10448
  exports.toSlackBlocks = toSlackBlocks;
10449
+ exports.useSlackSignIn = useSlackSignIn;
10316
10450
  //# sourceMappingURL=index.cjs.map
10317
10451
  //# sourceMappingURL=index.cjs.map