@tappify/extension-sdk 2.0.0 → 2.1.0-beta.2

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 (148) hide show
  1. package/README.md +208 -0
  2. package/dist/chunk-2CY5HVY7.mjs +59 -0
  3. package/dist/chunk-2CY5HVY7.mjs.map +1 -0
  4. package/dist/{chunk-2DBSVKAU.cjs → chunk-6HETNZJY.cjs} +1 -1
  5. package/dist/chunk-6HETNZJY.cjs.map +1 -0
  6. package/dist/{chunk-PYJMJF44.cjs → chunk-7YKH4LZN.cjs} +9 -2
  7. package/dist/chunk-7YKH4LZN.cjs.map +1 -0
  8. package/dist/{chunk-RMUWRESK.cjs → chunk-A74HCO6T.cjs} +31 -18
  9. package/dist/chunk-A74HCO6T.cjs.map +1 -0
  10. package/dist/chunk-BPCGXXMD.cjs +781 -0
  11. package/dist/chunk-BPCGXXMD.cjs.map +1 -0
  12. package/dist/chunk-EB5XWQAU.cjs +132 -0
  13. package/dist/chunk-EB5XWQAU.cjs.map +1 -0
  14. package/dist/{chunk-5AAPD5WV.cjs → chunk-GAUZYBXE.cjs} +11 -11
  15. package/dist/chunk-GAUZYBXE.cjs.map +1 -0
  16. package/dist/chunk-J5LGTIGS.mjs +10 -0
  17. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  18. package/dist/{chunk-ZT35X7KL.mjs → chunk-KZ5NGFUP.mjs} +22 -9
  19. package/dist/chunk-KZ5NGFUP.mjs.map +1 -0
  20. package/dist/chunk-NN263PGQ.cjs +70 -0
  21. package/dist/chunk-NN263PGQ.cjs.map +1 -0
  22. package/dist/{chunk-3YWVYA42.cjs → chunk-O357OJXL.cjs} +14 -18
  23. package/dist/chunk-O357OJXL.cjs.map +1 -0
  24. package/dist/chunk-ONJMPO3P.cjs +59 -0
  25. package/dist/chunk-ONJMPO3P.cjs.map +1 -0
  26. package/dist/{chunk-OPPYEA7F.cjs → chunk-PRCZMOLD.cjs} +3 -3
  27. package/dist/chunk-PRCZMOLD.cjs.map +1 -0
  28. package/dist/chunk-Q7SFCCGT.cjs +10 -0
  29. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  30. package/dist/{chunk-QHM7XNRK.mjs → chunk-QA5AL3VZ.mjs} +2 -2
  31. package/dist/{chunk-QHM7XNRK.mjs.map → chunk-QA5AL3VZ.mjs.map} +1 -1
  32. package/dist/chunk-QJMZRV7A.mjs +132 -0
  33. package/dist/chunk-QJMZRV7A.mjs.map +1 -0
  34. package/dist/{chunk-CSTYBKZC.mjs → chunk-QXD7WVKK.mjs} +1 -1
  35. package/dist/chunk-QXD7WVKK.mjs.map +1 -0
  36. package/dist/{chunk-4C5Z3CIK.mjs → chunk-RBIQT75L.mjs} +2 -2
  37. package/dist/chunk-RBIQT75L.mjs.map +1 -0
  38. package/dist/{chunk-MNDVYPEP.mjs → chunk-SGCYEEXD.mjs} +9 -2
  39. package/dist/chunk-SGCYEEXD.mjs.map +1 -0
  40. package/dist/{chunk-BDCX6LRQ.mjs → chunk-SGEYDV3K.mjs} +3 -3
  41. package/dist/chunk-SGEYDV3K.mjs.map +1 -0
  42. package/dist/{chunk-IGK2AP6J.mjs → chunk-SLGOHIBL.mjs} +13 -17
  43. package/dist/chunk-SLGOHIBL.mjs.map +1 -0
  44. package/dist/chunk-SWG7CNTZ.mjs +781 -0
  45. package/dist/chunk-SWG7CNTZ.mjs.map +1 -0
  46. package/dist/{chunk-7KIKOH5H.cjs → chunk-VV7ELNRW.cjs} +3 -3
  47. package/dist/chunk-VV7ELNRW.cjs.map +1 -0
  48. package/dist/chunk-Z43DPNZ6.mjs +70 -0
  49. package/dist/chunk-Z43DPNZ6.mjs.map +1 -0
  50. package/dist/client-BO3kavn0.d.ts +25 -0
  51. package/dist/client-DE-vMgdO.d.cts +25 -0
  52. package/dist/codegen.cjs.map +1 -1
  53. package/dist/codegen.d.cts +43 -7
  54. package/dist/codegen.d.ts +43 -7
  55. package/dist/codegen.mjs.map +1 -1
  56. package/dist/{constants-DJ_KgZpN.d.cts → constants-Bh759FUI.d.cts} +44 -8
  57. package/dist/{constants-DJ_KgZpN.d.ts → constants-Bh759FUI.d.ts} +44 -8
  58. package/dist/{context-Dt6xh_5t.d.cts → context-DI0EJCRD.d.cts} +5 -4
  59. package/dist/{context-Ml7k_V14.d.ts → context-mr8R5CNt.d.ts} +5 -4
  60. package/dist/{errors-COsBh7kd.d.cts → errors-Cqh0wrqt.d.cts} +3 -2
  61. package/dist/{errors-COsBh7kd.d.ts → errors-Cqh0wrqt.d.ts} +3 -2
  62. package/dist/host.cjs +5 -4
  63. package/dist/host.cjs.map +1 -1
  64. package/dist/host.d.cts +5 -5
  65. package/dist/host.d.ts +5 -5
  66. package/dist/host.mjs +4 -3
  67. package/dist/host.mjs.map +1 -1
  68. package/dist/index.cjs +9 -656
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +69 -627
  71. package/dist/index.d.ts +69 -627
  72. package/dist/index.mjs +5 -652
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/manifest.cjs +142 -86
  75. package/dist/manifest.cjs.map +1 -1
  76. package/dist/manifest.d.cts +97 -12
  77. package/dist/manifest.d.ts +97 -12
  78. package/dist/manifest.mjs +129 -73
  79. package/dist/manifest.mjs.map +1 -1
  80. package/dist/server.cjs +15 -66
  81. package/dist/server.cjs.map +1 -1
  82. package/dist/server.d.cts +6 -6
  83. package/dist/server.d.ts +6 -6
  84. package/dist/server.mjs +10 -61
  85. package/dist/server.mjs.map +1 -1
  86. package/dist/testing/index.cjs +73 -750
  87. package/dist/testing/index.cjs.map +1 -1
  88. package/dist/testing/index.d.cts +30 -11
  89. package/dist/testing/index.d.ts +30 -11
  90. package/dist/testing/index.mjs +89 -766
  91. package/dist/testing/index.mjs.map +1 -1
  92. package/dist/testing/jest.cjs +6 -5
  93. package/dist/testing/jest.cjs.map +1 -1
  94. package/dist/testing/jest.mjs +3 -2
  95. package/dist/testing/jest.mjs.map +1 -1
  96. package/dist/testing/vitest.cjs +6 -5
  97. package/dist/testing/vitest.cjs.map +1 -1
  98. package/dist/testing/vitest.d.cts +10 -0
  99. package/dist/testing/vitest.d.ts +10 -0
  100. package/dist/testing/vitest.mjs +3 -2
  101. package/dist/testing/vitest.mjs.map +1 -1
  102. package/dist/{types-DDAGLpov.d.ts → types-BdDaLnqs.d.cts} +4 -3
  103. package/dist/{types-DrpQSv_2.d.cts → types-uGHrRaOj.d.ts} +4 -3
  104. package/dist/ui/styles.css +740 -0
  105. package/dist/ui.cjs +1018 -0
  106. package/dist/ui.cjs.map +1 -0
  107. package/dist/ui.d.cts +231 -0
  108. package/dist/ui.d.ts +231 -0
  109. package/dist/ui.mjs +1018 -0
  110. package/dist/ui.mjs.map +1 -0
  111. package/dist/vite-preview-bootstrap.cjs +66 -0
  112. package/dist/vite-preview-bootstrap.cjs.map +1 -0
  113. package/dist/vite-preview-bootstrap.d.cts +15 -0
  114. package/dist/vite-preview-bootstrap.d.ts +15 -0
  115. package/dist/vite-preview-bootstrap.mjs +66 -0
  116. package/dist/vite-preview-bootstrap.mjs.map +1 -0
  117. package/dist/vite-preview.cjs +876 -0
  118. package/dist/vite-preview.cjs.map +1 -0
  119. package/dist/vite-preview.d.cts +9 -0
  120. package/dist/vite-preview.d.ts +9 -0
  121. package/dist/vite-preview.mjs +876 -0
  122. package/dist/vite-preview.mjs.map +1 -0
  123. package/dist/vite.cjs +320 -60
  124. package/dist/vite.cjs.map +1 -1
  125. package/dist/vite.d.cts +54 -8
  126. package/dist/vite.d.ts +54 -8
  127. package/dist/vite.mjs +312 -54
  128. package/dist/vite.mjs.map +1 -1
  129. package/dist/work-events-BlOVpK0k.d.ts +547 -0
  130. package/dist/work-events-D97EoqvS.d.cts +547 -0
  131. package/package.json +19 -2
  132. package/schema/extension-v2.json +440 -121
  133. package/dist/chunk-2DBSVKAU.cjs.map +0 -1
  134. package/dist/chunk-3YWVYA42.cjs.map +0 -1
  135. package/dist/chunk-4C5Z3CIK.mjs.map +0 -1
  136. package/dist/chunk-5AAPD5WV.cjs.map +0 -1
  137. package/dist/chunk-7KIKOH5H.cjs.map +0 -1
  138. package/dist/chunk-BDCX6LRQ.mjs.map +0 -1
  139. package/dist/chunk-CSTYBKZC.mjs.map +0 -1
  140. package/dist/chunk-IGK2AP6J.mjs.map +0 -1
  141. package/dist/chunk-MNDVYPEP.mjs.map +0 -1
  142. package/dist/chunk-OPPYEA7F.cjs.map +0 -1
  143. package/dist/chunk-PYJMJF44.cjs.map +0 -1
  144. package/dist/chunk-RMUWRESK.cjs.map +0 -1
  145. package/dist/chunk-ZT35X7KL.mjs.map +0 -1
  146. package/dist/handler-CZIlZ5vu.d.ts +0 -227
  147. package/dist/handler-kU9Jf36K.d.cts +0 -227
  148. package/dist/styles.css +0 -367
