fundus 0.28.1 → 0.29.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.
Files changed (179) hide show
  1. package/README.md +41 -33
  2. package/build/client/_app/immutable/chunks/{OL1Dh1oP.js → BCmR1_e0.js} +1 -1
  3. package/build/client/_app/immutable/chunks/BCmR1_e0.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/BCmR1_e0.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{apKHI4vq.js → C0uU_Rhe.js} +2 -2
  6. package/build/client/_app/immutable/chunks/C0uU_Rhe.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/C0uU_Rhe.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{g2rma9Ym.js → CMJ10zbJ.js} +1 -1
  9. package/build/client/_app/immutable/chunks/CMJ10zbJ.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/CMJ10zbJ.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/{QlQ-jeIO.js → kkVEh3_1.js} +1 -1
  12. package/build/client/_app/immutable/chunks/kkVEh3_1.js.br +2 -0
  13. package/build/client/_app/immutable/chunks/kkVEh3_1.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/wBLhaPea.js +1 -0
  15. package/build/client/_app/immutable/chunks/wBLhaPea.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/wBLhaPea.js.gz +0 -0
  17. package/build/client/_app/immutable/entry/{app.DpZM49_s.js → app.DCXSqiQ1.js} +2 -2
  18. package/build/client/_app/immutable/entry/app.DCXSqiQ1.js.br +0 -0
  19. package/build/client/_app/immutable/entry/app.DCXSqiQ1.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/start.BOUpxuMU.js +1 -0
  21. package/build/client/_app/immutable/entry/start.BOUpxuMU.js.br +0 -0
  22. package/build/client/_app/immutable/entry/start.BOUpxuMU.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/{0.Wqvn8XD-.js → 0.CsqkK05g.js} +1 -1
  24. package/build/client/_app/immutable/nodes/0.CsqkK05g.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/0.CsqkK05g.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{1.Z-UvvLkP.js → 1.BVSXvtU9.js} +1 -1
  27. package/build/client/_app/immutable/nodes/1.BVSXvtU9.js.br +2 -0
  28. package/build/client/_app/immutable/nodes/1.BVSXvtU9.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{2.lu89W_qJ.js → 2.BJEXWmrT.js} +1 -1
  30. package/build/client/_app/immutable/nodes/2.BJEXWmrT.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/2.BJEXWmrT.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{3.Dt32EAoq.js → 3.BDMmDyL1.js} +1 -1
  33. package/build/client/_app/immutable/nodes/3.BDMmDyL1.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/3.BDMmDyL1.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{4.WipzcX8_.js → 4.dd49_qQE.js} +1 -1
  36. package/build/client/_app/immutable/nodes/4.dd49_qQE.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/4.dd49_qQE.js.gz +0 -0
  38. package/build/client/_app/version.json +1 -1
  39. package/build/client/_app/version.json.br +0 -0
  40. package/build/client/_app/version.json.gz +0 -0
  41. package/build/handler.js +7 -7
  42. package/build/index.js +7 -7
  43. package/build/server/chunks/chunks/{context.js-BZSZNuAd.js → context.js-DFZgDxCD.js} +157 -95
  44. package/build/server/chunks/chunks/context.js-DFZgDxCD.js.map +1 -0
  45. package/build/server/chunks/chunks/{contract.js-B4JdXFgm.js → contract.js-DGZXpcNX.js} +7 -7
  46. package/build/server/chunks/chunks/{contract.js-B4JdXFgm.js.map → contract.js-DGZXpcNX.js.map} +1 -1
  47. package/build/server/chunks/chunks/{core.js-BHxa3VBo.js → core.js-AGKlECJ0.js} +32 -32
  48. package/build/server/chunks/chunks/core.js-AGKlECJ0.js.map +1 -0
  49. package/build/server/chunks/chunks/{exports.js-D0dALD2Q.js → exports.js-BsVKSGJk.js} +2 -2
  50. package/build/server/chunks/chunks/{exports.js-D0dALD2Q.js.map → exports.js-BsVKSGJk.js.map} +1 -1
  51. package/build/server/chunks/chunks/{http.js-DYYKvppy.js → http.js-BQhjMaW9.js} +2 -2
  52. package/build/server/chunks/chunks/{http.js-DYYKvppy.js.map → http.js-BQhjMaW9.js.map} +1 -1
  53. package/build/server/chunks/chunks/{internal.js-CJkB_gzV.js → internal.js-DJGcNryj.js} +3 -3
  54. package/build/server/chunks/chunks/{internal.js-CJkB_gzV.js.map → internal.js-DJGcNryj.js.map} +1 -1
  55. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  56. package/build/server/chunks/chunks/{manifest-membership.js-BkrPsM_r.js → manifest-membership.js-DkHaPk2F.js} +2 -2
  57. package/build/server/chunks/chunks/{manifest-membership.js-BkrPsM_r.js.map → manifest-membership.js-DkHaPk2F.js.map} +1 -1
  58. package/build/server/chunks/chunks/{raw-reconciliation.js-DPnNXe0b.js → raw-reconciliation.js-D2sI2C-t.js} +3 -3
  59. package/build/server/chunks/chunks/raw-reconciliation.js-D2sI2C-t.js.map +1 -0
  60. package/build/server/chunks/chunks/{raw-reconciliation2.js-DKOpJ8if.js → raw-reconciliation2.js-CMpUvuMd.js} +2 -2
  61. package/build/server/chunks/chunks/{raw-reconciliation2.js-DKOpJ8if.js.map → raw-reconciliation2.js-CMpUvuMd.js.map} +1 -1
  62. package/build/server/chunks/chunks/{shared.js-D3ZDVUlh.js → shared.js-BtmYg0tL.js} +2 -2
  63. package/build/server/chunks/chunks/{shared.js-D3ZDVUlh.js.map → shared.js-BtmYg0tL.js.map} +1 -1
  64. package/build/server/chunks/chunks/{utils.js-CqWW_v-5.js → utils.js-CqXk0Ug6.js} +3 -3
  65. package/build/server/chunks/chunks/{utils.js-CqWW_v-5.js.map → utils.js-CqXk0Ug6.js.map} +1 -1
  66. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-Dw2MW5Tm.js → _server.ts.js-DKGD9Jk2.js} +6 -6
  67. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-Dw2MW5Tm.js.map → _server.ts.js-DKGD9Jk2.js.map} +1 -1
  68. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-GQDuJGug.js → _server.ts.js-C1UfF0EO.js} +7 -7
  69. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-GQDuJGug.js.map → _server.ts.js-C1UfF0EO.js.map} +1 -1
  70. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-JbLSv7Xx.js → _server.ts.js-DpASkk00.js} +7 -7
  71. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-JbLSv7Xx.js.map → _server.ts.js-DpASkk00.js.map} +1 -1
  72. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-BPae-BYF.js → _server.ts.js-C3jU2vC8.js} +7 -7
  73. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-BPae-BYF.js.map → _server.ts.js-C3jU2vC8.js.map} +1 -1
  74. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-BafrrUxu.js → _server.ts.js-B7OwufF3.js} +7 -7
  75. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-BafrrUxu.js.map → _server.ts.js-B7OwufF3.js.map} +1 -1
  76. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-UOlSNEi9.js → _server.ts.js-COhg1nYk.js} +7 -7
  77. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-UOlSNEi9.js.map → _server.ts.js-COhg1nYk.js.map} +1 -1
  78. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QE1vUuAx.js → _server.ts.js-DAEE17eM.js} +7 -7
  79. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QE1vUuAx.js.map → _server.ts.js-DAEE17eM.js.map} +1 -1
  80. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DIVfppGt.js → _server.ts.js-BrZSS4gm.js} +7 -7
  81. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DIVfppGt.js.map → _server.ts.js-BrZSS4gm.js.map} +1 -1
  82. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-B6R_kqP8.js → _server.ts.js-Cfst-Brd.js} +7 -7
  83. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-B6R_kqP8.js.map → _server.ts.js-Cfst-Brd.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CertbDsH.js → _server.ts.js-aN_h2Ifo.js} +7 -7
  85. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CertbDsH.js.map → _server.ts.js-aN_h2Ifo.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-DEIF3Hu2.js → _server.ts.js-CX2A66ij.js} +7 -7
  87. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-DEIF3Hu2.js.map → _server.ts.js-CX2A66ij.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-DNFsNGr4.js → _server.ts.js-CwTiaheP.js} +7 -7
  89. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-DNFsNGr4.js.map → _server.ts.js-CwTiaheP.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DDW8XRPe.js → _server.ts.js-fIbsr_2e.js} +7 -7
  91. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DDW8XRPe.js.map → _server.ts.js-fIbsr_2e.js.map} +1 -1
  92. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-C222-xli.js → _server.ts.js-Dvo1bsLi.js} +7 -7
  93. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-C222-xli.js.map → _server.ts.js-Dvo1bsLi.js.map} +1 -1
  94. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-CI1P1uyd.js → _server.ts.js-VLg1G3mN.js} +7 -7
  95. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-CI1P1uyd.js.map → _server.ts.js-VLg1G3mN.js.map} +1 -1
  96. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-DpiUwaSh.js → _server.ts.js-DqctsOop.js} +7 -7
  97. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-DpiUwaSh.js.map → _server.ts.js-DqctsOop.js.map} +1 -1
  98. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DT0VPqPT.js → _server.ts.js-CoxSU5iO.js} +7 -7
  99. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DT0VPqPT.js.map → _server.ts.js-CoxSU5iO.js.map} +1 -1
  100. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DBpHM42g.js → _server.ts.js-wUxsLZdr.js} +7 -7
  101. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DBpHM42g.js.map → _server.ts.js-wUxsLZdr.js.map} +1 -1
  102. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-Cxu5b6Au.js → _server.ts.js-BKsF0Oit.js} +7 -7
  103. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-Cxu5b6Au.js.map → _server.ts.js-BKsF0Oit.js.map} +1 -1
  104. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-W4dhMo8Y.js → _server.ts.js-Dk9oOZwM.js} +7 -7
  105. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-W4dhMo8Y.js.map → _server.ts.js-Dk9oOZwM.js.map} +1 -1
  106. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DJ_adxbT.js → _server.ts.js-DxjNmmtL.js} +7 -7
  107. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DJ_adxbT.js.map → _server.ts.js-DxjNmmtL.js.map} +1 -1
  108. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BisoHihD.js → _server.ts.js-DD0b38VV.js} +6 -6
  109. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BisoHihD.js.map → _server.ts.js-DD0b38VV.js.map} +1 -1
  110. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-D4jLLuko.js → _server.ts.js-DId6F2i_.js} +7 -7
  111. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-D4jLLuko.js.map → _server.ts.js-DId6F2i_.js.map} +1 -1
  112. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-Dfv2ItjT.js → _server.ts.js-DKKkGoK2.js} +7 -7
  113. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-Dfv2ItjT.js.map → _server.ts.js-DKKkGoK2.js.map} +1 -1
  114. package/build/server/chunks/entries/{hooks.server.js-oHJX5INm.js → hooks.server.js-B5Ctvorj.js} +6 -6
  115. package/build/server/chunks/entries/{hooks.server.js-oHJX5INm.js.map → hooks.server.js-B5Ctvorj.js.map} +1 -1
  116. package/build/server/chunks/{handler-BAe0c9yw.js → handler-DwVfBx6r.js} +5 -5
  117. package/build/server/chunks/{handler-BAe0c9yw.js.map → handler-DwVfBx6r.js.map} +1 -1
  118. package/build/server/chunks/{index.js-DUYD4JB0.js → index.js-DgPuPQJn.js} +5 -5
  119. package/build/server/chunks/{index.js-DUYD4JB0.js.map → index.js-DgPuPQJn.js.map} +1 -1
  120. package/build/server/chunks/{manifest.js-zwun-T7O.js → manifest.js-B054EcAf.js} +31 -31
  121. package/build/server/chunks/{manifest.js-zwun-T7O.js.map → manifest.js-B054EcAf.js.map} +1 -1
  122. package/build/server/chunks/nodes/{0.js-BmkXwDRY.js → 0.js-DoHZTmnT.js} +2 -2
  123. package/build/server/chunks/nodes/{0.js-BmkXwDRY.js.map → 0.js-DoHZTmnT.js.map} +1 -1
  124. package/build/server/chunks/nodes/1.js-BcPlsloO.js +7 -0
  125. package/build/server/chunks/nodes/{1.js-xbOwEHCW.js.map → 1.js-BcPlsloO.js.map} +1 -1
  126. package/build/server/chunks/nodes/{2.js-BuNw1vTZ.js → 2.js-5qCXkvzl.js} +2 -2
  127. package/build/server/chunks/nodes/{2.js-BuNw1vTZ.js.map → 2.js-5qCXkvzl.js.map} +1 -1
  128. package/build/server/chunks/nodes/3.js-D6fAMX1w.js +7 -0
  129. package/build/server/chunks/nodes/{3.js-Dpr_DvNU.js.map → 3.js-D6fAMX1w.js.map} +1 -1
  130. package/build/server/chunks/nodes/4.js-CGkeT0SQ.js +7 -0
  131. package/build/server/chunks/nodes/{4.js-14Lypd4o.js.map → 4.js-CGkeT0SQ.js.map} +1 -1
  132. package/dist/cli.js +3 -4
  133. package/dist/core/figma-tag-links.d.ts +66 -0
  134. package/dist/core/figma-tag-links.js +97 -0
  135. package/dist/core/index.d.ts +2 -2
  136. package/dist/core/index.js +1 -1
  137. package/dist/server/cli-commands.d.ts +1 -1
  138. package/dist/server/cli-commands.js +1 -1
  139. package/dist/server/cli-figma-tags.d.ts +1 -1
  140. package/dist/server/cli-figma-tags.js +149 -88
  141. package/dist/server/figma-tags.d.ts +72 -24
  142. package/dist/server/figma-tags.js +130 -79
  143. package/dist/server/importers/figma.d.ts +20 -15
  144. package/dist/server/importers/figma.js +172 -92
  145. package/package.json +1 -1
  146. package/build/client/_app/immutable/chunks/Bsj1ssvc.js +0 -1
  147. package/build/client/_app/immutable/chunks/Bsj1ssvc.js.br +0 -0
  148. package/build/client/_app/immutable/chunks/Bsj1ssvc.js.gz +0 -0
  149. package/build/client/_app/immutable/chunks/OL1Dh1oP.js.br +0 -0
  150. package/build/client/_app/immutable/chunks/OL1Dh1oP.js.gz +0 -0
  151. package/build/client/_app/immutable/chunks/QlQ-jeIO.js.br +0 -2
  152. package/build/client/_app/immutable/chunks/QlQ-jeIO.js.gz +0 -0
  153. package/build/client/_app/immutable/chunks/apKHI4vq.js.br +0 -0
  154. package/build/client/_app/immutable/chunks/apKHI4vq.js.gz +0 -0
  155. package/build/client/_app/immutable/chunks/g2rma9Ym.js.br +0 -0
  156. package/build/client/_app/immutable/chunks/g2rma9Ym.js.gz +0 -0
  157. package/build/client/_app/immutable/entry/app.DpZM49_s.js.br +0 -0
  158. package/build/client/_app/immutable/entry/app.DpZM49_s.js.gz +0 -0
  159. package/build/client/_app/immutable/entry/start.B3EMVPEN.js +0 -1
  160. package/build/client/_app/immutable/entry/start.B3EMVPEN.js.br +0 -0
  161. package/build/client/_app/immutable/entry/start.B3EMVPEN.js.gz +0 -0
  162. package/build/client/_app/immutable/nodes/0.Wqvn8XD-.js.br +0 -0
  163. package/build/client/_app/immutable/nodes/0.Wqvn8XD-.js.gz +0 -0
  164. package/build/client/_app/immutable/nodes/1.Z-UvvLkP.js.br +0 -0
  165. package/build/client/_app/immutable/nodes/1.Z-UvvLkP.js.gz +0 -0
  166. package/build/client/_app/immutable/nodes/2.lu89W_qJ.js.br +0 -0
  167. package/build/client/_app/immutable/nodes/2.lu89W_qJ.js.gz +0 -0
  168. package/build/client/_app/immutable/nodes/3.Dt32EAoq.js.br +0 -0
  169. package/build/client/_app/immutable/nodes/3.Dt32EAoq.js.gz +0 -0
  170. package/build/client/_app/immutable/nodes/4.WipzcX8_.js.br +0 -0
  171. package/build/client/_app/immutable/nodes/4.WipzcX8_.js.gz +0 -0
  172. package/build/server/chunks/chunks/context.js-BZSZNuAd.js.map +0 -1
  173. package/build/server/chunks/chunks/core.js-BHxa3VBo.js.map +0 -1
  174. package/build/server/chunks/chunks/raw-reconciliation.js-DPnNXe0b.js.map +0 -1
  175. package/build/server/chunks/nodes/1.js-xbOwEHCW.js +0 -7
  176. package/build/server/chunks/nodes/3.js-Dpr_DvNU.js +0 -7
  177. package/build/server/chunks/nodes/4.js-14Lypd4o.js +0 -7
  178. package/dist/core/figma-tag-script.d.ts +0 -154
  179. package/dist/core/figma-tag-script.js +0 -248
