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

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-CV1qzQgx.d.cts +25 -0
  51. package/dist/client-CnlSHDWE.d.ts +25 -0
  52. package/dist/codegen.cjs.map +1 -1
  53. package/dist/codegen.d.cts +40 -7
  54. package/dist/codegen.d.ts +40 -7
  55. package/dist/codegen.mjs.map +1 -1
  56. package/dist/{constants-DJ_KgZpN.d.cts → constants-BAimnqOK.d.cts} +41 -8
  57. package/dist/{constants-DJ_KgZpN.d.ts → constants-BAimnqOK.d.ts} +41 -8
  58. package/dist/{context-Ml7k_V14.d.ts → context-Cr93B05o.d.ts} +5 -4
  59. package/dist/{context-Dt6xh_5t.d.cts → context-DJ3REtdT.d.cts} +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 +138 -85
  75. package/dist/manifest.cjs.map +1 -1
  76. package/dist/manifest.d.cts +93 -12
  77. package/dist/manifest.d.ts +93 -12
  78. package/dist/manifest.mjs +125 -72
  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-DrpQSv_2.d.cts → types-BYGcp5pZ.d.ts} +4 -3
  103. package/dist/{types-DDAGLpov.d.ts → types-I1JOZyI_.d.cts} +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 +316 -59
  124. package/dist/vite.cjs.map +1 -1
  125. package/dist/vite.d.cts +51 -8
  126. package/dist/vite.d.ts +51 -8
  127. package/dist/vite.mjs +308 -53
  128. package/dist/vite.mjs.map +1 -1
  129. package/dist/work-events-DB3WR468.d.ts +547 -0
  130. package/dist/work-events-Dxe7Djdy.d.cts +547 -0
  131. package/package.json +19 -2
  132. package/schema/extension-v2.json +432 -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
  },
@@ -664,7 +671,7 @@ var aiContribution = z.object({
664
671
  ).optional()
665
672
  }).strict();
666
673
  var statusNames = (stage) => z.array(z.string()).describe(`Your own status names that mean ${stage}.`);