package/dist/vite.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/vite/plugin.ts
2
2
  import { federation } from "@module-federation/vite";
3
3
  import { readFileSync as readFileSync2 } from "fs";
4
- import { resolve as resolve2, sep as sep2 } from "path";
4
+ import { resolve as resolve3, sep as sep2 } from "path";
5
5
 
6
6
  // src/client/errors.ts
7
7
  var TAP_ERROR_CODES = [
@@ -143,6 +143,13 @@ var SCOPES = [
143
143
  requiresSecurityReview: false,
144
144
  requiresJustification: false
145
145
  },
146
+ {
147
+ key: "review.threads:read",
148
+ label: "Read Review threads",
149
+ description: "Review threads, comments, screenshot descriptors, build context and safe device context",
150
+ requiresSecurityReview: false,
151
+ requiresJustification: false
152
+ },
146
153
  {
147
154
  key: "crashes:read",
148
155
  label: "Read crash data",
@@ -216,7 +223,7 @@ var SCOPES = [
216
223
  {
217
224
  key: "work:sync",
218
225
  label: "Sync work items",
219
- description: "Two-way comments and status on created work items",
226
+ description: "Sync comments and status on created work items",
220
227
  requiresSecurityReview: false,
221
228
  requiresJustification: true
222
229
  },
@@ -520,7 +527,10 @@ var connectorContribution = z.object({
520
527
  ),
521
528
  scopes: z.array(z.string()).describe(
522
529
  "The scopes asked of the outside service. These are that service's scopes, not Tappify's."
523
- )
530
+ ),
531
+ tokenRequestEncoding: z.enum(["form", "json"]).describe(
532
+ "The request body encoding for authorization-code and refresh token exchanges. When omitted, uses form encoding (application/x-www-form-urlencoded); json uses application/json."
533
+ ).optional()
524
534
  }).strict().describe('The endpoints, for `method: "oauth"`.').optional()
525
535
  }).strict().describe("How owners authenticate this connector.").optional(),