@@ -1,13 +1,12 @@
1
1
  /**
2
- * Planning for the `fundus figma prepare-*-tag` commands. The Figma REST API
3
- * can read shared plugin data but not write it, so Fundus validates against a
4
- * REST snapshot of the file plus the local library, then hands back a
5
- * `use_figma` script (figma-tag-script.ts) that performs the write inside
6
- * Figma and re-checks the live document first.
2
+ * Planning and REST writes for `fundus figma set-tag` / `delete-tag`. Fundus
3
+ * confirms the file's tag links against the nodes that still exist (one link
4
+ * list plus one `/nodes` call), plans purely on that snapshot and the library,
5
+ * then writes the links in an order where an interruption can at worst leave
6
+ * a tag on two nodes (`ambiguous-tag`), never lose it.
7
7
  */
8
- import { assetIdError, buildDeleteTagScript, buildSetTagScript } from '../core/index.js';
9
- import { fetchFigmaDocument, figmaDocumentNodes, validateFigmaSourceParameters } from './importers/figma.js';
10
- import { LibraryStore } from './library-store.js';
8
+ import { assetIdError } from '../core/index.js';
9
+ import { createFigmaTagLink, deleteFigmaDevResource, fetchFigmaNodeSummaries, listFigmaTagEntries, validateFigmaSourceParameters } from './importers/figma.js';
11
10
  /** An expected refusal that carries the file's current tags so the caller can pick another. */