667
- var workContribution = z.object({
674
+ var legacyWorkContribution = z.object({
668
675
  kind: z.enum(["tracker", "docs", "chat"]).describe("What kind of destination this is."),
669
676
  container: z.object({
670
677
  label: z.string().min(1).max(60).describe(
@@ -699,6 +706,55 @@ var workContribution = z.object({
699
706
  "Whether items are created as the owner who asked (`user`) or as the install itself."
700
707
  )
701
708
  }).strict();
709
+ var workDirection = z.enum([
710
+ "none",
711
+ "to_provider",
712
+ "from_provider",
713
+ "two-way"
714
+ ]);
715
+ var workAuth = z.discriminatedUnion("method", [
716
+ z.object({
717
+ method: z.literal("oauth"),
718
+ provider: z.string().min(1).describe("The OAuth provider identifier."),
719
+ scopes: z.array(z.string()).describe("The scopes requested from the provider.")
720
+ }).strict(),
721
+ z.object({
722
+ method: z.literal("api_key"),
723
+ fields: z.array(credentialField).max(20)
724
+ }).strict()
725
+ ]);
726
+ var currentWorkContribution = legacyWorkContribution.extend({
727
+ container: legacyWorkContribution.shape.container.describe(
728
+ "Deprecated. Resolve containers dynamically with describe_container."
729
+ ).optional(),
730
+ fields: legacyWorkContribution.shape.fields.describe("Deprecated. Resolve fields dynamically with describe_container.").optional(),
731
+ comments: z.union([
732
+ z.enum(["two-way", "none"]).describe("Deprecated. Declare a comments capability object."),
733
+ z.object({
734
+ direction: workDirection,
735
+ supportsEdits: z.literal(false),
736
+ supportsDeletes: z.literal(false)
737
+ }).strict()
738
+ ]),
739
+ status: z.object({
740
+ direction: workDirection,
741
+ webhook: z.string().regex(DOMAIN_EVENT).optional(),
742
+ map: z.object({
743
+ open: statusNames("not started"),
744
+ in_progress: statusNames("under way"),
745
+ done: statusNames("finished")
746
+ }).strict().describe(
747
+ "Deprecated. Resolve statuses dynamically with describe_container."
748
+ ).optional()
749
+ }).strict().optional(),
750
+ auth: workAuth.describe("How owners authenticate the work destination.")
751
+ });
752
+ var workContribution = z.union([
753
+ currentWorkContribution,
754
+ legacyWorkContribution.describe(
755
+ "Deprecated SDK v2 work declaration. Migrate to work capabilities and auth."
756
+ )
757
+ ]);
702
758
  var listing = z.object({
703
759
  longDescription: z.string().min(1).max(5e3).describe("The body of your listing page, in markdown.").optional(),
704
760
  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 +846,7 @@ var manifestObject = z.object({
790
846
  "What this extension offers the assistant: tools, mentions, actions, knowledge, skills, prompts and context providers."
791
847
  ).optional(),
792
848
  work: workContribution.describe(
793
- "The work destination this extension creates issues, pages or messages in. Needs the `work:create` and `work:sync` scopes."
849
+ "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
850
  ).optional()
795
851
  }).strict().describe(
796
852
  "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 +1067,36 @@ var manifestSchema = manifestObject.superRefine((manifest, ctx) => {
1011
1067
  }
1012
1068
  }
1013
1069
  if (contributes.work) {
1070
+ const work = contributes.work;
1071
+ const legacy = isLegacyWork(work);
1072
+ if (!legacy || !granted.has("reviews:read")) {
1073
+ requireScope(
1074
+ "review.threads:read",
1075
+ ["scopes"],
1076
+ "the work destination reads Review threads."
1077
+ );
1078
+ }
1014
1079
  requireScope(
1015
1080
  "work:create",
1016
1081
  ["scopes"],
1017
1082
  "the work destination creates items."
1018
1083
  );
1019
- requireScope(
1020
- "work:sync",
1021
- ["scopes"],
1022
- "the work destination syncs comments and status."
1023
- );
1084
+ const comments = typeof work.comments === "string" ? work.comments : work.comments.direction;
1085
+ const status = work.status ? "direction" in work.status ? work.status.direction : "from_provider" : "none";
1086
+ if (comments !== "none" || status !== "none") {
1087
+ requireScope(
1088
+ "work:sync",
1089
+ ["scopes"],
1090
+ "the work destination syncs comments or status."
1091
+ );
1092
+ }
1093
+ if (work.identity === "user") {
1094
+ ctx.addIssue({
1095
+ code: "custom",
1096
+ path: ["contributes", "work", "identity"],
1097
+ message: 'Per-user work identity is not supported. Set identity to "install".'
1098
+ });
1099
+ }
1024
1100
  }
1025
1101
  const needsServer = contributes.connector !== void 0 || (contributes.webhooks?.length ?? 0) > 0 || ai !== void 0 || contributes.work !== void 0 || manifest.server?.procedures !== void 0;
1026
1102
  if (needsServer && !manifest.server?.baseUrl) {
@@ -1095,10 +1171,176 @@ function validateManifest(value) {
1095
1171
  }));
1096
1172
  return { ok: false, issues };
1097
1173
  }
1174
+ function isLegacyWork(work) {
1175
+ return !("auth" in work);
1176
+ }
1177
+
1178
+ // src/vite/preview/plugin.ts
1179
+ import { randomUUID } from "crypto";
1180
+ import { isAbsolute, relative, resolve } from "path";
1181
+ import { normalizePath } from "vite";
1182
+ var PREVIEW_CLIENT_ID = "virtual:tappify-preview/client";
1183
+ var PREVIEW_MANIFEST_ID = "virtual:tappify-preview/manifest";
1184
+ var PREVIEW_REMOTE_ID = "virtual:tappify-preview/remote";
1185
+ var PREVIEW_REMOTE_ENV = "TAPPIFY_PREVIEW_REMOTE";
1186
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1187
+ var VIRTUAL_IDS = /* @__PURE__ */ new Set([
1188
+ PREVIEW_CLIENT_ID,
1189
+ PREVIEW_MANIFEST_ID,
1190
+ PREVIEW_REMOTE_ID
1191
+ ]);
1192
+ function previewRemote(remote, port) {
1193
+ if (remote === void 0 || remote.trim() === "") {
1194
+ return { ok: true, url: `http://localhost:${String(port)}/remoteEntry.js` };
1195
+ }
1196
+ let parsed;
1197
+ try {
1198
+ parsed = new URL(remote);
1199
+ } catch {
1200
+ return { ok: false, message: "The preview remote is not a valid URL." };
1201
+ }
1202
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname))) {
1203
+ return {
1204
+ ok: false,
1205
+ message: "A preview remote must use HTTPS or loopback HTTP."
1206
+ };
1207
+ }
1208
+ return { ok: true, url: parsed.toString() };
1209
+ }
1210
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1211
+ function messageDocument(title2, messages) {
1212
+ return `<!doctype html>
1213
+ <html lang="en">
1214
+ <head>
1215
+ <meta charset="UTF-8" />
1216
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1217
+ <title>${escapeHtml(title2)} \xB7 Tappify local preview</title>
1218
+ <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>
1219
+ </head>
1220
+ <body><main><h1>${escapeHtml(title2)}</h1><ul>${messages.map((message) => `<li>${escapeHtml(message)}</li>`).join("")}</ul></main></body>
1221
+ </html>`;
1222
+ }
1223
+ function previewDocument(manifest, remote) {
1224
+ if (!manifest.ok) {
1225
+ return messageDocument(
1226
+ "Manifest is invalid",
1227
+ manifest.issues.map((issue) => `${issue.path}: ${issue.message}`)
1228
+ );
1229
+ }
1230
+ if (!remote.ok)
1231
+ return messageDocument("Preview remote was refused", [remote.message]);
1232
+ const icon = manifest.manifest.icon.replace(/^\.\//, "");
1233
+ return `<!doctype html>
1234
+ <html lang="en">
1235
+ <head>
1236
+ <meta charset="UTF-8" />
1237
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1238
+ <link rel="icon" href="/${escapeHtml(icon)}" />
1239
+ <title>${escapeHtml(manifest.manifest.name)} \xB7 Tappify local preview</title>
1240
+ </head>
1241
+ <body>
1242
+ <div id="tappify-preview-root"></div>
1243
+ <script type="module" src="/@id/${PREVIEW_CLIENT_ID}"></script>
1244
+ </body>
1245
+ </html>`;
1246
+ }
1247
+ var moduleSource = (value) => `export default ${JSON.stringify(value)};`;
1248
+ function optimizedReactUrl(root, cacheDir, cacheGeneration) {
1249
+ const depsCacheDir = normalizePath(resolve(cacheDir, "deps"));
1250
+ const relativeCacheDir = normalizePath(relative(root, depsCacheDir));
1251
+ const prefix = isAbsolute(relativeCacheDir) || relativeCacheDir.startsWith("../") ? `/@fs/${depsCacheDir.replace(/^\/+/, "")}` : `/${relativeCacheDir}`;
1252
+ return `${prefix}/react.js?tappify=${encodeURIComponent(cacheGeneration)}`;
1253
+ }
1254
+ function createPreviewPlugins(options) {
1255
+ const remote = previewRemote(options.remote, options.port);
1256
+ const cacheGeneration = randomUUID();
1257
+ let reactUrl = optimizedReactUrl(
1258
+ resolve(options.manifestFile, ".."),
1259
+ resolve(options.manifestFile, "../node_modules/.vite"),
1260
+ cacheGeneration
1261
+ );
1262
+ const preview = {
1263
+ name: "tappify:preview",
1264
+ apply: "serve",
1265
+ config() {
1266
+ return {
1267
+ optimizeDeps: {
1268
+ exclude: ["@tappify/extension-sdk/vite/preview/bootstrap"]
1269
+ }
1270
+ };
1271
+ },
1272
+ configResolved(config) {
1273
+ reactUrl = optimizedReactUrl(
1274
+ config.root,
1275
+ config.cacheDir,
1276
+ cacheGeneration
1277
+ );
1278
+ },
1279
+ resolveId(id) {
1280
+ if (VIRTUAL_IDS.has(id)) return `\0${id}`;
1281
+ if (id.startsWith("\0") && VIRTUAL_IDS.has(id.slice(1))) return id;
1282
+ return null;
1283
+ },
1284
+ load(id) {
1285
+ if (id === `\0${PREVIEW_MANIFEST_ID}`) {
1286
+ return moduleSource(options.loadManifest());
1287
+ }
1288
+ if (id === `\0${PREVIEW_REMOTE_ID}`) return moduleSource(remote);
1289
+ if (id !== `\0${PREVIEW_CLIENT_ID}`) return null;
1290
+ return [
1291
+ `import { startLocalPreview } from '@tappify/extension-sdk/vite/preview/bootstrap';`,
1292
+ `import manifest from '${PREVIEW_MANIFEST_ID}';`,
1293
+ `import remote from '${PREVIEW_REMOTE_ID}';`,
1294
+ `const mount = document.getElementById('tappify-preview-root');`,
1295
+ `if (mount && manifest.ok && remote.ok) void startLocalPreview({ manifest: manifest.manifest, remote: remote.url, reactUrl: ${JSON.stringify(reactUrl)}, mount });`
1296
+ ].join("\n");
1297
+ },
1298
+ configureServer(server) {
1299
+ server.middlewares.use((request, response, next) => {
1300
+ const path = request.url?.split("?", 1)[0];
1301
+ if (path !== "/" && path !== "/index.html") return next();
1302
+ void server.transformIndexHtml(
1303
+ path,
1304
+ previewDocument(options.loadManifest(), remote)
1305
+ ).then((html) => {
1306
+ response.statusCode = 200;
1307
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
1308
+ response.end(html);
1309
+ }).catch(next);
1310
+ });
1311
+ }
1312
+ };
1313
+ const dev = {
1314
+ name: "tappify:dev",
1315
+ apply: "serve",
1316
+ config() {
1317
+ return {
1318
+ server: {
1319
+ host: "127.0.0.1",
1320
+ port: options.port,
1321
+ strictPort: true,
1322
+ cors: { origin: true },
1323
+ origin: `http://localhost:${String(options.port)}`
1324
+ }
1325
+ };
1326
+ },
1327
+ configureServer(server) {
1328
+ server.watcher.add(options.manifestFile);
1329
+ },
1330
+ async hotUpdate(context) {
1331
+ if (this.environment.name !== "client") return;
1332
+ if (context.file !== options.manifestFile) return;
1333
+ const manifest = options.loadManifest();
1334
+ await options.onManifestChange?.(manifest);
1335
+ context.server.ws.send({ type: "full-reload", path: "*" });
1336
+ }
1337
+ };
1338
+ return [preview, dev];
1339
+ }
1098
1340
 
1099
1341
  // src/vite/styles.ts
1100
1342
  import { existsSync, readFileSync } from "fs";
1101
- import { dirname, extname, resolve, sep } from "path";
1343
+ import { dirname, extname, resolve as resolve2, sep } from "path";
1102
1344
  var IMPORT_PATTERN = /(?:^|\n)\s*(?:import|export)(?:[^'"]*?from\s*)?\s*['"]([^'"]+)['"]/g;
1103
1345
  var IMPORT_SPECIFIER_PATTERN = /((?:^|\n)\s*(?:import|export)(?:[^'"]*?from\s*)?\s*['"])([^'"]+)(['"])/g;
1104
1346
  var BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
@@ -1111,14 +1353,14 @@ function isPlainStylesheet(specifier) {
1111
1353
  return specifier.endsWith(".css") && !specifier.endsWith(".module.css");
1112
1354
  }
1113
1355
  function resolveSource(specifier, importer) {
1114
- const base = resolve(dirname(importer), specifier);
1356
+ const base = resolve2(dirname(importer), specifier);
1115
1357
  if (extname(base) !== "" && existsSync(base)) return base;
1116
1358
  for (const extension of SOURCE_EXTENSIONS) {
1117
1359
  const candidate = `${base}${extension}`;
1118
1360
  if (existsSync(candidate)) return candidate;
1119
1361
  }
1120
1362
  for (const extension of SOURCE_EXTENSIONS) {
1121
- const candidate = resolve(base, `index${extension}`);
1363
+ const candidate = resolve2(base, `index${extension}`);
1122
1364
  if (existsSync(candidate)) return candidate;
1123
1365
  }
1124
1366
  return null;
@@ -1140,7 +1382,7 @@ function collectEntryStyles(entryAbsolutePath, root) {
1140
1382
  while (match !== null) {
1141
1383
  const specifier = match[1];
1142
1384
  if (specifier.endsWith(".css")) {
1143
- const value = specifier.startsWith(".") ? resolve(dirname(file), specifier) : specifier;
1385
+ const value = specifier.startsWith(".") ? resolve2(dirname(file), specifier) : specifier;
1144
1386
  if (!seenStyles.has(value)) {
1145
1387
  seenStyles.add(value);
1146
1388
  styles.push(value);
@@ -1173,38 +1415,52 @@ var DEFAULT_PORT = 5273;
1173
1415
  var SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
1174
1416
  var federationOptionsByPlugins = /* @__PURE__ */ new WeakMap();
1175
1417
  function manifestFile(options) {
1176
- return options.manifestPath ?? resolve2(options.root ?? process.cwd(), "tappify.extension.json");
1418
+ return options.manifestPath ?? resolve3(options.root ?? process.cwd(), "tappify.extension.json");
1177
1419
  }
1178
1420
  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:
1421
+ let raw;
1422
+ try {
1423
+ raw = options.manifest ?? JSON.parse(readFileSync2(manifestFile(options), "utf8"));
1424
+ } catch (error) {
1425
+ return {
1426
+ ok: false,
1427
+ issues: [
1428
+ {
1429
+ path: "$",
1430
+ message: error instanceof Error ? `The manifest is not valid JSON: ${error.message}` : "The manifest is not valid JSON."
1431
+ }
1432
+ ]
1433
+ };
1434
+ }
1435
+ return validateManifest(raw);
1436
+ }
1437
+ function manifestError(result) {
1438
+ if (result.ok) throw new Error("A valid manifest has no validation error.");
1439
+ const lines = result.issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n");
1440
+ return new TapError(
1441
+ "TAP_MANIFEST_INVALID",
1442
+ `tappify.extension.json does not validate:
1186
1443
  ${lines}
1187
1444
  Fix the fields above, or run \`tappify extension doctor\` for the same list with suggested fixes.`
1188
- );
1189
- }
1190
- return result.manifest;
1445
+ );
1191
1446
  }
1192
1447
  function tappifyExtension(options = {}) {
1193
1448
  const root = options.root ?? process.cwd();
1194
1449
  const boundary = root.endsWith(sep2) ? root : `${root}${sep2}`;
1195
- const manifest = loadManifest(options);
1196
- const contributions = uiContributions(manifest);
1450
+ const loaded = loadManifest(options);
1451
+ const manifest = loaded.ok ? loaded.manifest : null;
1452
+ const contributions = manifest === null ? [] : uiContributions(manifest);
1197
1453
  const port = options.port ?? DEFAULT_PORT;
1198
1454
  const entryByKey = /* @__PURE__ */ new Map();
1199
1455
  const exposes = {};
1200
1456
  for (const contribution of contributions) {
1201
1457
  const key = contribution.expose.slice("./".length);
1202
- entryByKey.set(key, resolve2(root, contribution.entry));
1458
+ entryByKey.set(key, resolve3(root, contribution.entry));
1203
1459
  exposes[contribution.expose] = `${VIRTUAL_PREFIX}${key}`;
1204
1460
  }
1205
1461
  const singleton = { singleton: true, requiredVersion: false };
1206
1462
  const federationOptions = {
1207
- name: remoteName(manifest.id),
1463
+ name: remoteName(manifest?.id ?? "invalid-extension"),
1208
1464
  filename: "remoteEntry.js",
1209
1465
  manifest: true,
1210
1466
  exposes,
@@ -1260,6 +1516,16 @@ function tappifyExtension(options = {}) {
1260
1516
  ].join("\n");
1261
1517
  }
1262
1518
  };
1519
+ const guard = {
1520
+ name: "tappify:manifest",
1521
+ enforce: "pre",
1522
+ config(_config, environment) {
1523
+ if (environment.command !== "build") return null;
1524
+ const current = loadManifest(options);
1525
+ if (!current.ok) throw manifestError(current);
1526
+ return null;
1527
+ }
1528
+ };
1263
1529
  const css = {
1264
1530
  name: "tappify:css",
1265
1531
  enforce: "pre",
@@ -1272,43 +1538,32 @@ function tappifyExtension(options = {}) {
1272
1538
  return next === null ? null : { code: next, map: null };
1273
1539
  }
1274
1540
  };
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
1541
  const plugins = [
1542
+ guard,
1300
1543
  expose,
1301
1544
  css,
1302
1545
  ...federation(federationOptions),
1303
- dev
1546
+ ...createPreviewPlugins({
1547
+ manifestFile: manifestFile(options),
1548
+ loadManifest: () => loadManifest(options),
1549
+ port,
1550
+ remote: process.env[PREVIEW_REMOTE_ENV],
1551
+ onManifestChange: async (result) => {
1552
+ if (result.ok && options.onManifestChange !== void 0) {
1553
+ await options.onManifestChange(result.manifest);
1554
+ }
1555
+ }
1556
+ })
1304
1557
  ];
1305
1558
  federationOptionsByPlugins.set(plugins, federationOptions);
1306
1559
  return plugins;
1307
1560
  }
1308
1561
  export {
1562
+ PREVIEW_REMOTE_ENV,
1309
1563
  VIRTUAL_PREFIX,
1310
1564
  collectEntryStyles,
1311
1565
  inlineCssImports,
1566
+ previewRemote,
1312
1567
  tappifyExtension
1313
1568
  };
1314
1569
  //# sourceMappingURL=vite.mjs.map