526
536
  sync: z.enum(["5m", "15m", "1h", "6h", "24h"]).describe("How often Tappify asks your server to sync.").optional(),
@@ -664,7 +674,7 @@ var aiContribution = z.object({
664
674
  ).optional()
665
675
  }).strict();
666
676
  var statusNames = (stage) => z.array(z.string()).describe(`Your own status names that mean ${stage}.`);
667
- var workContribution = z.object({
677
+ var legacyWorkContribution = z.object({
668
678
  kind: z.enum(["tracker", "docs", "chat"]).describe("What kind of destination this is."),
669
679
  container: z.object({
670
680
  label: z.string().min(1).max(60).describe(
@@ -699,6 +709,55 @@ var workContribution = z.object({
699
709
  "Whether items are created as the owner who asked (`user`) or as the install itself."
700
710
  )
701
711
  }).strict();
712
+ var workDirection = z.enum([
713
+ "none",
714
+ "to_provider",
715
+ "from_provider",
716
+ "two-way"
717
+ ]);
718
+ var workAuth = z.discriminatedUnion("method", [
719
+ z.object({
720
+ method: z.literal("oauth"),
721
+ provider: z.string().min(1).describe("The OAuth provider identifier."),
722
+ scopes: z.array(z.string()).describe("The scopes requested from the provider.")
723
+ }).strict(),
724
+ z.object({
725
+ method: z.literal("api_key"),
726
+ fields: z.array(credentialField).max(20)
727
+ }).strict()
728
+ ]);
729
+ var currentWorkContribution = legacyWorkContribution.extend({
730
+ container: legacyWorkContribution.shape.container.describe(
731
+ "Deprecated. Resolve containers dynamically with describe_container."
732
+ ).optional(),
733
+ fields: legacyWorkContribution.shape.fields.describe("Deprecated. Resolve fields dynamically with describe_container.").optional(),
734
+ comments: z.union([
735
+ z.enum(["two-way", "none"]).describe("Deprecated. Declare a comments capability object."),
736
+ z.object({
737
+ direction: workDirection,
738
+ supportsEdits: z.literal(false),
739
+ supportsDeletes: z.literal(false)
740
+ }).strict()
741
+ ]),
742
+ status: z.object({
743
+ direction: workDirection,
744
+ webhook: z.string().regex(DOMAIN_EVENT).optional(),
745
+ map: z.object({
746
+ open: statusNames("not started"),
747
+ in_progress: statusNames("under way"),
748
+ done: statusNames("finished")
749
+ }).strict().describe(
750
+ "Deprecated. Resolve statuses dynamically with describe_container."
751
+ ).optional()
752
+ }).strict().optional(),
753
+ auth: workAuth.describe("How owners authenticate the work destination.")
754
+ });
755
+ var workContribution = z.union([
756
+ currentWorkContribution,
757
+ legacyWorkContribution.describe(
758
+ "Deprecated SDK v2 work declaration. Migrate to work capabilities and auth."
759
+ )
760
+ ]);
702
761
  var listing = z.object({
703
762
  longDescription: z.string().min(1).max(5e3).describe("The body of your listing page, in markdown.").optional(),
704
763
  screenshots: z.array(z.string().min(1)).min(1, "A listing shows 1 to 6 screenshots.").max(6, "A listing shows 1 to 6 screenshots.").describe(
@@ -790,7 +849,7 @@ var manifestObject = z.object({
790
849
  "What this extension offers the assistant: tools, mentions, actions, knowledge, skills, prompts and context providers."
791
850
  ).optional(),
792
851
  work: workContribution.describe(
793
- "The work destination this extension creates issues, pages or messages in. Needs the `work:create` and `work:sync` scopes."
852
+ "The work destination this extension creates issues, pages or messages in. Needs `review.threads:read` and `work:create`; active comment or status sync also needs `work:sync`."
794
853
  ).optional()
795
854
  }).strict().describe(
796
855
  "What this extension adds to the host. At most 20 UI contributions in total, and a page, tab, widget, row action, marker or settings panel needs the `ui:render` scope."
@@ -1011,16 +1070,36 @@ var manifestSchema = manifestObject.superRefine((manifest, ctx) => {
1011
1070
  }
1012
1071
  }
1013
1072
  if (contributes.work) {
1073
+ const work = contributes.work;
1074
+ const legacy = isLegacyWork(work);
1075
+ if (!legacy || !granted.has("reviews:read")) {
1076
+ requireScope(
1077
+ "review.threads:read",
1078
+ ["scopes"],
1079
+ "the work destination reads Review threads."
1080
+ );
1081
+ }
1014
1082
  requireScope(
1015
1083
  "work:create",
1016
1084
  ["scopes"],
1017
1085
  "the work destination creates items."
1018
1086
  );
1019
- requireScope(
1020
- "work:sync",
1021
- ["scopes"],
1022
- "the work destination syncs comments and status."
1023
- );
1087
+ const comments = typeof work.comments === "string" ? work.comments : work.comments.direction;
1088
+ const status = work.status ? "direction" in work.status ? work.status.direction : "from_provider" : "none";
1089
+ if (comments !== "none" || status !== "none") {
1090
+ requireScope(
1091
+ "work:sync",
1092
+ ["scopes"],
1093
+ "the work destination syncs comments or status."
1094
+ );
1095
+ }
1096
+ if (work.identity === "user") {
1097
+ ctx.addIssue({
1098
+ code: "custom",
1099
+ path: ["contributes", "work", "identity"],
1100
+ message: 'Per-user work identity is not supported. Set identity to "install".'
1101
+ });
1102
+ }
1024
1103
  }
1025
1104
  const needsServer = contributes.connector !== void 0 || (contributes.webhooks?.length ?? 0) > 0 || ai !== void 0 || contributes.work !== void 0 || manifest.server?.procedures !== void 0;
1026
1105
  if (needsServer && !manifest.server?.baseUrl) {
@@ -1095,10 +1174,176 @@ function validateManifest(value) {
1095
1174
  }));
1096
1175
  return { ok: false, issues };
1097
1176
  }
1177
+ function isLegacyWork(work) {
1178
+ return !("auth" in work);
1179
+ }
1180
+
1181
+ // src/vite/preview/plugin.ts
1182
+ import { randomUUID } from "crypto";
1183
+ import { isAbsolute, relative, resolve } from "path";
1184
+ import { normalizePath } from "vite";
1185
+ var PREVIEW_CLIENT_ID = "virtual:tappify-preview/client";
1186
+ var PREVIEW_MANIFEST_ID = "virtual:tappify-preview/manifest";
1187
+ var PREVIEW_REMOTE_ID = "virtual:tappify-preview/remote";
1188
+ var PREVIEW_REMOTE_ENV = "TAPPIFY_PREVIEW_REMOTE";
1189
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1190
+ var VIRTUAL_IDS = /* @__PURE__ */ new Set([
1191
+ PREVIEW_CLIENT_ID,
1192
+ PREVIEW_MANIFEST_ID,
1193
+ PREVIEW_REMOTE_ID
1194
+ ]);
1195
+ function previewRemote(remote, port) {
1196
+ if (remote === void 0 || remote.trim() === "") {
1197
+ return { ok: true, url: `http://localhost:${String(port)}/remoteEntry.js` };
1198
+ }
1199
+ let parsed;
1200
+ try {
1201
+ parsed = new URL(remote);
1202
+ } catch {
1203
+ return { ok: false, message: "The preview remote is not a valid URL." };
1204
+ }
1205
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname))) {
1206
+ return {
1207
+ ok: false,
1208
+ message: "A preview remote must use HTTPS or loopback HTTP."
1209
+ };
1210
+ }
1211
+ return { ok: true, url: parsed.toString() };
1212
+ }
1213
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1214
+ function messageDocument(title2, messages) {
1215
+ return `<!doctype html>
1216
+ <html lang="en">
1217
+ <head>
1218
+ <meta charset="UTF-8" />
1219
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1220
+ <title>${escapeHtml(title2)} \xB7 Tappify local preview</title>
1221
+ <style>body{margin:0;min-height:100vh;display:grid;place-content:center;padding:32px;box-sizing:border-box;background:#f4f5ef;color:#142012;font-family:Inter,ui-sans-serif,system-ui,sans-serif}main{max-width:720px;padding:28px;border:1px solid #d8ddcf;border-radius:12px;background:#fff;box-shadow:0 12px 36px rgb(20 32 18 / 9%)}h1{font-size:22px;margin:0 0 14px}ul{margin:0;padding-left:22px;color:#4d5849}li+li{margin-top:8px}</style>
1222
+ </head>
1223
+ <body><main><h1>${escapeHtml(title2)}</h1><ul>${messages.map((message) => `<li>${escapeHtml(message)}</li>`).join("")}</ul></main></body>
1224
+ </html>`;
1225
+ }
1226
+ function previewDocument(manifest, remote) {
1227
+ if (!manifest.ok) {
1228
+ return messageDocument(
1229
+ "Manifest is invalid",
1230
+ manifest.issues.map((issue) => `${issue.path}: ${issue.message}`)
1231
+ );
1232
+ }
1233
+ if (!remote.ok)
1234
+ return messageDocument("Preview remote was refused", [remote.message]);
1235
+ const icon = manifest.manifest.icon.replace(/^\.\//, "");
1236
+ return `<!doctype html>
1237
+ <html lang="en">
1238
+ <head>
1239
+ <meta charset="UTF-8" />
1240
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1241
+ <link rel="icon" href="/${escapeHtml(icon)}" />
1242
+ <title>${escapeHtml(manifest.manifest.name)} \xB7 Tappify local preview</title>
1243
+ </head>
1244
+ <body>
1245
+ <div id="tappify-preview-root"></div>
1246
+ <script type="module" src="/@id/${PREVIEW_CLIENT_ID}"></script>
1247
+ </body>
1248
+ </html>`;
1249
+ }
1250
+ var moduleSource = (value) => `export default ${JSON.stringify(value)};`;
1251
+ function optimizedReactUrl(root, cacheDir, cacheGeneration) {
1252
+ const depsCacheDir = normalizePath(resolve(cacheDir, "deps"));
1253
+ const relativeCacheDir = normalizePath(relative(root, depsCacheDir));
1254
+ const prefix = isAbsolute(relativeCacheDir) || relativeCacheDir.startsWith("../") ? `/@fs/${depsCacheDir.replace(/^\/+/, "")}` : `/${relativeCacheDir}`;
1255
+ return `${prefix}/react.js?tappify=${encodeURIComponent(cacheGeneration)}`;
1256
+ }
1257
+ function createPreviewPlugins(options) {
1258
+ const remote = previewRemote(options.remote, options.port);
1259
+ const cacheGeneration = randomUUID();
1260
+ let reactUrl = optimizedReactUrl(
1261
+ resolve(options.manifestFile, ".."),
1262
+ resolve(options.manifestFile, "../node_modules/.vite"),
1263
+ cacheGeneration
1264
+ );
1265
+ const preview = {
1266
+ name: "tappify:preview",
1267
+ apply: "serve",
1268
+ config() {
1269
+ return {
1270
+ optimizeDeps: {
1271
+ exclude: ["@tappify/extension-sdk/vite/preview/bootstrap"]
1272
+ }
1273
+ };
1274
+ },
1275
+ configResolved(config) {
1276
+ reactUrl = optimizedReactUrl(
1277
+ config.root,
1278
+ config.cacheDir,
1279
+ cacheGeneration
1280
+ );
1281
+ },
1282
+ resolveId(id) {
1283
+ if (VIRTUAL_IDS.has(id)) return `\0${id}`;
1284
+ if (id.startsWith("\0") && VIRTUAL_IDS.has(id.slice(1))) return id;
1285
+ return null;
1286
+ },
1287
+ load(id) {
1288
+ if (id === `\0${PREVIEW_MANIFEST_ID}`) {
1289
+ return moduleSource(options.loadManifest());
1290
+ }
1291
+ if (id === `\0${PREVIEW_REMOTE_ID}`) return moduleSource(remote);
1292
+ if (id !== `\0${PREVIEW_CLIENT_ID}`) return null;
1293
+ return [
1294
+ `import { startLocalPreview } from '@tappify/extension-sdk/vite/preview/bootstrap';`,
1295
+ `import manifest from '${PREVIEW_MANIFEST_ID}';`,
1296
+ `import remote from '${PREVIEW_REMOTE_ID}';`,
1297
+ `const mount = document.getElementById('tappify-preview-root');`,
1298
+ `if (mount && manifest.ok && remote.ok) void startLocalPreview({ manifest: manifest.manifest, remote: remote.url, reactUrl: ${JSON.stringify(reactUrl)}, mount });`
1299
+ ].join("\n");
1300
+ },
1301
+ configureServer(server) {
1302
+ server.middlewares.use((request, response, next) => {
1303
+ const path = request.url?.split("?", 1)[0];
1304
+ if (path !== "/" && path !== "/index.html") return next();
1305
+ void server.transformIndexHtml(
1306
+ path,
1307
+ previewDocument(options.loadManifest(), remote)
1308
+ ).then((html) => {
1309
+ response.statusCode = 200;
1310
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
1311
+ response.end(html);
1312
+ }).catch(next);
1313
+ });
1314
+ }
1315
+ };
1316
+ const dev = {
1317
+ name: "tappify:dev",
1318
+ apply: "serve",
1319
+ config() {
1320
+ return {
1321
+ server: {
1322
+ host: "127.0.0.1",
1323
+ port: options.port,
1324
+ strictPort: true,
1325
+ cors: { origin: true },
1326
+ origin: `http://localhost:${String(options.port)}`
1327
+ }
1328
+ };
1329
+ },
1330
+ configureServer(server) {
1331
+ server.watcher.add(options.manifestFile);
1332
+ },
1333
+ async hotUpdate(context) {
1334
+ if (this.environment.name !== "client") return;
1335
+ if (context.file !== options.manifestFile) return;
1336
+ const manifest = options.loadManifest();
1337
+ await options.onManifestChange?.(manifest);
1338
+ context.server.ws.send({ type: "full-reload", path: "*" });
1339
+ }
1340
+ };
1341
+ return [preview, dev];
1342
+ }
1098
1343
 
1099
1344
  // src/vite/styles.ts
1100
1345
  import { existsSync, readFileSync } from "fs";
1101
- import { dirname, extname, resolve, sep } from "path";
1346
+ import { dirname, extname, resolve as resolve2, sep } from "path";
1102
1347
  var IMPORT_PATTERN = /(?:^|\n)\s*(?:import|export)(?:[^'"]*?from\s*)?\s*['"]([^'"]+)['"]/g;
1103
1348
  var IMPORT_SPECIFIER_PATTERN = /((?:^|\n)\s*(?:import|export)(?:[^'"]*?from\s*)?\s*['"])([^'"]+)(['"])/g;
1104
1349
  var BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
@@ -1111,14 +1356,14 @@ function isPlainStylesheet(specifier) {
1111
1356
  return specifier.endsWith(".css") && !specifier.endsWith(".module.css");
1112
1357
  }
1113
1358
  function resolveSource(specifier, importer) {
1114
- const base = resolve(dirname(importer), specifier);
1359
+ const base = resolve2(dirname(importer), specifier);
1115
1360
  if (extname(base) !== "" && existsSync(base)) return base;
1116
1361
  for (const extension of SOURCE_EXTENSIONS) {
1117
1362
  const candidate = `${base}${extension}`;
1118
1363
  if (existsSync(candidate)) return candidate;
1119
1364
  }
1120
1365
  for (const extension of SOURCE_EXTENSIONS) {
1121
- const candidate = resolve(base, `index${extension}`);
1366
+ const candidate = resolve2(base, `index${extension}`);
1122
1367
  if (existsSync(candidate)) return candidate;
1123
1368
  }
1124
1369
  return null;
@@ -1140,7 +1385,7 @@ function collectEntryStyles(entryAbsolutePath, root) {
1140
1385
  while (match !== null) {
1141
1386
  const specifier = match[1];
1142
1387
  if (specifier.endsWith(".css")) {
1143
- const value = specifier.startsWith(".") ? resolve(dirname(file), specifier) : specifier;
1388
+ const value = specifier.startsWith(".") ? resolve2(dirname(file), specifier) : specifier;
1144
1389
  if (!seenStyles.has(value)) {
1145
1390
  seenStyles.add(value);
1146
1391
  styles.push(value);
@@ -1173,38 +1418,52 @@ var DEFAULT_PORT = 5273;
1173
1418
  var SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
1174
1419
  var federationOptionsByPlugins = /* @__PURE__ */ new WeakMap();
1175
1420
  function manifestFile(options) {
1176
- return options.manifestPath ?? resolve2(options.root ?? process.cwd(), "tappify.extension.json");
1421
+ return options.manifestPath ?? resolve3(options.root ?? process.cwd(), "tappify.extension.json");
1177
1422
  }
1178
1423
  function loadManifest(options) {
1179
- const raw = options.manifest ?? JSON.parse(readFileSync2(manifestFile(options), "utf8"));
1180
- const result = validateManifest(raw);
1181
- if (!result.ok) {
1182
- const lines = result.issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n");
1183
- throw new TapError(
1184
- "TAP_MANIFEST_INVALID",
1185
- `tappify.extension.json does not validate:
1424
+ let raw;
1425
+ try {
1426
+ raw = options.manifest ?? JSON.parse(readFileSync2(manifestFile(options), "utf8"));
1427
+ } catch (error) {
1428
+ return {
1429
+ ok: false,
1430
+ issues: [
1431
+ {
1432
+ path: "$",
1433
+ message: error instanceof Error ? `The manifest is not valid JSON: ${error.message}` : "The manifest is not valid JSON."
1434
+ }
1435
+ ]
1436
+ };
1437
+ }
1438
+ return validateManifest(raw);
1439
+ }
1440
+ function manifestError(result) {
1441
+ if (result.ok) throw new Error("A valid manifest has no validation error.");
1442
+ const lines = result.issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n");
1443
+ return new TapError(
1444
+ "TAP_MANIFEST_INVALID",
1445
+ `tappify.extension.json does not validate:
1186
1446
  ${lines}
1187
1447
  Fix the fields above, or run \`tappify extension doctor\` for the same list with suggested fixes.`
1188
- );
1189
- }
1190
- return result.manifest;
1448
+ );
1191
1449
  }
1192
1450
  function tappifyExtension(options = {}) {
1193
1451
  const root = options.root ?? process.cwd();
1194
1452
  const boundary = root.endsWith(sep2) ? root : `${root}${sep2}`;
1195
- const manifest = loadManifest(options);
1196
- const contributions = uiContributions(manifest);
1453
+ const loaded = loadManifest(options);
1454
+ const manifest = loaded.ok ? loaded.manifest : null;
1455
+ const contributions = manifest === null ? [] : uiContributions(manifest);
1197
1456
  const port = options.port ?? DEFAULT_PORT;
1198
1457
  const entryByKey = /* @__PURE__ */ new Map();
1199
1458
  const exposes = {};
1200
1459
  for (const contribution of contributions) {
1201
1460
  const key = contribution.expose.slice("./".length);
1202
- entryByKey.set(key, resolve2(root, contribution.entry));
1461
+ entryByKey.set(key, resolve3(root, contribution.entry));
1203
1462
  exposes[contribution.expose] = `${VIRTUAL_PREFIX}${key}`;
1204
1463
  }
1205
1464
  const singleton = { singleton: true, requiredVersion: false };
1206
1465
  const federationOptions = {
1207
- name: remoteName(manifest.id),
1466
+ name: remoteName(manifest?.id ?? "invalid-extension"),
1208
1467
  filename: "remoteEntry.js",
1209
1468
  manifest: true,
1210
1469
  exposes,
@@ -1260,6 +1519,16 @@ function tappifyExtension(options = {}) {
1260
1519
  ].join("\n");
1261
1520
  }
1262
1521
  };
1522
+ const guard = {
1523
+ name: "tappify:manifest",
1524
+ enforce: "pre",
1525
+ config(_config, environment) {
1526
+ if (environment.command !== "build") return null;
1527
+ const current = loadManifest(options);
1528
+ if (!current.ok) throw manifestError(current);
1529
+ return null;
1530
+ }
1531
+ };
1263
1532
  const css = {
1264
1533
  name: "tappify:css",
1265
1534
  enforce: "pre",
@@ -1272,43 +1541,32 @@ function tappifyExtension(options = {}) {
1272
1541
  return next === null ? null : { code: next, map: null };
1273
1542
  }
1274
1543
  };
1275
- const dev = {
1276
- name: "tappify:dev",
1277
- apply: "serve",
1278
- config() {
1279
- return {
1280
- server: {
1281
- port,
1282
- strictPort: true,
1283
- cors: { origin: true },
1284
- origin: `http://localhost:${String(port)}`
1285
- }
1286
- };
1287
- },
1288
- configureServer(server) {
1289
- server.watcher.add(manifestFile(options));
1290
- },
1291
- /** Vite runs hotUpdate once per dev environment, and the manifest is one file. */
1292
- async hotUpdate(context) {
1293
- if (this.environment.name !== "client") return;
1294
- if (context.file !== manifestFile(options)) return;
1295
- if (options.onManifestChange === void 0) return;
1296
- await options.onManifestChange(loadManifest(options));
1297
- }
1298
- };
1299
1544
  const plugins = [
1545
+ guard,
1300
1546
  expose,
1301
1547
  css,
1302
1548
  ...federation(federationOptions),
1303
- dev
1549
+ ...createPreviewPlugins({
1550
+ manifestFile: manifestFile(options),
1551
+ loadManifest: () => loadManifest(options),
1552
+ port,
1553
+ remote: process.env[PREVIEW_REMOTE_ENV],
1554
+ onManifestChange: async (result) => {
1555
+ if (result.ok && options.onManifestChange !== void 0) {
1556
+ await options.onManifestChange(result.manifest);
1557
+ }
1558
+ }
1559
+ })
1304
1560
  ];
1305
1561
  federationOptionsByPlugins.set(plugins, federationOptions);
1306
1562
  return plugins;
1307
1563
  }
1308
1564
  export {
1565
+ PREVIEW_REMOTE_ENV,
1309
1566
  VIRTUAL_PREFIX,
1310
1567
  collectEntryStyles,
1311
1568
  inlineCssImports,
1569
+ previewRemote,
1312
1570
  tappifyExtension
1313
1571
  };
1314
1572
  //# sourceMappingURL=vite.mjs.map