12
11
  export class FigmaTagConflictError extends Error {
13
12
  existingTags;
@@ -16,26 +15,24 @@ export class FigmaTagConflictError extends Error {
16
15
  this.existingTags = existingTags;
17
16
  }
18
17
  }
19
- /** Normalize a `12:34` / `12-34` node id; Fundus imports only this id form. */
18
+ /** Normalize a `12:34` / `12-34` node id; Fundus tags only this id form. */
20
19
  export function parseFigmaNodeId(value) {
21
20
  if (!/^\d+[-:]\d+$/.test(value)) {
22
- throw new Error(`"${value}" is not a taggable Figma node id — use the form 12:34 (or 12-34 as in a node-id link parameter). Nodes inside instances (ids starting with "I") inherit their tag from the main component; tag the node there.`);
21
+ throw new Error(`"${value}" is not a taggable Figma node id — use the form 12:34 (or 12-34 as in a node-id link parameter). Layers inside instances (ids starting with "I") cannot be tagged.`);
23
22
  }
24
23
  return value.replace('-', ':');
25
24
  }
26
- function tagListing(nodes) {
27
- return nodes
28
- .filter((node) => node.tag !== null)
29
- .map((node) => ({
30
- tag: node.tag,
31
- nodeId: node.nodeId,
32
- nodeName: node.nodeName,
33
- page: node.page
34
- }))
35
- .sort((a, b) => a.tag.localeCompare(b.tag));
25
+ function tagListing(tags) {
26
+ return tags.map(({ tag, nodeId, nodeName }) => ({ tag, nodeId, nodeName }));
36
27
  }
37
28
  function describeNode(node) {
38
- return `"${node.nodeName}" (${node.nodeId}) on page "${node.page}"`;
29
+ return `"${node.nodeName}" (${node.nodeId})`;
30
+ }
31
+ function quotedTags(tags) {
32
+ return `${tags.length === 1 ? 'tag' : 'tags'} ${tags.map((entry) => `"${entry.tag}"`).join(', ')}`;
33
+ }
34
+ function deleteWrite({ nodeId, nodeName, tag, linkId }) {
35
+ return { action: 'delete', nodeId, nodeName, tag, linkId };
39
36
  }
40
37
  /** Whether `record` is a node-mode Figma import of `nodeId` in `fileKey`. */
41
38
  function isNodeImportOf(record, fileKey, nodeId) {
@@ -54,21 +51,32 @@ function tagImportedAsset(library, fileKey, tag) {
54
51
  ? tag
55
52
  : null;
56
53
  }
57
- export function planSetFigmaTag(request, { library, document }) {
54
+ export function planSetFigmaTag(request, { library, snapshot }) {
58
55
  const { fileKey, nodeId, tag, force } = request;
59
56
  const problem = assetIdError(tag);
60
57
  if (problem)
61
58
  throw new Error(problem);
62
- const nodes = figmaDocumentNodes(document);
63
- const existingTags = tagListing(nodes);
59
+ const { tags, target } = snapshot;
60
+ if (!target)
61
+ throw new Error(`Node ${nodeId} does not exist in Figma file "${fileKey}".`);
62
+ if (target.nodeType === 'DOCUMENT' || target.nodeType === 'CANVAS') {
63
+ throw new Error(`Node ${describeNode(target)} is a ${target.nodeType} node and cannot be tagged — tag a layer on the page.`);
64
+ }
65
+ const existingTags = tagListing(tags);
64
66
  const warnings = [];
65
- const holders = nodes.filter((node) => node.tag === tag && node.nodeId !== nodeId);
66
- if (holders.length > 0) {
67
- const described = holders.map(describeNode).join(', ');
67
+ const others = tags.filter((entry) => entry.tag === tag && entry.nodeId !== nodeId);
68
+ const ownOther = tags.filter((entry) => entry.nodeId === nodeId && entry.tag !== tag);
69
+ const carries = tags.some((entry) => entry.nodeId === nodeId && entry.tag === tag);
70
+ if (others.length > 0) {
71
+ const described = others.map(describeNode).join(', ');
68
72
  if (!force) {
69
- throw new FigmaTagConflictError(`Tag "${tag}" is already used by ${described} — choose another tag, or pass --force to move it to node ${nodeId} (existing tags are listed).`, existingTags);
73
+ throw new FigmaTagConflictError(carries
74
+ ? `Tag "${tag}" is also on ${described} — pass --force to remove it there and keep it only on node ${describeNode(target)} (existing tags are listed).`
75
+ : `Tag "${tag}" is already used by ${described} — choose another tag, or pass --force to move it to node ${describeNode(target)} (existing tags are listed).`, existingTags);
70
76
  }
71
- warnings.push(`Moves tag "${tag}" from ${described} to node ${nodeId}.`);
77
+ warnings.push(carries
78
+ ? `Removes tag "${tag}" from ${described}; it stays on node ${describeNode(target)}.`
79
+ : `Moves tag "${tag}" from ${described} to node ${describeNode(target)}.`);
72
80
  }
73
81
  const existing = Object.hasOwn(library.assets, tag) ? library.assets[tag] : null;
74
82
  if (existing && tagImportedAsset(library, fileKey, tag) === null) {
@@ -88,75 +96,77 @@ export function planSetFigmaTag(request, { library, document }) {
88
96
  warnings.push(`Fundus asset "${tag}" already exists from another source — afterwards run ${replace} to switch it to this tag; that replaces its current source.`);
89
97
  }
90
98
  }
91
- const node = nodes.find((candidate) => candidate.nodeId === nodeId) ?? null;
92
- if (!node) {
93
- warnings.push(`Node ${nodeId} is not in the Figma REST snapshot yet (new or unsynced node); the script verifies it exists before writing.`);
94
- }
95
- const previousTag = node?.tag ?? null;
96
- if (previousTag === tag) {
97
- if (!force) {
98
- throw new Error(`Node ${describeNode(node)} already carries tag "${tag}" — nothing to do. Pass --force to regenerate the script anyway (e.g. to repair its annotation).`);
99
- }
100
- warnings.push(`Node ${nodeId} already carries tag "${tag}"; the script rewrites it.`);
101
- }
102
- else if (previousTag !== null) {
103
- const orphaned = tagImportedAsset(library, fileKey, previousTag);
104
- const followUp = orphaned
105
- ? ` Fundus asset "${orphaned}" is imported from that tag — afterwards run \`fundus asset reimport ${orphaned} --figma-tag ${tag}\` (renames it to "${tag}").`
106
- : '';
99
+ if (ownOther.length > 0) {
100
+ const followUp = ownOther
101
+ .map((entry) => tagImportedAsset(library, fileKey, entry.tag))
102
+ .filter((orphaned) => orphaned !== null)
103
+ .map((orphaned) => ` Fundus asset "${orphaned}" is imported from that tag — afterwards run \`fundus asset reimport ${orphaned} --figma-tag ${tag}\` (renames it to "${tag}").`)
104
+ .join('');
107
105
  if (!force) {
108
- throw new Error(`Node ${describeNode(node)} already carries tag "${previousTag}". Pass --force to replace it.${followUp}`);
106
+ throw new FigmaTagConflictError(`Node ${describeNode(target)} already carries ${quotedTags(ownOther)}. Pass --force to replace ${ownOther.length === 1 ? 'it' : 'them'}.${followUp}`, existingTags);
109
107
  }
110
- warnings.push(`Replaces tag "${previousTag}" on node ${nodeId}.${followUp}`);
108
+ warnings.push(`Replaces ${quotedTags(ownOther)} on node ${describeNode(target)}.${followUp}`);
111
109
  }
112
- if (tagImportedAsset(library, fileKey, tag) !== null && previousTag !== tag) {
110
+ const writes = [
111
+ ...(carries ? [] : [{ action: 'create', nodeId, nodeName: target.nodeName, tag }]),
112
+ ...ownOther.map(deleteWrite),
113
+ ...others.map(deleteWrite)
114
+ ];
115
+ if (tagImportedAsset(library, fileKey, tag) !== null && writes.length > 0) {
113
116
  warnings.push(`Fundus asset "${tag}" is imported from this tag; its next \`fundus asset reimport ${tag}\` resolves to this node.`);
114
117
  }
115
118
  return {
116
119
  fileKey,
117
120
  nodeId,
121
+ nodeName: target.nodeName,
118
122
  tag,
119
- previousTag,
120
- warnings,
121
- code: buildSetTagScript({ nodeId, tag, force })
123
+ previousTag: ownOther[0]?.tag ?? (carries ? tag : null),
124
+ movedFrom: others.map(({ nodeId, nodeName }) => ({ nodeId, nodeName })),
125
+ writes,
126
+ warnings
122
127
  };
123
128
  }
124
- export function planDeleteFigmaTag(request, { library, document }) {
129
+ export function planDeleteFigmaTag(request, { library, snapshot }) {
125
130
  const { fileKey, force } = request;
126
131
  if (request.tag !== undefined) {
127
132
  const problem = assetIdError(request.tag);
128
133
  if (problem)
129
134
  throw new Error(problem);
130
135
  }
131
- const nodes = figmaDocumentNodes(document);
132
- const existingTags = tagListing(nodes);
133
- let node;
136
+ const { tags, target } = snapshot;
137
+ const existingTags = tagListing(tags);
138
+ let holders;
134
139
  if (request.nodeId !== undefined) {
135
- const found = nodes.find((candidate) => candidate.nodeId === request.nodeId);
136
- if (!found) {
137
- throw new Error(`Node ${request.nodeId} is not in Figma file "${fileKey}".`);
140
+ if (!target) {
141
+ throw new Error(`Node ${request.nodeId} does not exist in Figma file "${fileKey}".`);
138
142
  }
139
- if (found.tag === null) {
140
- throw new FigmaTagConflictError(`Node ${describeNode(found)} carries no Fundus tag (existing tags are listed).`, existingTags);
143
+ const own = tags.filter((entry) => entry.nodeId === request.nodeId);
144
+ if (own.length === 0) {
145
+ throw new FigmaTagConflictError(`Node ${describeNode(target)} carries no Fundus tag (existing tags are listed).`, existingTags);
141
146
  }
142
- if (request.tag !== undefined && found.tag !== request.tag) {
143
- throw new FigmaTagConflictError(`Node ${describeNode(found)} carries tag "${found.tag}", not "${request.tag}".`, existingTags);
147
+ holders = request.tag === undefined ? own : own.filter((entry) => entry.tag === request.tag);
148
+ if (holders.length === 0) {
149
+ throw new FigmaTagConflictError(`Node ${describeNode(target)} carries ${quotedTags(own)}, not "${request.tag}".`, existingTags);
150
+ }
151
+ if (new Set(holders.map((entry) => entry.tag)).size > 1) {
152
+ throw new FigmaTagConflictError(`Node ${describeNode(target)} carries ${quotedTags(own)} — pass --tag to pick one.`, existingTags);
144
153
  }
145
- node = found;
146
154
  }
147
155
  else {
148
- const matches = nodes.filter((candidate) => candidate.tag === request.tag);
149
- if (matches.length === 0) {
156
+ holders = tags.filter((entry) => entry.tag === request.tag);
157
+ if (holders.length === 0) {
150
158
  throw new FigmaTagConflictError(`No node carries tag "${request.tag}" in Figma file "${fileKey}" (existing tags are listed).`, existingTags);
151
159
  }
152
- if (matches.length > 1) {
153
- throw new FigmaTagConflictError(`Tag "${request.tag}" is on ${matches.length} nodes (${matches.map((match) => match.nodeId).join(', ')}) — pass --node-id to pick one.`, existingTags);
160
+ const nodeIds = [...new Set(holders.map((entry) => entry.nodeId))];
161
+ if (nodeIds.length > 1) {
162
+ throw new FigmaTagConflictError(`Tag "${request.tag}" is on ${nodeIds.length} nodes (${nodeIds.join(', ')}) — pass --node-id to pick one.`, existingTags);
154
163
  }
155
- node = matches[0];
156
164
  }
157
- const tag = node.tag;
165
+ const [holder] = holders;
158
166
  const warnings = [];
159
- const imported = tagImportedAsset(library, fileKey, tag);
167
+ // Deleting one of several holders fixes an ambiguous tag rather than losing it.
168
+ const keptElsewhere = tags.some((entry) => entry.tag === holder.tag && entry.nodeId !== holder.nodeId);
169
+ const imported = keptElsewhere ? null : tagImportedAsset(library, fileKey, holder.tag);
160
170
  if (imported) {
161
171
  const consequence = `Fundus asset "${imported}" is imported from this tag; without it, \`fundus asset reimport ${imported}\` fails until the tag is set again.`;
162
172
  if (!force)
@@ -165,15 +175,56 @@ export function planDeleteFigmaTag(request, { library, document }) {
165
175
  }
166
176
  return {
167
177
  fileKey,
168
- nodeId: node.nodeId,
169
- tag,
170
- warnings,
171
- code: buildDeleteTagScript({ nodeId: node.nodeId, tag })
178
+ nodeId: holder.nodeId,
179
+ nodeName: holder.nodeName,
180
+ tag: holder.tag,
181
+ writes: holders.map(deleteWrite),
182
+ warnings
172
183
  };
173
184
  }
174
- /** Load the library and the file's REST document for planning. */
175
- export async function loadFigmaTagPlanContext(config, fileKey) {
176
- const library = await new LibraryStore(config).read();
177
- const document = await fetchFigmaDocument(fileKey, { config, fetch: globalThis.fetch });
178
- return { library, document };
185
+ /**
186
+ * The file's tag links confirmed against the nodes that exist: one link list,
187
+ * then one `/nodes` call over every tagged node plus the target. Links on
188
+ * missing nodes are orphaned and dropped, so nothing ever touches them.
189
+ */
190
+ export async function loadFigmaTagSnapshot(context, fileKey, targetNodeId) {
191
+ const entries = await listFigmaTagEntries(fileKey, context);
192
+ const nodeIds = new Set(entries.map((entry) => entry.nodeId));
193
+ if (targetNodeId !== undefined)
194
+ nodeIds.add(targetNodeId);
195
+ const nodes = new Map((await fetchFigmaNodeSummaries(fileKey, [...nodeIds], context)).map((node) => [
196
+ node.nodeId,
197
+ node
198
+ ]));
199
+ const tags = [];
200
+ for (const { tag, nodeId, linkId } of entries) {
201
+ const node = nodes.get(nodeId);
202
+ if (node)
203
+ tags.push({ tag, nodeId, nodeName: node.nodeName, nodeType: node.nodeType, linkId });
204
+ }
205
+ return {
206
+ tags,
207
+ target: targetNodeId === undefined ? null : (nodes.get(targetNodeId) ?? null)
208
+ };
209
+ }
210
+ /** Runs the writes in order and stops at the first failure; retries live in the REST client. */
211
+ export async function applyFigmaTagWrites(fileKey, writes, context) {
212
+ for (const [index, write] of writes.entries()) {
213
+ try {
214
+ if (write.action === 'create') {
215
+ await createFigmaTagLink(fileKey, write.nodeId, write.tag, context);
216
+ }
217
+ else {
218
+ await deleteFigmaDevResource(fileKey, write.linkId, context);
219
+ }
220
+ }
221
+ catch (cause) {
222
+ return {
223
+ written: writes.slice(0, index),
224
+ failure: { write, message: cause instanceof Error ? cause.message : String(cause) },
225
+ notWritten: writes.slice(index)
226
+ };
227
+ }
228
+ }
229
+ return { written: [...writes], failure: null, notWritten: [] };
179
230
  }
@@ -1,3 +1,4 @@
1
+ import { type FigmaTagEntry } from '../../core/index.js';
1
2
  import { type ImporterContext, type ServerSourceImporter } from './contract.js';
2
3
  import { type PinnedHttpsTarget } from './pinned-https.js';
3
4
  export interface FigmaNodeSourceParameters {
@@ -10,9 +11,9 @@ export interface FigmaNodeSourceParameters {
10
11
  export interface FigmaTagSourceParameters {
11
12
  mode: 'tag';
12
13
  fileKey: string;
13
- /** The Fundus asset id carried in Figma as `sharedPluginData.fundus.assetId`. */
14
+ /** The Fundus asset id, carried in Figma as a Fundus tag link (Dev Resource) on the node. */
14
15
  fundusTag: string;
15
- /** Resolved node id (colon-form), cached from the last tag resolution. */
16
+ /** Node id (colon-form) of the last tag resolution; every reimport resolves the tag again. */
16
17
  nodeId: string;
17
18
  format: 'png';
18
19
  scale: 1 | 2 | 3 | 4;
@@ -40,22 +41,26 @@ export declare function parseFigmaFileKey(value: string): string;
40
41
  export declare function validateFigmaSourceParameters(value: unknown): FigmaSourceParameters;
41
42
  export declare function resolveSafeDownloadTarget(value: string | URL): Promise<PinnedHttpsTarget>;
42
43
  export declare function assertSafeDownloadUrl(value: string | URL): Promise<URL>;
43
- /** One node in a Figma REST document tree, as the tag commands report it. */
44
- export interface FigmaDocumentNode {
44
+ /** Every Fundus tag link in the file (orphaned ones included); each entry has its `linkId`. */
45
+ export declare function listFigmaTagEntries(fileKey: string, context: ImporterContext): Promise<Array<FigmaTagEntry & {
46
+ linkId: string;
47
+ }>>;
48
+ export interface FigmaNodeSummary {
45
49
  nodeId: string;
46
50
  nodeName: string;
47
- /** Name of the page (canvas) containing the node. */
48
- page: string;
49
- /** The node's Fundus tag, or null when untagged. */
50
- tag: string | null;
51
+ nodeType: string;
51
52
  }
53
+ /** The requested nodes that exist; Figma answers deleted or unknown ids with `null` (dropped). */
54
+ export declare function fetchFigmaNodeSummaries(fileKey: string, nodeIds: readonly string[], context: ImporterContext): Promise<FigmaNodeSummary[]>;
55
+ export declare function createFigmaTagLink(fileKey: string, nodeId: string, tag: string, context: ImporterContext): Promise<void>;
56
+ export declare function deleteFigmaDevResource(fileKey: string, linkId: string, context: ImporterContext): Promise<void>;
52
57
  /**
53
- * Every taggable node in a REST document tree (`plugin_data=shared`), in
54
- * document order. Nodes inside instances are skipped: they echo the main
55
- * component node's tag, and Fundus imports only plain `12:34` node ids.
58
+ * The one renderable node carrying `tag`. All candidates go into a single
59
+ * `/images` call, which answers deleted nodes with `null`; a live but
60
+ * unrenderable second candidate therefore goes unnoticed.
56
61
  */
57
- export declare function figmaDocumentNodes(document: unknown): FigmaDocumentNode[];
58
- /** The whole document tree of a Figma file, including every node's shared plugin data. */
59
- export declare function fetchFigmaDocument(fileKey: string, context: ImporterContext): Promise<unknown>;
60
- export declare function resolveTagToNode(fileKey: string, tag: string, context: ImporterContext): Promise<string>;
62
+ export declare function resolveTaggedNode(fileKey: string, tag: string, scale: 1 | 2 | 3 | 4, context: ImporterContext): Promise<{
63
+ nodeId: string;
64
+ imageUrl: string;
65
+ }>;
61
66
  export declare const figmaSourceImporter: ServerSourceImporter<FigmaImportInput, FigmaSourceParameters>;