@fluid-app/fluid-cli-theme-dev 0.1.59 → 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/dist-cli-preview/.vite/manifest.json +1831 -0
- package/dist/dist-cli-preview/account-screens.js +1 -0
- package/dist/dist-cli-preview/capabilities.json +1 -0
- package/dist/dist-cli-preview/chunks/Breadcrumb-CZ-skiUy.js +91 -0
- package/dist/dist-cli-preview/chunks/Combobox-BGiCYa56.js +1455 -0
- package/dist/dist-cli-preview/chunks/ContactsScreen-vsNgy_gn.js +6930 -0
- package/dist/dist-cli-preview/chunks/MemberManagementProviders-DMwkh3JW.js +1509 -0
- package/dist/dist-cli-preview/chunks/MessagingScreen-DzJOwPGH.js +38475 -0
- package/dist/dist-cli-preview/chunks/MobileActionSheet-DqVPgFKI.js +2895 -0
- package/dist/dist-cli-preview/chunks/MySiteScreen-BSmWlCKA.js +4950 -0
- package/dist/dist-cli-preview/chunks/OrdersScreen-CVEl9FnN.js +1349 -0
- package/dist/dist-cli-preview/chunks/OrdersScreen-d4fJE_dm.js +9 -0
- package/dist/dist-cli-preview/chunks/ProfileScreen-BC5gyJDY.js +2581 -0
- package/dist/dist-cli-preview/chunks/SubscriptionsScreen-CFM4Pufg.js +38 -0
- package/dist/dist-cli-preview/chunks/account-screen-runtime-DxhJm6Qd.js +12407 -0
- package/dist/dist-cli-preview/chunks/ar-4OhalXtk.js +628 -0
- package/dist/dist-cli-preview/chunks/ar-8iaFSgop.js +189 -0
- package/dist/dist-cli-preview/chunks/ar-B7NQmkZ1.js +341 -0
- package/dist/dist-cli-preview/chunks/ar-DEUMqcX3.js +563 -0
- package/dist/dist-cli-preview/chunks/ar-DT9j-dOi.js +720 -0
- package/dist/dist-cli-preview/chunks/ar-D_B30x0o.js +176 -0
- package/dist/dist-cli-preview/chunks/arrow-left-UFKI-PQf.js +14 -0
- package/dist/dist-cli-preview/chunks/bg-CIgMfa9k.js +176 -0
- package/dist/dist-cli-preview/chunks/bg-CS2zt-UV.js +720 -0
- package/dist/dist-cli-preview/chunks/bg-CxfL8rZN.js +341 -0
- package/dist/dist-cli-preview/chunks/bg-DOdbUYGw.js +563 -0
- package/dist/dist-cli-preview/chunks/bg-D_C_HmWn.js +189 -0
- package/dist/dist-cli-preview/chunks/bg-DeBjzn9B.js +628 -0
- package/dist/dist-cli-preview/chunks/bn-BAGJspTJ.js +563 -0
- package/dist/dist-cli-preview/chunks/bn-BgnBXWHo.js +176 -0
- package/dist/dist-cli-preview/chunks/bn-CRBwi_3N.js +628 -0
- package/dist/dist-cli-preview/chunks/bn-CdhHCK--.js +341 -0
- package/dist/dist-cli-preview/chunks/bn-DiqHsF7K.js +720 -0
- package/dist/dist-cli-preview/chunks/bn-UEAt87N9.js +189 -0
- package/dist/dist-cli-preview/chunks/cli-preview-DunPfnjU.js +151 -0
- package/dist/dist-cli-preview/chunks/client.gen-CSTBgfxj.js +672 -0
- package/dist/dist-cli-preview/chunks/contacts-CpYlEgAq.js +4 -0
- package/dist/dist-cli-preview/chunks/countries-api-adapter-B3usO25w.js +108 -0
- package/dist/dist-cli-preview/chunks/create-card-entry-container-DHgsbSyr.js +23942 -0
- package/dist/dist-cli-preview/chunks/cs-B8rJsMOS.js +189 -0
- package/dist/dist-cli-preview/chunks/cs-ChKaPMtV.js +563 -0
- package/dist/dist-cli-preview/chunks/cs-CkdXQxaf.js +720 -0
- package/dist/dist-cli-preview/chunks/cs-DTe-xGeI.js +341 -0
- package/dist/dist-cli-preview/chunks/cs-DmLSumGB.js +176 -0
- package/dist/dist-cli-preview/chunks/cs-jXPm6Nhj.js +628 -0
- package/dist/dist-cli-preview/chunks/da-7uI_CROE.js +628 -0
- package/dist/dist-cli-preview/chunks/da-B8Cl_vkl.js +341 -0
- package/dist/dist-cli-preview/chunks/da-Bee-xQNq.js +563 -0
- package/dist/dist-cli-preview/chunks/da-DYlEkykt.js +720 -0
- package/dist/dist-cli-preview/chunks/da-EfkyseOR.js +176 -0
- package/dist/dist-cli-preview/chunks/da-J--1RxZp.js +189 -0
- package/dist/dist-cli-preview/chunks/de-BTNiErdB.js +189 -0
- package/dist/dist-cli-preview/chunks/de-CIhZnrdS.js +720 -0
- package/dist/dist-cli-preview/chunks/de-CKtChtTF.js +628 -0
- package/dist/dist-cli-preview/chunks/de-D8cYy8G-.js +341 -0
- package/dist/dist-cli-preview/chunks/de-EARi-xk5.js +176 -0
- package/dist/dist-cli-preview/chunks/de-zeOnL7Nl.js +563 -0
- package/dist/dist-cli-preview/chunks/el-0NNTwBnJ.js +563 -0
- package/dist/dist-cli-preview/chunks/el-BPz4rb5a.js +176 -0
- package/dist/dist-cli-preview/chunks/el-Bg6zRbVK.js +720 -0
- package/dist/dist-cli-preview/chunks/el-CUjODD23.js +628 -0
- package/dist/dist-cli-preview/chunks/el-Chjin2JW.js +341 -0
- package/dist/dist-cli-preview/chunks/el-DYay96x-.js +189 -0
- package/dist/dist-cli-preview/chunks/es-CFZtv6aF.js +176 -0
- package/dist/dist-cli-preview/chunks/es-CGgD1AV1.js +720 -0
- package/dist/dist-cli-preview/chunks/es-CXXB8ubn.js +628 -0
- package/dist/dist-cli-preview/chunks/es-Cf56ydFZ.js +189 -0
- package/dist/dist-cli-preview/chunks/es-DirUtV7k.js +563 -0
- package/dist/dist-cli-preview/chunks/es-mp8RwFN4.js +341 -0
- package/dist/dist-cli-preview/chunks/fi-91gcpxoy.js +341 -0
- package/dist/dist-cli-preview/chunks/fi-BfDoQ1rt.js +628 -0
- package/dist/dist-cli-preview/chunks/fi-Bn9_l4IH.js +176 -0
- package/dist/dist-cli-preview/chunks/fi-Bo5QdhP_.js +563 -0
- package/dist/dist-cli-preview/chunks/fi-CC2YqekN.js +189 -0
- package/dist/dist-cli-preview/chunks/fi-DUhIWph7.js +720 -0
- package/dist/dist-cli-preview/chunks/fr-BSouGEGN.js +176 -0
- package/dist/dist-cli-preview/chunks/fr-BnYsse99.js +563 -0
- package/dist/dist-cli-preview/chunks/fr-C32S8PvA.js +720 -0
- package/dist/dist-cli-preview/chunks/fr-DIQcYUq-.js +628 -0
- package/dist/dist-cli-preview/chunks/fr-Do6AIa1z.js +189 -0
- package/dist/dist-cli-preview/chunks/fr-ekvUCKTH.js +341 -0
- package/dist/dist-cli-preview/chunks/he-B7QiuquT.js +341 -0
- package/dist/dist-cli-preview/chunks/he-BYEZTUry.js +176 -0
- package/dist/dist-cli-preview/chunks/he-CO8gUz9X.js +628 -0
- package/dist/dist-cli-preview/chunks/he-DaWGyHjM.js +189 -0
- package/dist/dist-cli-preview/chunks/he-pFefco4W.js +563 -0
- package/dist/dist-cli-preview/chunks/he-xSGbvsti.js +720 -0
- package/dist/dist-cli-preview/chunks/hey-api-client-DSvKWGPq.js +153 -0
- package/dist/dist-cli-preview/chunks/hi-BLnEaAml.js +176 -0
- package/dist/dist-cli-preview/chunks/hi-BMFNssNU.js +720 -0
- package/dist/dist-cli-preview/chunks/hi-BXBdXuFm.js +189 -0
- package/dist/dist-cli-preview/chunks/hi-DNNhi7ud.js +341 -0
- package/dist/dist-cli-preview/chunks/hi-DVSXxEzh.js +628 -0
- package/dist/dist-cli-preview/chunks/hi-Uqd1FKtv.js +563 -0
- package/dist/dist-cli-preview/chunks/hr-B1T2UY_p.js +176 -0
- package/dist/dist-cli-preview/chunks/hr-B432GvmU.js +720 -0
- package/dist/dist-cli-preview/chunks/hr-BJR58Xci.js +563 -0
- package/dist/dist-cli-preview/chunks/hr-CAwxMKX_.js +341 -0
- package/dist/dist-cli-preview/chunks/hr-DBrPFNLx.js +189 -0
- package/dist/dist-cli-preview/chunks/hr-yH6ab4ea.js +628 -0
- package/dist/dist-cli-preview/chunks/hu-BcC628Ln.js +189 -0
- package/dist/dist-cli-preview/chunks/hu-Bv0_YQEn.js +628 -0
- package/dist/dist-cli-preview/chunks/hu-D_oQwH4Q.js +341 -0
- package/dist/dist-cli-preview/chunks/hu-Q0aE9GvJ.js +720 -0
- package/dist/dist-cli-preview/chunks/hu-TNzzMEzp.js +563 -0
- package/dist/dist-cli-preview/chunks/hu-cQw9hgLe.js +176 -0
- package/dist/dist-cli-preview/chunks/id-Becf5X73.js +720 -0
- package/dist/dist-cli-preview/chunks/id-CLvNjXjm.js +189 -0
- package/dist/dist-cli-preview/chunks/id-CzSVbo5E.js +628 -0
- package/dist/dist-cli-preview/chunks/id-Vudf_d_f.js +563 -0
- package/dist/dist-cli-preview/chunks/id-Zd_5csjz.js +176 -0
- package/dist/dist-cli-preview/chunks/id-oeAszBoo.js +341 -0
- package/dist/dist-cli-preview/chunks/isAfter-BUGgByEf.js +1414 -0
- package/dist/dist-cli-preview/chunks/it-BlwQOBaX.js +720 -0
- package/dist/dist-cli-preview/chunks/it-C0jghILa.js +563 -0
- package/dist/dist-cli-preview/chunks/it-CmxC_FIJ.js +176 -0
- package/dist/dist-cli-preview/chunks/it-D5BXSiSl.js +189 -0
- package/dist/dist-cli-preview/chunks/it-Dme6GXSG.js +341 -0
- package/dist/dist-cli-preview/chunks/it-xT70eFoJ.js +628 -0
- package/dist/dist-cli-preview/chunks/ja-BJ7phJBG.js +720 -0
- package/dist/dist-cli-preview/chunks/ja-Bc8dA1LN.js +189 -0
- package/dist/dist-cli-preview/chunks/ja-BcOunH2q.js +176 -0
- package/dist/dist-cli-preview/chunks/ja-CRnvQNCa.js +341 -0
- package/dist/dist-cli-preview/chunks/ja-CfYfDP_F.js +563 -0
- package/dist/dist-cli-preview/chunks/ja-Dh3z4uJk.js +628 -0
- package/dist/dist-cli-preview/chunks/ko-8mk8A3_I.js +341 -0
- package/dist/dist-cli-preview/chunks/ko-Bnp-5GAn.js +720 -0
- package/dist/dist-cli-preview/chunks/ko-CG_0ufw6.js +176 -0
- package/dist/dist-cli-preview/chunks/ko-CJ-8gCc9.js +189 -0
- package/dist/dist-cli-preview/chunks/ko-CnpxBQ2W.js +628 -0
- package/dist/dist-cli-preview/chunks/ko-CtkKNEZB.js +563 -0
- package/dist/dist-cli-preview/chunks/log-out-CBUq5Hd-.js +15 -0
- package/dist/dist-cli-preview/chunks/map-pin-DHfjSyoR.js +20 -0
- package/dist/dist-cli-preview/chunks/messaging-DkyH4CPP.js +4 -0
- package/dist/dist-cli-preview/chunks/mount-account-screen-pc1VISwF.js +11041 -0
- package/dist/dist-cli-preview/chunks/ms-BF5gF9D5.js +189 -0
- package/dist/dist-cli-preview/chunks/ms-Bs0GvdOw.js +341 -0
- package/dist/dist-cli-preview/chunks/ms-CyhqvfOb.js +563 -0
- package/dist/dist-cli-preview/chunks/ms-D6Cf3VUm.js +628 -0
- package/dist/dist-cli-preview/chunks/ms-DR0U3aeO.js +720 -0
- package/dist/dist-cli-preview/chunks/ms-DTN7PIDq.js +176 -0
- package/dist/dist-cli-preview/chunks/mysite-wvfITV7T.js +4 -0
- package/dist/dist-cli-preview/chunks/nl-C1v8tc97.js +720 -0
- package/dist/dist-cli-preview/chunks/nl-C5fWLZkK.js +189 -0
- package/dist/dist-cli-preview/chunks/nl-Cgt3zlZ8.js +628 -0
- package/dist/dist-cli-preview/chunks/nl-DWtWnOvH.js +563 -0
- package/dist/dist-cli-preview/chunks/nl-tsI6lLxl.js +341 -0
- package/dist/dist-cli-preview/chunks/nl-x2lc8Ct5.js +176 -0
- package/dist/dist-cli-preview/chunks/no-BeVycdYu.js +563 -0
- package/dist/dist-cli-preview/chunks/no-DMot6qX0.js +341 -0
- package/dist/dist-cli-preview/chunks/no-Dk9D1gjY.js +189 -0
- package/dist/dist-cli-preview/chunks/no-R8FScfrK.js +176 -0
- package/dist/dist-cli-preview/chunks/no-dhNRqvE4.js +720 -0
- package/dist/dist-cli-preview/chunks/no-xVFexdWN.js +628 -0
- package/dist/dist-cli-preview/chunks/orders-c13r499A.js +4 -0
- package/dist/dist-cli-preview/chunks/pl-BH1bEHfg.js +720 -0
- package/dist/dist-cli-preview/chunks/pl-Bl9G1yOR.js +176 -0
- package/dist/dist-cli-preview/chunks/pl-Cu0JzUUV.js +563 -0
- package/dist/dist-cli-preview/chunks/pl-D7AZQwtK.js +189 -0
- package/dist/dist-cli-preview/chunks/pl-Dl0M9JDp.js +628 -0
- package/dist/dist-cli-preview/chunks/pl-DmTxONNg.js +341 -0
- package/dist/dist-cli-preview/chunks/plus-C-lo0pFM.js +14 -0
- package/dist/dist-cli-preview/chunks/profile-CoWo9Sxe.js +4 -0
- package/dist/dist-cli-preview/chunks/pt-62Kd-NN1.js +189 -0
- package/dist/dist-cli-preview/chunks/pt-BAq7bseT.js +628 -0
- package/dist/dist-cli-preview/chunks/pt-DgmwnAV-.js +341 -0
- package/dist/dist-cli-preview/chunks/pt-K0EDcWuR.js +720 -0
- package/dist/dist-cli-preview/chunks/pt-nD60RpwN.js +176 -0
- package/dist/dist-cli-preview/chunks/pt-voHVxH2b.js +563 -0
- package/dist/dist-cli-preview/chunks/ro-B4M17oRc.js +341 -0
- package/dist/dist-cli-preview/chunks/ro-CFfjUJ1T.js +563 -0
- package/dist/dist-cli-preview/chunks/ro-CtL0oK2c.js +628 -0
- package/dist/dist-cli-preview/chunks/ro-D6ybG7vp.js +176 -0
- package/dist/dist-cli-preview/chunks/ro-DTauvv2V.js +189 -0
- package/dist/dist-cli-preview/chunks/ro-DUxhal7c.js +720 -0
- package/dist/dist-cli-preview/chunks/ru-45VizwWQ.js +628 -0
- package/dist/dist-cli-preview/chunks/ru-BS2gZnbs.js +341 -0
- package/dist/dist-cli-preview/chunks/ru-C0oMeuhb.js +176 -0
- package/dist/dist-cli-preview/chunks/ru-COxlz18L.js +563 -0
- package/dist/dist-cli-preview/chunks/ru-CjEXCUxb.js +720 -0
- package/dist/dist-cli-preview/chunks/ru-g5Cz5HSk.js +189 -0
- package/dist/dist-cli-preview/chunks/schemas-9_ZwC5nC.js +3977 -0
- package/dist/dist-cli-preview/chunks/screen-route-slug-utils-B254xI_J.js +14954 -0
- package/dist/dist-cli-preview/chunks/sheet-haptics-context-D2WUiXJm.js +116 -0
- package/dist/dist-cli-preview/chunks/sidebar-CMSWZCPS.js +848 -0
- package/dist/dist-cli-preview/chunks/sk-BGDxj2sS.js +563 -0
- package/dist/dist-cli-preview/chunks/sk-BdDjjHfn.js +341 -0
- package/dist/dist-cli-preview/chunks/sk-Bpdt_mn0.js +176 -0
- package/dist/dist-cli-preview/chunks/sk-DX2wOf1g.js +628 -0
- package/dist/dist-cli-preview/chunks/sk-QbdQWjZy.js +714 -0
- package/dist/dist-cli-preview/chunks/sk-YuAeAaeZ.js +189 -0
- package/dist/dist-cli-preview/chunks/store-api-context-BIdILNWL.js +16 -0
- package/dist/dist-cli-preview/chunks/subscriptions-BHALQrcU.js +4 -0
- package/dist/dist-cli-preview/chunks/sv-BlNLvbmj.js +628 -0
- package/dist/dist-cli-preview/chunks/sv-CEmTDhJy.js +176 -0
- package/dist/dist-cli-preview/chunks/sv-CW7FL5HA.js +341 -0
- package/dist/dist-cli-preview/chunks/sv-CiCeNX1x.js +720 -0
- package/dist/dist-cli-preview/chunks/sv-DefBhX0c.js +563 -0
- package/dist/dist-cli-preview/chunks/sv-iaAh80e4.js +189 -0
- package/dist/dist-cli-preview/chunks/th-BNEA6ESz.js +720 -0
- package/dist/dist-cli-preview/chunks/th-BYBMKmSh.js +341 -0
- package/dist/dist-cli-preview/chunks/th-DMPCj1Fg.js +189 -0
- package/dist/dist-cli-preview/chunks/th-DV3lk4-6.js +563 -0
- package/dist/dist-cli-preview/chunks/th-F8oLBw5n.js +176 -0
- package/dist/dist-cli-preview/chunks/th-fPXxGW_m.js +628 -0
- package/dist/dist-cli-preview/chunks/tl-BMLmbhme.js +628 -0
- package/dist/dist-cli-preview/chunks/tl-BXIS85ly.js +176 -0
- package/dist/dist-cli-preview/chunks/tl-CDigaLsK.js +341 -0
- package/dist/dist-cli-preview/chunks/tl-DVdSJCnc.js +563 -0
- package/dist/dist-cli-preview/chunks/tl-fdtlfqan.js +720 -0
- package/dist/dist-cli-preview/chunks/tl-w5CgYWXn.js +189 -0
- package/dist/dist-cli-preview/chunks/tr-BA1MDk8A.js +563 -0
- package/dist/dist-cli-preview/chunks/tr-CHri_kqV.js +189 -0
- package/dist/dist-cli-preview/chunks/tr-DD75kuPl.js +720 -0
- package/dist/dist-cli-preview/chunks/tr-Mq5Hfqt2.js +628 -0
- package/dist/dist-cli-preview/chunks/tr-PxiurNCr.js +341 -0
- package/dist/dist-cli-preview/chunks/tr-YXIIDIEV.js +176 -0
- package/dist/dist-cli-preview/chunks/trash-2-Bq4IqH0j.js +137 -0
- package/dist/dist-cli-preview/chunks/uk-B4aYnJ8t.js +341 -0
- package/dist/dist-cli-preview/chunks/uk-ByJtQJHi.js +189 -0
- package/dist/dist-cli-preview/chunks/uk-CCVTTskN.js +720 -0
- package/dist/dist-cli-preview/chunks/uk-CS0tJc6H.js +628 -0
- package/dist/dist-cli-preview/chunks/uk-CYYjeT-V.js +176 -0
- package/dist/dist-cli-preview/chunks/uk-DGb0m_JV.js +563 -0
- package/dist/dist-cli-preview/chunks/use-optional-store-Csor1Z3z.js +22 -0
- package/dist/dist-cli-preview/chunks/useInfiniteQuery-BpFtQ7DH.js +67 -0
- package/dist/dist-cli-preview/chunks/user-BGrQJ1GJ.js +31 -0
- package/dist/dist-cli-preview/chunks/users-DBL0isYy.js +27 -0
- package/dist/dist-cli-preview/chunks/vi-BoKSdzH3.js +341 -0
- package/dist/dist-cli-preview/chunks/vi-CHKjehVL.js +628 -0
- package/dist/dist-cli-preview/chunks/vi-DFicISRE.js +189 -0
- package/dist/dist-cli-preview/chunks/vi-DT4XS4HU.js +176 -0
- package/dist/dist-cli-preview/chunks/vi-RFwPNQ96.js +720 -0
- package/dist/dist-cli-preview/chunks/vi-SG6trKny.js +563 -0
- package/dist/dist-cli-preview/chunks/zh_CN-BJVGrpVl.js +714 -0
- package/dist/dist-cli-preview/chunks/zh_CN-BeGANQIJ.js +176 -0
- package/dist/dist-cli-preview/chunks/zh_CN-BwpNGMQN.js +189 -0
- package/dist/dist-cli-preview/chunks/zh_CN-CFu3GI2I.js +563 -0
- package/dist/dist-cli-preview/chunks/zh_CN-DrCrXsZC.js +341 -0
- package/dist/dist-cli-preview/chunks/zh_CN-x6mJyUQH.js +628 -0
- package/dist/dist-cli-preview/chunks/zh_TW-CorN6cwY.js +720 -0
- package/dist/dist-cli-preview/chunks/zh_TW-D9gGVyEb.js +176 -0
- package/dist/dist-cli-preview/chunks/zh_TW-DdD6Hf_A.js +628 -0
- package/dist/dist-cli-preview/chunks/zh_TW-JmyC1W5K.js +341 -0
- package/dist/dist-cli-preview/chunks/zh_TW-Oq4Chn8F.js +189 -0
- package/dist/dist-cli-preview/chunks/zh_TW-kUnD3TL4.js +563 -0
- package/dist/index.mjs +1220 -513
- package/dist/index.mjs.map +1 -1
- package/dist/skills/themes-review/SKILL.md +2 -2
- package/dist/skills/themes-review/references/navigation.md +3 -1
- package/package.json +60 -10
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import prompts from "prompts";
|
|
2
3
|
import { getAuthToken, gitSyncActorFromMe, gitSyncCommitSubject, readConfig, resolveGitSyncActor, summarizeChanges, updateConfig } from "@fluid-app/fluid-cli";
|
|
4
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
3
5
|
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
6
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
7
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
8
|
import http from "node:http";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { parse, parseFragment, serialize } from "parse5";
|
|
7
11
|
import https from "node:https";
|
|
8
12
|
import chokidar from "chokidar";
|
|
9
13
|
import net from "node:net";
|
|
10
14
|
import { execFileSync, spawn } from "node:child_process";
|
|
11
15
|
import { tmpdir } from "node:os";
|
|
12
16
|
import chalk from "chalk";
|
|
13
|
-
import prompts from "prompts";
|
|
14
17
|
import ora from "ora";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
18
|
//#region ../../platform/api-client-core/src/api-error-shape.ts
|
|
17
19
|
/**
|
|
18
20
|
* Narrows a parsed JSON error payload to the envelope contract. Arrays and
|
|
@@ -292,10 +294,34 @@ async function backgroundToken(tokenOverride) {
|
|
|
292
294
|
return tokenOverride ?? getAuthToken() ?? null;
|
|
293
295
|
}
|
|
294
296
|
function createApiClient(tokenOverride) {
|
|
295
|
-
|
|
297
|
+
const client = createFetchClient({
|
|
296
298
|
baseUrl: getApiBase(),
|
|
297
299
|
getAuthToken: () => process.env["FLUID_BACKGROUND_SYNC"] === "1" ? backgroundToken(tokenOverride) : tokenOverride ?? getAuthToken() ?? null
|
|
298
300
|
});
|
|
301
|
+
const request = async (endpoint, options = {}) => {
|
|
302
|
+
for (let attempt = 0;; attempt++) {
|
|
303
|
+
options.signal?.throwIfAborted();
|
|
304
|
+
try {
|
|
305
|
+
return await client.request(endpoint, options);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if ((options.method ?? "GET") !== "GET" || !(error instanceof ApiError) || ![
|
|
308
|
+
502,
|
|
309
|
+
503,
|
|
310
|
+
504
|
|
311
|
+
].includes(error.status) || attempt >= 2 || options.signal?.aborted) throw error;
|
|
312
|
+
await setTimeout$1(500 * 2 ** attempt, void 0, { signal: options.signal });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
...client,
|
|
318
|
+
request,
|
|
319
|
+
get: (endpoint, params, options) => request(endpoint, {
|
|
320
|
+
...options,
|
|
321
|
+
method: "GET",
|
|
322
|
+
params
|
|
323
|
+
})
|
|
324
|
+
};
|
|
299
325
|
}
|
|
300
326
|
function requireToken() {
|
|
301
327
|
const token = getAuthToken();
|
|
@@ -306,54 +332,6 @@ function requireToken() {
|
|
|
306
332
|
return token;
|
|
307
333
|
}
|
|
308
334
|
//#endregion
|
|
309
|
-
//#region src/theme-config.ts
|
|
310
|
-
const CONFIG_FILE = ".fluid-theme.json";
|
|
311
|
-
/**
|
|
312
|
-
* `company` must be the bare subdomain slug — the dev server builds
|
|
313
|
-
* `<company>.fluid.app` from it, so a full domain here produces an
|
|
314
|
-
* unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling
|
|
315
|
-
* writes the full domain (or a URL); accept those and reduce them to
|
|
316
|
-
* the subdomain.
|
|
317
|
-
*/
|
|
318
|
-
function normalizeCompany(company) {
|
|
319
|
-
return company.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/[/?#].*$/, "").replace(/\.fluid\.app$/i, "");
|
|
320
|
-
}
|
|
321
|
-
function configPath(themeRoot) {
|
|
322
|
-
return join(themeRoot, CONFIG_FILE);
|
|
323
|
-
}
|
|
324
|
-
/** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
|
|
325
|
-
function readThemeConfig(themeRoot) {
|
|
326
|
-
const path = configPath(themeRoot);
|
|
327
|
-
if (!existsSync(path)) return null;
|
|
328
|
-
try {
|
|
329
|
-
const raw = readFileSync(path, "utf-8");
|
|
330
|
-
const config = JSON.parse(raw);
|
|
331
|
-
if (typeof config.company === "string") config.company = normalizeCompany(config.company);
|
|
332
|
-
return config;
|
|
333
|
-
} catch {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Read a legacy config (pre-shadow-repo). Used only by the migration
|
|
339
|
-
* path on first new-CLI pull to seed shadow HEAD from files whose
|
|
340
|
-
* local content still matches their stored sha256 checksum.
|
|
341
|
-
*/
|
|
342
|
-
function readLegacyThemeConfig(themeRoot) {
|
|
343
|
-
const path = configPath(themeRoot);
|
|
344
|
-
if (!existsSync(path)) return null;
|
|
345
|
-
try {
|
|
346
|
-
const raw = readFileSync(path, "utf-8");
|
|
347
|
-
return JSON.parse(raw);
|
|
348
|
-
} catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
/** Write `.fluid-theme.json` to a theme directory. */
|
|
353
|
-
function writeThemeConfig(themeRoot, config) {
|
|
354
|
-
writeFileSync(configPath(themeRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
355
|
-
}
|
|
356
|
-
//#endregion
|
|
357
335
|
//#region src/plugin-state.ts
|
|
358
336
|
const PLUGIN_KEY = "theme-dev";
|
|
359
337
|
function getState() {
|
|
@@ -473,13 +451,530 @@ function setLastDevThemeId(id) {
|
|
|
473
451
|
});
|
|
474
452
|
}
|
|
475
453
|
/**
|
|
476
|
-
* The dev theme to target by default in `navigate` — the most recently started
|
|
477
|
-
* dev server. Falls back to the legacy global id for users who haven't yet run
|
|
478
|
-
* the per-project `theme dev`.
|
|
454
|
+
* The dev theme to target by default in `navigate` — the most recently started
|
|
455
|
+
* dev server. Falls back to the legacy global id for users who haven't yet run
|
|
456
|
+
* the per-project `theme dev`.
|
|
457
|
+
*/
|
|
458
|
+
function getLastDevThemeId() {
|
|
459
|
+
const state = getState();
|
|
460
|
+
return state.lastDevThemeId ?? state.devThemeId;
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/theme/dev-server/member-preview-contract.ts
|
|
464
|
+
var MemberPreviewError = class extends Error {
|
|
465
|
+
constructor(message, status = 502) {
|
|
466
|
+
super(message);
|
|
467
|
+
this.status = status;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
function isRecord$5(value) {
|
|
471
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
472
|
+
}
|
|
473
|
+
function isId(value) {
|
|
474
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
475
|
+
}
|
|
476
|
+
/** The metadata API still exposes storefront-era /accounts paths. Account hosts omit it. */
|
|
477
|
+
function accountPath(path) {
|
|
478
|
+
return path === "/accounts" ? "/" : path.startsWith("/accounts/") ? path.slice(9) : path;
|
|
479
|
+
}
|
|
480
|
+
function isProductionPath(value) {
|
|
481
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return false;
|
|
482
|
+
return new URL(value, "http://preview.invalid").pathname === value && !value.startsWith("/_members");
|
|
483
|
+
}
|
|
484
|
+
function parseMemberMetadata(value) {
|
|
485
|
+
if (!isRecord$5(value) || !Array.isArray(value.members_sites) || !Array.isArray(value.system_screens)) throw new MemberPreviewError("This server does not provide compatible member preview metadata.");
|
|
486
|
+
const sites = value.members_sites.map((site) => {
|
|
487
|
+
if (!isRecord$5(site) || !isId(site.id) || typeof site.name !== "string" || !Array.isArray(site.pages)) throw new MemberPreviewError("Invalid member site metadata.");
|
|
488
|
+
const pages = site.pages.map((page) => {
|
|
489
|
+
if (!isRecord$5(page) || !isId(page.id) || typeof page.title !== "string" || !isProductionPath(page.path) || typeof page.published !== "boolean") throw new MemberPreviewError("Invalid member page metadata.");
|
|
490
|
+
return {
|
|
491
|
+
id: page.id,
|
|
492
|
+
title: page.title,
|
|
493
|
+
path: accountPath(page.path),
|
|
494
|
+
published: page.published
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
if (new Set(pages.map((p) => p.path)).size !== pages.length) throw new MemberPreviewError("Ambiguous member page paths.");
|
|
498
|
+
return {
|
|
499
|
+
id: site.id,
|
|
500
|
+
name: site.name,
|
|
501
|
+
pages
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
if (new Set(sites.map((s) => s.id)).size !== sites.length) throw new MemberPreviewError("Duplicate member site identifiers.");
|
|
505
|
+
return {
|
|
506
|
+
members_sites: sites,
|
|
507
|
+
system_screens: value.system_screens.map((screen) => {
|
|
508
|
+
if (!isRecord$5(screen) || typeof screen.key !== "string" || !/^[a-z][a-z0-9_-]*$/.test(screen.key)) throw new MemberPreviewError("Invalid system screen metadata.");
|
|
509
|
+
return { key: screen.key };
|
|
510
|
+
})
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function parseSelection(params) {
|
|
514
|
+
const site = numericParam(params, "members_site_id");
|
|
515
|
+
if (!site) throw new MemberPreviewError("Select a member site at /_members first.", 400);
|
|
516
|
+
const memberType = params.get("member_type_id");
|
|
517
|
+
if (params.getAll("member_type_id").length > 1 || memberType !== null && !/^[\w-]{1,100}$/.test(memberType)) throw new MemberPreviewError("Invalid member type selection.", 400);
|
|
518
|
+
return {
|
|
519
|
+
members_site_id: site,
|
|
520
|
+
country_id: numericParam(params, "country_id"),
|
|
521
|
+
language_id: numericParam(params, "language_id"),
|
|
522
|
+
...memberType ? { member_type_id: memberType } : {}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function numericParam(params, key) {
|
|
526
|
+
const value = params.get(key);
|
|
527
|
+
if (value === null) return void 0;
|
|
528
|
+
if (params.getAll(key).length !== 1 || !/^[1-9]\d*$/.test(value) || !isId(Number(value))) throw new MemberPreviewError(`Invalid ${key}.`, 400);
|
|
529
|
+
return Number(value);
|
|
530
|
+
}
|
|
531
|
+
function memberPreviewUrl(path, selection) {
|
|
532
|
+
const url = new URL(path, "http://preview.invalid");
|
|
533
|
+
if (url.origin !== "http://preview.invalid") throw new MemberPreviewError("Expected a local member path.", 400);
|
|
534
|
+
if (url.pathname !== "/_members" && !url.pathname.startsWith("/_members/")) url.pathname = `/_members${url.pathname}`;
|
|
535
|
+
for (const key of [
|
|
536
|
+
"members_site_id",
|
|
537
|
+
"country_id",
|
|
538
|
+
"language_id",
|
|
539
|
+
"member_type_id"
|
|
540
|
+
]) {
|
|
541
|
+
const value = selection[key];
|
|
542
|
+
if (value === void 0) url.searchParams.delete(key);
|
|
543
|
+
else url.searchParams.set(key, String(value));
|
|
544
|
+
}
|
|
545
|
+
return url.pathname + url.search + url.hash;
|
|
546
|
+
}
|
|
547
|
+
function parseMemberRender(value, themeId, selection, pageId, screenKey = null) {
|
|
548
|
+
const preview = isRecord$5(value) ? value.preview : void 0;
|
|
549
|
+
if (!isRecord$5(preview) || preview.contract_version !== 1 || preview.mode !== "sample" || preview.theme_id !== themeId || preview.members_site_id !== selection.members_site_id || preview.members_page_id !== pageId || preview.system_screen_key !== screenKey || typeof preview.html_content !== "string" || !(preview.page_stylesheet === null || typeof preview.page_stylesheet === "string") || !(preview.template_stylesheet === null || typeof preview.template_stylesheet === "string")) throw new MemberPreviewError("Incompatible member preview response. Update the server and CLI together.");
|
|
550
|
+
return {
|
|
551
|
+
html: preview.html_content,
|
|
552
|
+
styles: preview.page_stylesheet ?? "",
|
|
553
|
+
templateStyles: preview.template_stylesheet ?? ""
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/theme/navigation-members.ts
|
|
558
|
+
/** Read the running server's acknowledged route map, without sending credentials. */
|
|
559
|
+
async function readPreviewMetadata(address, themeId) {
|
|
560
|
+
let value;
|
|
561
|
+
try {
|
|
562
|
+
const response = await fetch(`${address}/_members/_metadata`, {
|
|
563
|
+
redirect: "error",
|
|
564
|
+
signal: AbortSignal.timeout(1e4)
|
|
565
|
+
});
|
|
566
|
+
if (!response.ok) throw new Error("Member metadata unavailable");
|
|
567
|
+
value = await response.json();
|
|
568
|
+
} catch {
|
|
569
|
+
console.warn("Member preview is unavailable. Start an updated theme dev server to list member pages.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!isRecord$5(value) || value.mode !== "sample") throw new Error("The running server does not provide compatible preview metadata.");
|
|
573
|
+
if (value.theme_id !== themeId) throw new Error(`The running server is not serving theme #${themeId}. Use its theme ID or restart theme dev.`);
|
|
574
|
+
if (!Array.isArray(value.supported_system_screens) || !value.supported_system_screens.every((key) => typeof key === "string")) throw new Error("The running server does not provide preview capabilities.");
|
|
575
|
+
return {
|
|
576
|
+
...parseMemberMetadata(value),
|
|
577
|
+
supported_system_screens: value.supported_system_screens
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function memberChoices(metadata) {
|
|
581
|
+
if (!metadata) return [];
|
|
582
|
+
return metadata.members_sites.flatMap((site) => {
|
|
583
|
+
const prefix = `Member · ${site.name} (#${site.id}) · `;
|
|
584
|
+
const selection = { members_site_id: site.id };
|
|
585
|
+
return [...site.pages.map((page) => ({
|
|
586
|
+
title: `${prefix}${page.title}${page.published ? "" : " (draft)"} — ${page.path}`,
|
|
587
|
+
value: memberPreviewUrl(page.path, selection)
|
|
588
|
+
})), ...metadata.system_screens.filter((screen) => metadata.supported_system_screens.includes(screen.key)).map((screen) => {
|
|
589
|
+
return {
|
|
590
|
+
title: `${prefix}${screen.key}`,
|
|
591
|
+
value: memberPreviewUrl(`/${screen.key}`, selection)
|
|
592
|
+
};
|
|
593
|
+
})];
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/** A watcher update may remove a page while the prompt is open. */
|
|
597
|
+
async function validateMemberDestination(address, themeId, path) {
|
|
598
|
+
if (!memberChoices(await readPreviewMetadata(address, themeId)).some((choice) => choice.value === path)) throw new Error("This member page is no longer available. Run the navigator again to refresh the choices.");
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region ../../api-clients/themes/src/namespaces/v0.ts
|
|
602
|
+
/**
|
|
603
|
+
* List application themes
|
|
604
|
+
* Get all application themes with optional filters
|
|
605
|
+
*
|
|
606
|
+
* @param client - Fetch client instance
|
|
607
|
+
* @param [params] - params
|
|
608
|
+
*/
|
|
609
|
+
async function listApplicationThemes(client, params) {
|
|
610
|
+
return client.get(`/api/application_themes`, params);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Create an application theme
|
|
614
|
+
*
|
|
615
|
+
*
|
|
616
|
+
* @param client - Fetch client instance
|
|
617
|
+
* @param body - body
|
|
618
|
+
*/
|
|
619
|
+
async function createApplicationTheme(client, body) {
|
|
620
|
+
return client.post(`/api/application_themes`, body);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get an application theme
|
|
624
|
+
*
|
|
625
|
+
*
|
|
626
|
+
* @param client - Fetch client instance
|
|
627
|
+
* @param id - id
|
|
628
|
+
* @param [params] - params
|
|
629
|
+
*/
|
|
630
|
+
async function getApplicationTheme(client, id, params) {
|
|
631
|
+
return client.get(`/api/application_themes/${id}`, params);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Returns available themeables for a given type scoped to the theme's company
|
|
635
|
+
*
|
|
636
|
+
*
|
|
637
|
+
* @param client - Fetch client instance
|
|
638
|
+
* @param id - id
|
|
639
|
+
* @param [params] - params
|
|
640
|
+
*/
|
|
641
|
+
async function getApplicationThemeAvailableThemeables(client, id, params) {
|
|
642
|
+
return client.get(`/api/application_themes/${id}/available_themeables`, params);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Create a development reference clone of an application theme
|
|
646
|
+
* Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.
|
|
647
|
+
*
|
|
648
|
+
* @param client - Fetch client instance
|
|
649
|
+
* @param id - id
|
|
650
|
+
* @param body - body
|
|
651
|
+
*/
|
|
652
|
+
async function cloneApplicationThemeForDevelopment(client, id, body) {
|
|
653
|
+
return client.post(`/api/application_themes/${id}/clone_for_development`, body);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Publishes the theme
|
|
657
|
+
*
|
|
658
|
+
*
|
|
659
|
+
* @param client - Fetch client instance
|
|
660
|
+
* @param id - id
|
|
661
|
+
*/
|
|
662
|
+
async function publishApplicationTheme(client, id) {
|
|
663
|
+
return client.post(`/api/application_themes/${id}/publish`);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get theme assets
|
|
667
|
+
*
|
|
668
|
+
*
|
|
669
|
+
* @param client - Fetch client instance
|
|
670
|
+
* @param id - id
|
|
671
|
+
*/
|
|
672
|
+
async function getThemeAssets(client, id) {
|
|
673
|
+
return client.get(`/api/application_themes/${id}/theme_assets`);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Lists all theme resources
|
|
677
|
+
* Lists resources for a theme owned by the current company, including the canonical members_sites.json snapshot. Themes without member sites receive an explicit empty version 2 document. Companyless root themes are not accessible through this endpoint. The content field remains JSON-encoded text.
|
|
678
|
+
*
|
|
679
|
+
* @param client - Fetch client instance
|
|
680
|
+
* @param application_theme_id - application_theme_id
|
|
681
|
+
*/
|
|
682
|
+
async function listThemeResources(client, application_theme_id) {
|
|
683
|
+
return client.get(`/api/application_themes/${application_theme_id}/resources`);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Updates a theme resource
|
|
687
|
+
* Submit member template dependencies together with members_sites.json using member_files so navigation is captured before any file changes. The files and member graph apply atomically; a rejected dependency rolls back the bundle. Existing single-resource writes remain supported. Missing manifests preserve existing configuration; deleting the file is refused.
|
|
688
|
+
*
|
|
689
|
+
* @param client - Fetch client instance
|
|
690
|
+
* @param application_theme_id - application_theme_id
|
|
691
|
+
* @param body - body
|
|
692
|
+
*/
|
|
693
|
+
async function updateThemeResource(client, application_theme_id, body) {
|
|
694
|
+
return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Deletes a theme resource
|
|
698
|
+
*
|
|
699
|
+
*
|
|
700
|
+
* @param client - Fetch client instance
|
|
701
|
+
* @param application_theme_id - application_theme_id
|
|
702
|
+
* @param body - body
|
|
703
|
+
*/
|
|
704
|
+
async function deleteThemeResource(client, application_theme_id, body) {
|
|
705
|
+
return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Creates a file resource
|
|
709
|
+
*
|
|
710
|
+
*
|
|
711
|
+
* @param client - Fetch client instance
|
|
712
|
+
* @param body - body
|
|
713
|
+
*/
|
|
714
|
+
async function createFileResource(client, body) {
|
|
715
|
+
return client.post(`/api/file_resources`, body);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Deletes a file resource
|
|
719
|
+
*
|
|
720
|
+
*
|
|
721
|
+
* @param client - Fetch client instance
|
|
722
|
+
* @param id - id
|
|
723
|
+
*/
|
|
724
|
+
async function destroyFileResource(client, id) {
|
|
725
|
+
return client.delete(`/api/file_resources/${id}`);
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/commands/navigate.ts
|
|
729
|
+
function localSuggest(input, choices) {
|
|
730
|
+
if (!input) return choices;
|
|
731
|
+
const lower = input.toLowerCase();
|
|
732
|
+
return choices.filter((c) => c.title.toLowerCase().includes(lower));
|
|
733
|
+
}
|
|
734
|
+
const THEMEABLE_TYPE_MAP = {
|
|
735
|
+
"/home": "home_page",
|
|
736
|
+
"/home/shop": "shop_page",
|
|
737
|
+
"/home/join": "join_page",
|
|
738
|
+
"/cart": "cart_page",
|
|
739
|
+
"/home/blog": "post_page",
|
|
740
|
+
"/home/categories": "category_page",
|
|
741
|
+
"/home/collections": "collection_page"
|
|
742
|
+
};
|
|
743
|
+
const STATIC_ROUTES = [
|
|
744
|
+
{
|
|
745
|
+
label: "Home",
|
|
746
|
+
path: "/home"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
label: "Shop",
|
|
750
|
+
path: "/home/shop"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
label: "Join / Sign Up",
|
|
754
|
+
path: "/home/join"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
label: "Cart",
|
|
758
|
+
path: "/cart"
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
label: "Blog",
|
|
762
|
+
path: "/home/blog"
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
label: "Categories (all)",
|
|
766
|
+
path: "/home/categories"
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
label: "Collections (all)",
|
|
770
|
+
path: "/home/collections"
|
|
771
|
+
}
|
|
772
|
+
];
|
|
773
|
+
const RESOURCE_ROUTES = [
|
|
774
|
+
{
|
|
775
|
+
label: "Category",
|
|
776
|
+
type: "category",
|
|
777
|
+
template: "/home/categories/%s",
|
|
778
|
+
fallback: "/home/categories"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
label: "Collection",
|
|
782
|
+
type: "collection",
|
|
783
|
+
template: "/home/collections/%s",
|
|
784
|
+
fallback: "/home/collections"
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
label: "Product",
|
|
788
|
+
type: "product",
|
|
789
|
+
template: "/home/products/%s",
|
|
790
|
+
fallback: "/home/shop"
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
label: "Library",
|
|
794
|
+
type: "library",
|
|
795
|
+
template: "/home/libraries/%s",
|
|
796
|
+
fallback: "/home/libraries"
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
label: "Post",
|
|
800
|
+
type: "post",
|
|
801
|
+
template: "/home/posts/%s",
|
|
802
|
+
fallback: "/home/blog"
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
label: "Media",
|
|
806
|
+
type: "medium",
|
|
807
|
+
template: "/home/media/%s",
|
|
808
|
+
fallback: "/home/media"
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
label: "Enrollment Pack",
|
|
812
|
+
type: "enrollment_pack",
|
|
813
|
+
template: "/home/enrollments/%s",
|
|
814
|
+
fallback: "/home/join"
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
label: "Page",
|
|
818
|
+
type: "page",
|
|
819
|
+
template: "/home/pages/%s",
|
|
820
|
+
fallback: "/home/pages"
|
|
821
|
+
}
|
|
822
|
+
];
|
|
823
|
+
async function fetchTemplatesForType(api, themeId, themeableType) {
|
|
824
|
+
const params = new URLSearchParams({
|
|
825
|
+
application_theme_id: String(themeId),
|
|
826
|
+
themeable_type: themeableType,
|
|
827
|
+
published: "true"
|
|
828
|
+
});
|
|
829
|
+
return (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
|
|
830
|
+
}
|
|
831
|
+
async function selectTemplate(api, themeId, themeableType, onCancel) {
|
|
832
|
+
const templates = await fetchTemplatesForType(api, themeId, themeableType);
|
|
833
|
+
if (templates.length <= 1) return null;
|
|
834
|
+
const { templateId } = await prompts({
|
|
835
|
+
type: "autocomplete",
|
|
836
|
+
name: "templateId",
|
|
837
|
+
message: "Select a template",
|
|
838
|
+
choices: templates.map((t) => ({
|
|
839
|
+
title: `${t.name}${t.default ? " (default)" : ""}`,
|
|
840
|
+
value: t.id
|
|
841
|
+
})),
|
|
842
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
843
|
+
}, { onCancel });
|
|
844
|
+
return templateId ?? null;
|
|
845
|
+
}
|
|
846
|
+
function createNavigateCommand() {
|
|
847
|
+
return new Command("navigate").description("Interactively navigate to a route in the dev server browser").option("--host <host>", "Dev server host", "127.0.0.1").option("--port <port>", "Dev server port", "9292").option("-t, --theme <id>", "Theme ID (defaults to active dev theme)").action(async (opts) => {
|
|
848
|
+
requireToken();
|
|
849
|
+
const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();
|
|
850
|
+
if (!themeId) {
|
|
851
|
+
console.error("No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.");
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
const address = `http://${opts.host}:${opts.port}`;
|
|
855
|
+
await navigateTheme(createApiClient(), themeId, address);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** Shared by theme navigate and theme dev --navigate. */
|
|
859
|
+
async function navigateTheme(api, themeId, address) {
|
|
860
|
+
const choices = [...STATIC_ROUTES.map((r) => ({
|
|
861
|
+
title: r.label,
|
|
862
|
+
value: r.path
|
|
863
|
+
})), ...RESOURCE_ROUTES.map((r) => ({
|
|
864
|
+
title: `${r.label} (select specific)`,
|
|
865
|
+
value: {
|
|
866
|
+
resourceType: r.type,
|
|
867
|
+
template: r.template,
|
|
868
|
+
fallback: r.fallback,
|
|
869
|
+
label: r.label
|
|
870
|
+
}
|
|
871
|
+
}))];
|
|
872
|
+
let cancelled = false;
|
|
873
|
+
const onCancel = () => {
|
|
874
|
+
cancelled = true;
|
|
875
|
+
return false;
|
|
876
|
+
};
|
|
877
|
+
const metadata = await readPreviewMetadata(address, themeId);
|
|
878
|
+
choices.push(...memberChoices(metadata));
|
|
879
|
+
const unavailable = metadata?.system_screens.filter((screen) => !metadata.supported_system_screens.includes(screen.key));
|
|
880
|
+
if (unavailable?.length) console.log(`System-screen preview unavailable: ${unavailable.map((screen) => screen.key).join(", ")}.`);
|
|
881
|
+
const { dest } = await prompts({
|
|
882
|
+
type: "autocomplete",
|
|
883
|
+
name: "dest",
|
|
884
|
+
message: "Select a route",
|
|
885
|
+
choices,
|
|
886
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
887
|
+
}, { onCancel });
|
|
888
|
+
if (!dest) return;
|
|
889
|
+
let path;
|
|
890
|
+
let themeableType;
|
|
891
|
+
if (typeof dest === "string") {
|
|
892
|
+
if (dest.startsWith("/_members/")) await validateMemberDestination(address, themeId, dest);
|
|
893
|
+
path = dest;
|
|
894
|
+
themeableType = THEMEABLE_TYPE_MAP[dest];
|
|
895
|
+
} else {
|
|
896
|
+
themeableType = dest.resourceType;
|
|
897
|
+
const resources = (await getApplicationThemeAvailableThemeables(api, themeId, {
|
|
898
|
+
themeable: dest.resourceType,
|
|
899
|
+
per_page: 50
|
|
900
|
+
})).available_themeables ?? [];
|
|
901
|
+
if (!resources.length) {
|
|
902
|
+
console.log(`No ${dest.label} resources found, using listing page.`);
|
|
903
|
+
path = dest.fallback;
|
|
904
|
+
} else {
|
|
905
|
+
const resourceChoices = resources.map((r) => ({
|
|
906
|
+
title: r.title ?? r.slug ?? "Untitled",
|
|
907
|
+
value: r.slug
|
|
908
|
+
}));
|
|
909
|
+
const { slug } = await prompts({
|
|
910
|
+
type: "autocomplete",
|
|
911
|
+
name: "slug",
|
|
912
|
+
message: `Select a ${dest.label.toLowerCase()}`,
|
|
913
|
+
choices: resourceChoices,
|
|
914
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
915
|
+
}, { onCancel });
|
|
916
|
+
if (cancelled || !slug) return;
|
|
917
|
+
path = dest.template.replace("%s", encodeURIComponent(slug));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
let templateParam = "";
|
|
921
|
+
if (themeableType) {
|
|
922
|
+
const templateId = await selectTemplate(api, themeId, themeableType, onCancel);
|
|
923
|
+
if (templateId) templateParam = `?theme_template_id=${templateId}`;
|
|
924
|
+
}
|
|
925
|
+
if (cancelled) return;
|
|
926
|
+
const url = `${address}${path}${templateParam}`;
|
|
927
|
+
console.log(`\nNavigating to: ${url}\n`);
|
|
928
|
+
const open = (await import("open")).default;
|
|
929
|
+
await open(url);
|
|
930
|
+
}
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/theme-config.ts
|
|
933
|
+
const CONFIG_FILE = ".fluid-theme.json";
|
|
934
|
+
/**
|
|
935
|
+
* `company` must be the bare subdomain slug — the dev server builds
|
|
936
|
+
* `<company>.fluid.app` from it, so a full domain here produces an
|
|
937
|
+
* unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling
|
|
938
|
+
* writes the full domain (or a URL); accept those and reduce them to
|
|
939
|
+
* the subdomain.
|
|
940
|
+
*/
|
|
941
|
+
function normalizeCompany(company) {
|
|
942
|
+
return company.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/[/?#].*$/, "").replace(/\.fluid\.app$/i, "");
|
|
943
|
+
}
|
|
944
|
+
function configPath(themeRoot) {
|
|
945
|
+
return join(themeRoot, CONFIG_FILE);
|
|
946
|
+
}
|
|
947
|
+
/** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
|
|
948
|
+
function readThemeConfig(themeRoot) {
|
|
949
|
+
const path = configPath(themeRoot);
|
|
950
|
+
if (!existsSync(path)) return null;
|
|
951
|
+
try {
|
|
952
|
+
const raw = readFileSync(path, "utf-8");
|
|
953
|
+
const config = JSON.parse(raw);
|
|
954
|
+
if (typeof config.company === "string") config.company = normalizeCompany(config.company);
|
|
955
|
+
return config;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Read a legacy config (pre-shadow-repo). Used only by the migration
|
|
962
|
+
* path on first new-CLI pull to seed shadow HEAD from files whose
|
|
963
|
+
* local content still matches their stored sha256 checksum.
|
|
479
964
|
*/
|
|
480
|
-
function
|
|
481
|
-
const
|
|
482
|
-
|
|
965
|
+
function readLegacyThemeConfig(themeRoot) {
|
|
966
|
+
const path = configPath(themeRoot);
|
|
967
|
+
if (!existsSync(path)) return null;
|
|
968
|
+
try {
|
|
969
|
+
const raw = readFileSync(path, "utf-8");
|
|
970
|
+
return JSON.parse(raw);
|
|
971
|
+
} catch {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/** Write `.fluid-theme.json` to a theme directory. */
|
|
976
|
+
function writeThemeConfig(themeRoot, config) {
|
|
977
|
+
writeFileSync(configPath(themeRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
483
978
|
}
|
|
484
979
|
//#endregion
|
|
485
980
|
//#region src/theme/mime-type.ts
|
|
@@ -533,7 +1028,8 @@ function mimeTypeFor(ext) {
|
|
|
533
1028
|
const THEME_LEVEL_RESOURCE_KEYS = new Set([
|
|
534
1029
|
"global_styles.css",
|
|
535
1030
|
"styles.css",
|
|
536
|
-
"variables.json"
|
|
1031
|
+
"variables.json",
|
|
1032
|
+
"members_sites.json"
|
|
537
1033
|
]);
|
|
538
1034
|
const COMPOSITE_RESOURCE_FILE_NAMES = new Set([
|
|
539
1035
|
"index.liquid",
|
|
@@ -1072,37 +1568,39 @@ var ThemeRoot = class {
|
|
|
1072
1568
|
}
|
|
1073
1569
|
};
|
|
1074
1570
|
//#endregion
|
|
1075
|
-
//#region src/theme/dev-server/
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1571
|
+
//#region src/theme/dev-server/member-screen-assets.ts
|
|
1572
|
+
/** The runtime is shipped with this CLI, not taken from a mutable theme/CDN URL. */
|
|
1573
|
+
function findMemberScreenAssets() {
|
|
1574
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
1575
|
+
while (true) {
|
|
1576
|
+
for (const candidate of [join(directory, "dist-cli-preview"), join(directory, "dist", "dist-cli-preview")]) if (existsSync(join(candidate, "capabilities.json"))) return candidate;
|
|
1577
|
+
const parent = dirname(directory);
|
|
1578
|
+
if (parent === directory) return void 0;
|
|
1579
|
+
directory = parent;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
var MemberScreenAssets = class {
|
|
1583
|
+
screens;
|
|
1584
|
+
constructor(directory = findMemberScreenAssets()) {
|
|
1585
|
+
this.directory = directory;
|
|
1586
|
+
this.screens = [];
|
|
1587
|
+
if (!directory) return;
|
|
1588
|
+
try {
|
|
1589
|
+
const value = JSON.parse(readFileSync(join(directory, "capabilities.json"), "utf8"));
|
|
1590
|
+
if (typeof value !== "object" || value === null || !("version" in value) || value.version !== 1 || !("screens" in value) || !Array.isArray(value.screens)) return;
|
|
1591
|
+
if (!value.screens.every((screen) => typeof screen === "string" && /^[a-z][a-z-]*$/.test(screen))) return;
|
|
1592
|
+
if (!existsSync(join(directory, "account-screens.js"))) return;
|
|
1593
|
+
this.screens = value.screens;
|
|
1594
|
+
} catch {}
|
|
1088
1595
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1596
|
+
read(path) {
|
|
1597
|
+
if (!this.directory || !/^(?:account-screens\.js|chunks\/[a-zA-Z0-9_.-]+\.js)$/.test(path)) return void 0;
|
|
1598
|
+
try {
|
|
1599
|
+
return readFileSync(join(this.directory, path));
|
|
1093
1600
|
} catch {
|
|
1094
|
-
|
|
1601
|
+
return;
|
|
1095
1602
|
}
|
|
1096
1603
|
}
|
|
1097
|
-
close() {
|
|
1098
|
-
for (const res of this.responses) try {
|
|
1099
|
-
res.end();
|
|
1100
|
-
} catch {}
|
|
1101
|
-
this.responses.clear();
|
|
1102
|
-
}
|
|
1103
|
-
get size() {
|
|
1104
|
-
return this.responses.size;
|
|
1105
|
-
}
|
|
1106
1604
|
};
|
|
1107
1605
|
//#endregion
|
|
1108
1606
|
//#region src/theme/dev-server/hot-reload.ts
|
|
@@ -1166,6 +1664,287 @@ function injectHotReload(html, mode) {
|
|
|
1166
1664
|
return html + script;
|
|
1167
1665
|
}
|
|
1168
1666
|
//#endregion
|
|
1667
|
+
//#region src/theme/dev-server/member-preview-html.ts
|
|
1668
|
+
function escapeHtml(value) {
|
|
1669
|
+
return value.replace(/[&<>"']/g, (char) => ({
|
|
1670
|
+
"&": "&",
|
|
1671
|
+
"<": "<",
|
|
1672
|
+
">": ">",
|
|
1673
|
+
"\"": """,
|
|
1674
|
+
"'": "'"
|
|
1675
|
+
})[char] ?? char);
|
|
1676
|
+
}
|
|
1677
|
+
/** Use the configured Rails environment, never a browser-supplied upstream. */
|
|
1678
|
+
function memberOrigin(apiBase, company) {
|
|
1679
|
+
const url = new URL(apiBase);
|
|
1680
|
+
if (!/^[a-z0-9-]+$/i.test(company)) throw new Error("Invalid company subdomain.");
|
|
1681
|
+
if (url.hostname.startsWith("api.")) url.hostname = `${company}.account.${url.hostname.slice(4)}`;
|
|
1682
|
+
return url.origin;
|
|
1683
|
+
}
|
|
1684
|
+
function renderMemberHtml(render, opts) {
|
|
1685
|
+
const document = parse(render.html);
|
|
1686
|
+
const elements = [];
|
|
1687
|
+
function visit(parent) {
|
|
1688
|
+
for (const child of parent.childNodes) if ("tagName" in child) {
|
|
1689
|
+
elements.push(child);
|
|
1690
|
+
visit(child);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
visit(document);
|
|
1694
|
+
const head = elements.find((el) => el.tagName === "head");
|
|
1695
|
+
for (const el of elements) {
|
|
1696
|
+
if (el.tagName === "base" && el.parentNode) el.parentNode.childNodes.splice(el.parentNode.childNodes.indexOf(el), 1);
|
|
1697
|
+
if (el.tagName === "script") {
|
|
1698
|
+
const src = el.attrs.find((attr) => attr.name === "src")?.value ?? "";
|
|
1699
|
+
if (/\/(?:account-screens(?:\/|\.)|member-storefront-sdk\/)/.test(src) && el.parentNode) el.parentNode.childNodes.splice(el.parentNode.childNodes.indexOf(el), 1);
|
|
1700
|
+
}
|
|
1701
|
+
if (el.tagName === "fluid-account-screen" || el.tagName === "fluid-messaging") {
|
|
1702
|
+
el.attrs = el.attrs.filter((attr) => attr.name !== "preview" && attr.name !== "path");
|
|
1703
|
+
el.attrs.push({
|
|
1704
|
+
name: "preview",
|
|
1705
|
+
value: "true"
|
|
1706
|
+
});
|
|
1707
|
+
if (opts.screenKey) el.attrs.push({
|
|
1708
|
+
name: "path",
|
|
1709
|
+
value: new URL(opts.path, opts.origin).pathname.slice(1)
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
if (el.tagName === "form") {
|
|
1713
|
+
el.attrs = el.attrs.filter((attr) => attr.name !== "action");
|
|
1714
|
+
el.attrs.push({
|
|
1715
|
+
name: "action",
|
|
1716
|
+
value: `${opts.localOrigin}/_members/_actions`
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
const formAction = el.attrs.find((attr) => attr.name === "formaction");
|
|
1720
|
+
if (formAction) formAction.value = `${opts.localOrigin}/_members/_actions`;
|
|
1721
|
+
if (el.tagName === "a" || el.tagName === "area") {
|
|
1722
|
+
const href = el.attrs.find((a) => a.name === "href");
|
|
1723
|
+
if (href) href.value = previewLink(href.value, opts);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const runtime = parseFragment(`<script type="module" src="${escapeHtml(opts.localOrigin)}/_members/_runtime/account-screens.js"><\/script>`);
|
|
1727
|
+
head.childNodes.unshift(...runtime.childNodes);
|
|
1728
|
+
const base = parseFragment(`<base href="${escapeHtml(new URL(opts.path, opts.origin).href)}">`);
|
|
1729
|
+
head.childNodes.unshift(...base.childNodes);
|
|
1730
|
+
const styles = parseFragment(render.styles);
|
|
1731
|
+
head.childNodes.push(...styles.childNodes);
|
|
1732
|
+
const style = parseFragment("<style></style>").childNodes[0];
|
|
1733
|
+
if (style && "childNodes" in style) {
|
|
1734
|
+
style.childNodes.push({
|
|
1735
|
+
nodeName: "#text",
|
|
1736
|
+
value: render.templateStyles,
|
|
1737
|
+
parentNode: style
|
|
1738
|
+
});
|
|
1739
|
+
head.childNodes.push(style);
|
|
1740
|
+
}
|
|
1741
|
+
const guard = parseFragment(`<script>document.addEventListener("submit",function(event){event.preventDefault();},true);<\/script>`);
|
|
1742
|
+
head.childNodes.unshift(...guard.childNodes);
|
|
1743
|
+
const navigation = parseFragment(`<script>
|
|
1744
|
+
document.addEventListener("fluid-account-navigate",function(event){
|
|
1745
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
1746
|
+
var slug=event.detail && event.detail.slug;
|
|
1747
|
+
if(typeof slug!=="string" || !/^[a-z][a-z0-9_-]*(?:[/?#]|$)/.test(slug) || slug.includes("\\\\")) return;
|
|
1748
|
+
var next=new URL("/_members/"+slug,window.location.origin);
|
|
1749
|
+
if(next.origin!==window.location.origin || !next.pathname.startsWith("/_members/")) return;
|
|
1750
|
+
var context=${JSON.stringify(opts.selection).replace(/</g, "\\u003c")};
|
|
1751
|
+
["members_site_id","country_id","language_id","member_type_id"].forEach(function(key){
|
|
1752
|
+
if(context[key]===undefined) next.searchParams.delete(key); else next.searchParams.set(key,context[key]);
|
|
1753
|
+
});
|
|
1754
|
+
window.location.assign(next.href);
|
|
1755
|
+
},true);
|
|
1756
|
+
<\/script>`);
|
|
1757
|
+
head.childNodes.unshift(...navigation.childNodes);
|
|
1758
|
+
return injectHotReload(serialize(document), opts.reloadMode);
|
|
1759
|
+
}
|
|
1760
|
+
function previewLink(href, opts) {
|
|
1761
|
+
if (href.startsWith("#")) return opts.localOrigin + memberPreviewUrl(opts.path, opts.selection) + href;
|
|
1762
|
+
let target;
|
|
1763
|
+
try {
|
|
1764
|
+
target = new URL(href, new URL(opts.path, opts.origin));
|
|
1765
|
+
} catch {
|
|
1766
|
+
return href;
|
|
1767
|
+
}
|
|
1768
|
+
if (!["http:", "https:"].includes(target.protocol)) return href;
|
|
1769
|
+
const absolute = /^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith("//");
|
|
1770
|
+
const accountLink = absolute && target.origin === opts.origin;
|
|
1771
|
+
if (absolute && !accountLink && target.origin !== opts.localOrigin) return href;
|
|
1772
|
+
target.pathname = accountPath(target.pathname);
|
|
1773
|
+
const isMember = accountLink || /^\/(auth(?:\/|$)|login$|logout$)/.test(target.pathname) || target.pathname.startsWith("/_members/") || opts.metadata.members_sites.some((site) => site.pages.some((p) => p.path === target.pathname)) || opts.metadata.system_screens.some((screen) => target.pathname === `/${screen.key}` || target.pathname.startsWith(`/${screen.key}/`));
|
|
1774
|
+
const path = target.pathname + target.search + target.hash;
|
|
1775
|
+
return opts.localOrigin + (isMember ? memberPreviewUrl(path, opts.selection) : path);
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/theme/dev-server/member-preview.ts
|
|
1779
|
+
/** Deny DNS rebinding and cross-origin access before using Node's CLI credential. */
|
|
1780
|
+
function isLocalPreviewRequest(req, host) {
|
|
1781
|
+
const port = req.socket.localPort;
|
|
1782
|
+
const hosts = new Set([
|
|
1783
|
+
host,
|
|
1784
|
+
"localhost",
|
|
1785
|
+
"127.0.0.1",
|
|
1786
|
+
"[::1]"
|
|
1787
|
+
]);
|
|
1788
|
+
if ((host === "0.0.0.0" || host === "::") && req.socket.localAddress) {
|
|
1789
|
+
const address = req.socket.localAddress.replace(/^::ffff:/, "");
|
|
1790
|
+
hosts.add(address.includes(":") ? `[${address}]` : address);
|
|
1791
|
+
}
|
|
1792
|
+
let url;
|
|
1793
|
+
try {
|
|
1794
|
+
url = new URL(`http://${req.headers.host}`);
|
|
1795
|
+
} catch {
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
if (!hosts.has(url.hostname) || Number(url.port || 80) !== port || url.username || url.password || url.pathname !== "/") return false;
|
|
1799
|
+
if (req.headers.origin && req.headers.origin !== url.origin) return false;
|
|
1800
|
+
return req.headers["sec-fetch-site"] !== "cross-site";
|
|
1801
|
+
}
|
|
1802
|
+
var MemberPreview = class {
|
|
1803
|
+
metadata;
|
|
1804
|
+
loading;
|
|
1805
|
+
origin;
|
|
1806
|
+
constructor(api, theme, reloadMode, screenAssets = new MemberScreenAssets()) {
|
|
1807
|
+
this.api = api;
|
|
1808
|
+
this.theme = theme;
|
|
1809
|
+
this.reloadMode = reloadMode;
|
|
1810
|
+
this.screenAssets = screenAssets;
|
|
1811
|
+
this.origin = memberOrigin(getApiBase(), theme.company);
|
|
1812
|
+
}
|
|
1813
|
+
/** Only call after every upload in a watcher batch has been acknowledged. */
|
|
1814
|
+
async refreshAfterSync() {
|
|
1815
|
+
if (!this.metadata && !this.loading) return;
|
|
1816
|
+
if (this.loading) await this.loading;
|
|
1817
|
+
this.metadata = void 0;
|
|
1818
|
+
this.metadata = await this.loadMetadata();
|
|
1819
|
+
}
|
|
1820
|
+
async loadMetadata() {
|
|
1821
|
+
return parseMemberMetadata(await this.api.get("/api/company/members_sites", { application_theme_id: this.theme.id }));
|
|
1822
|
+
}
|
|
1823
|
+
async getMetadata() {
|
|
1824
|
+
if (this.metadata) return this.metadata;
|
|
1825
|
+
this.loading ??= this.loadMetadata();
|
|
1826
|
+
try {
|
|
1827
|
+
this.metadata = await this.loading;
|
|
1828
|
+
return this.metadata;
|
|
1829
|
+
} finally {
|
|
1830
|
+
this.loading = void 0;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async handle(req, res) {
|
|
1834
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1835
|
+
if (url.pathname !== "/_members" && !url.pathname.startsWith("/_members/")) return false;
|
|
1836
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1837
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
1838
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1839
|
+
try {
|
|
1840
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1841
|
+
res.setHeader("Allow", "GET, HEAD");
|
|
1842
|
+
throw new MemberPreviewError("Member preview is read-only.", 405);
|
|
1843
|
+
}
|
|
1844
|
+
if (url.pathname.startsWith("/_members/_runtime/")) {
|
|
1845
|
+
const asset = this.screenAssets.read(url.pathname.slice(19));
|
|
1846
|
+
if (!asset) throw new MemberPreviewError("Preview runtime asset unavailable. Rebuild or update the CLI.", 404);
|
|
1847
|
+
res.setHeader("Content-Type", "text/javascript; charset=utf-8");
|
|
1848
|
+
res.end(req.method === "HEAD" ? void 0 : asset);
|
|
1849
|
+
return true;
|
|
1850
|
+
}
|
|
1851
|
+
const metadata = await this.getMetadata();
|
|
1852
|
+
if (url.pathname === "/_members/_metadata") {
|
|
1853
|
+
res.setHeader("Content-Type", "application/json");
|
|
1854
|
+
res.end(req.method === "HEAD" ? void 0 : JSON.stringify({
|
|
1855
|
+
theme_id: this.theme.id,
|
|
1856
|
+
mode: "sample",
|
|
1857
|
+
...metadata,
|
|
1858
|
+
supported_system_screens: metadata.system_screens.filter((screen) => this.screenAssets.screens.includes(screen.key)).map((screen) => screen.key)
|
|
1859
|
+
}));
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
if (url.pathname === "/_members" || url.pathname === "/_members/") {
|
|
1863
|
+
const screens = metadata.system_screens.filter((screen) => this.screenAssets.screens.includes(screen.key));
|
|
1864
|
+
const links = metadata.members_sites.map((site) => {
|
|
1865
|
+
const destinations = [...site.pages.map((page) => ({
|
|
1866
|
+
path: page.path,
|
|
1867
|
+
title: page.title + (page.published ? "" : " (draft)")
|
|
1868
|
+
})), ...screens.map((screen) => ({
|
|
1869
|
+
path: `/${screen.key}`,
|
|
1870
|
+
title: screen.key + " (sample)"
|
|
1871
|
+
}))];
|
|
1872
|
+
return `<h2>${escapeHtml(site.name)}</h2><ul>${destinations.map((page) => `<li><a href="${escapeHtml(memberPreviewUrl(page.path, { members_site_id: site.id }))}">${escapeHtml(page.title)}</a></li>`).join("")}</ul>`;
|
|
1873
|
+
}).join("");
|
|
1874
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1875
|
+
res.end(req.method === "HEAD" ? void 0 : `<!doctype html><meta charset="utf-8"><title>Member preview</title><h1>Member preview</h1><p>Sample data. Select a page to preview its site's layout.</p>${links || "<p>No member pages in this theme.</p>"}`);
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
const selection = parseSelection(url.searchParams);
|
|
1879
|
+
const site = metadata.members_sites.find((s) => s.id === selection.members_site_id);
|
|
1880
|
+
if (!site) throw new MemberPreviewError("This site no longer belongs to the preview theme. Select a page at /_members.", 404);
|
|
1881
|
+
const path = url.pathname.slice(9);
|
|
1882
|
+
const page = site.pages.find((p) => p.path === path);
|
|
1883
|
+
const screen = page ? void 0 : metadata.system_screens.find((s) => path === `/${s.key}` || path.startsWith(`/${s.key}/`));
|
|
1884
|
+
if (!page && !screen) throw new MemberPreviewError("This member page is unavailable in the selected site. Select a page at /_members.", 404);
|
|
1885
|
+
if (screen && !this.screenAssets.screens.includes(screen.key)) throw new MemberPreviewError("This system screen has no packaged sample preview. Rebuild or update the CLI.", 501);
|
|
1886
|
+
const html = renderMemberHtml(parseMemberRender(await this.api.post(`/api/v202604/themes/${this.theme.id}/member-preview`, {
|
|
1887
|
+
...selection,
|
|
1888
|
+
...page ? { members_page_id: page.id } : { system_screen_key: screen.key }
|
|
1889
|
+
}), this.theme.id, selection, page?.id ?? null, screen?.key ?? null), {
|
|
1890
|
+
origin: this.origin,
|
|
1891
|
+
localOrigin: url.origin,
|
|
1892
|
+
path: path + url.search,
|
|
1893
|
+
selection,
|
|
1894
|
+
metadata,
|
|
1895
|
+
reloadMode: this.reloadMode,
|
|
1896
|
+
screenKey: screen?.key
|
|
1897
|
+
});
|
|
1898
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1899
|
+
res.end(req.method === "HEAD" ? void 0 : html);
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
const status = error instanceof MemberPreviewError ? error.status : isRecord$5(error) && typeof error.status === "number" && [
|
|
1902
|
+
401,
|
|
1903
|
+
403,
|
|
1904
|
+
404,
|
|
1905
|
+
422
|
|
1906
|
+
].includes(error.status) ? error.status : 502;
|
|
1907
|
+
const message = error instanceof MemberPreviewError ? error.message : status === 401 || status === 403 ? "Member preview authorization failed. Check your CLI login and theme permissions." : "Member preview could not be rendered. Check the selected page and server preview support.";
|
|
1908
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1909
|
+
res.end(req.method === "HEAD" ? void 0 : message);
|
|
1910
|
+
}
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/theme/dev-server/sse.ts
|
|
1916
|
+
var SSEStream = class {
|
|
1917
|
+
responses = /* @__PURE__ */ new Set();
|
|
1918
|
+
add(res) {
|
|
1919
|
+
res.writeHead(200, {
|
|
1920
|
+
"Content-Type": "text/event-stream",
|
|
1921
|
+
"Cache-Control": "no-cache",
|
|
1922
|
+
Connection: "keep-alive",
|
|
1923
|
+
"Access-Control-Allow-Origin": "*"
|
|
1924
|
+
});
|
|
1925
|
+
res.write(":\n\n");
|
|
1926
|
+
this.responses.add(res);
|
|
1927
|
+
res.on("close", () => this.responses.delete(res));
|
|
1928
|
+
}
|
|
1929
|
+
broadcast(data) {
|
|
1930
|
+
const payload = `data: ${data}\n\n`;
|
|
1931
|
+
for (const res of this.responses) try {
|
|
1932
|
+
res.write(payload);
|
|
1933
|
+
} catch {
|
|
1934
|
+
this.responses.delete(res);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
close() {
|
|
1938
|
+
for (const res of this.responses) try {
|
|
1939
|
+
res.end();
|
|
1940
|
+
} catch {}
|
|
1941
|
+
this.responses.clear();
|
|
1942
|
+
}
|
|
1943
|
+
get size() {
|
|
1944
|
+
return this.responses.size;
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
//#endregion
|
|
1169
1948
|
//#region src/theme/dev-server/proxy.ts
|
|
1170
1949
|
const HOP_BY_HOP = new Set([
|
|
1171
1950
|
"connection",
|
|
@@ -1240,143 +2019,16 @@ async function proxyRequest(req, res, opts) {
|
|
|
1240
2019
|
reject(err);
|
|
1241
2020
|
});
|
|
1242
2021
|
if (body) proxyReq.write(body);
|
|
1243
|
-
proxyReq.end();
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
function readBody(req) {
|
|
1247
|
-
return new Promise((resolve, reject) => {
|
|
1248
|
-
const chunks = [];
|
|
1249
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
1250
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1251
|
-
req.on("error", reject);
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
//#endregion
|
|
1255
|
-
//#region ../../api-clients/themes/src/namespaces/v0.ts
|
|
1256
|
-
/**
|
|
1257
|
-
* List application themes
|
|
1258
|
-
* Get all application themes with optional filters
|
|
1259
|
-
*
|
|
1260
|
-
* @param client - Fetch client instance
|
|
1261
|
-
* @param [params] - params
|
|
1262
|
-
*/
|
|
1263
|
-
async function listApplicationThemes(client, params) {
|
|
1264
|
-
return client.get(`/api/application_themes`, params);
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Create an application theme
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
* @param client - Fetch client instance
|
|
1271
|
-
* @param body - body
|
|
1272
|
-
*/
|
|
1273
|
-
async function createApplicationTheme(client, body) {
|
|
1274
|
-
return client.post(`/api/application_themes`, body);
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Get an application theme
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
* @param client - Fetch client instance
|
|
1281
|
-
* @param id - id
|
|
1282
|
-
* @param [params] - params
|
|
1283
|
-
*/
|
|
1284
|
-
async function getApplicationTheme(client, id, params) {
|
|
1285
|
-
return client.get(`/api/application_themes/${id}`, params);
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Returns available themeables for a given type scoped to the theme's company
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
* @param client - Fetch client instance
|
|
1292
|
-
* @param id - id
|
|
1293
|
-
* @param [params] - params
|
|
1294
|
-
*/
|
|
1295
|
-
async function getApplicationThemeAvailableThemeables(client, id, params) {
|
|
1296
|
-
return client.get(`/api/application_themes/${id}/available_themeables`, params);
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Create a development reference clone of an application theme
|
|
1300
|
-
* Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.
|
|
1301
|
-
*
|
|
1302
|
-
* @param client - Fetch client instance
|
|
1303
|
-
* @param id - id
|
|
1304
|
-
* @param body - body
|
|
1305
|
-
*/
|
|
1306
|
-
async function cloneApplicationThemeForDevelopment(client, id, body) {
|
|
1307
|
-
return client.post(`/api/application_themes/${id}/clone_for_development`, body);
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Publishes the theme
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
* @param client - Fetch client instance
|
|
1314
|
-
* @param id - id
|
|
1315
|
-
*/
|
|
1316
|
-
async function publishApplicationTheme(client, id) {
|
|
1317
|
-
return client.post(`/api/application_themes/${id}/publish`);
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Get theme assets
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
* @param client - Fetch client instance
|
|
1324
|
-
* @param id - id
|
|
1325
|
-
*/
|
|
1326
|
-
async function getThemeAssets(client, id) {
|
|
1327
|
-
return client.get(`/api/application_themes/${id}/theme_assets`);
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Lists all theme resources
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
* @param client - Fetch client instance
|
|
1334
|
-
* @param application_theme_id - application_theme_id
|
|
1335
|
-
*/
|
|
1336
|
-
async function listThemeResources(client, application_theme_id) {
|
|
1337
|
-
return client.get(`/api/application_themes/${application_theme_id}/resources`);
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* Updates a theme resource
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
* @param client - Fetch client instance
|
|
1344
|
-
* @param application_theme_id - application_theme_id
|
|
1345
|
-
* @param body - body
|
|
1346
|
-
*/
|
|
1347
|
-
async function updateThemeResource(client, application_theme_id, body) {
|
|
1348
|
-
return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Deletes a theme resource
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
* @param client - Fetch client instance
|
|
1355
|
-
* @param application_theme_id - application_theme_id
|
|
1356
|
-
* @param body - body
|
|
1357
|
-
*/
|
|
1358
|
-
async function deleteThemeResource(client, application_theme_id, body) {
|
|
1359
|
-
return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Creates a file resource
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
* @param client - Fetch client instance
|
|
1366
|
-
* @param body - body
|
|
1367
|
-
*/
|
|
1368
|
-
async function createFileResource(client, body) {
|
|
1369
|
-
return client.post(`/api/file_resources`, body);
|
|
2022
|
+
proxyReq.end();
|
|
2023
|
+
});
|
|
1370
2024
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
async function destroyFileResource(client, id) {
|
|
1379
|
-
return client.delete(`/api/file_resources/${id}`);
|
|
2025
|
+
function readBody(req) {
|
|
2026
|
+
return new Promise((resolve, reject) => {
|
|
2027
|
+
const chunks = [];
|
|
2028
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2029
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
2030
|
+
req.on("error", reject);
|
|
2031
|
+
});
|
|
1380
2032
|
}
|
|
1381
2033
|
//#endregion
|
|
1382
2034
|
//#region src/theme/format-error.ts
|
|
@@ -1638,9 +2290,9 @@ function readDocument(path) {
|
|
|
1638
2290
|
}
|
|
1639
2291
|
}
|
|
1640
2292
|
function parseDocument$1(value) {
|
|
1641
|
-
if (!isRecord$
|
|
2293
|
+
if (!isRecord$4(value) || value["version"] !== MANIFEST_VERSION) throw new Error(`expected version ${MANIFEST_VERSION}`);
|
|
1642
2294
|
const rawAssets = value["assets"];
|
|
1643
|
-
if (!isRecord$
|
|
2295
|
+
if (!isRecord$4(rawAssets)) throw new Error("expected an assets object");
|
|
1644
2296
|
const assets = {};
|
|
1645
2297
|
for (const [key, rawLink] of Object.entries(rawAssets)) {
|
|
1646
2298
|
if (!isThemeAssetKey(key) || !isThemeAssetLink(rawLink)) throw new Error(`invalid asset entry for ${key}`);
|
|
@@ -1675,14 +2327,14 @@ function copyLink(link) {
|
|
|
1675
2327
|
};
|
|
1676
2328
|
}
|
|
1677
2329
|
function isThemeAssetLink(value) {
|
|
1678
|
-
return isRecord$
|
|
2330
|
+
return isRecord$4(value) && typeof value["sourceThemeId"] === "number" && Number.isInteger(value["sourceThemeId"]) && value["sourceThemeId"] > 0 && (value["checksum"] === void 0 || typeof value["checksum"] === "string" && value["checksum"].length > 0) && (value["url"] === void 0 || typeof value["url"] === "string" && value["url"].length > 0) && (value["contentType"] === void 0 || typeof value["contentType"] === "string" && value["contentType"].length > 0) && (value["contentSize"] === void 0 || typeof value["contentSize"] === "number" && Number.isInteger(value["contentSize"]) && value["contentSize"] > 0) && (value["previewImageUrl"] === void 0 || typeof value["previewImageUrl"] === "string" && value["previewImageUrl"].length > 0) && (value["altText"] === void 0 || typeof value["altText"] === "string") && (value["handle"] === void 0 || typeof value["handle"] === "string" && value["handle"].length > 0) && (value["pending"] === void 0 || typeof value["pending"] === "boolean") && (value["damAssetCode"] === void 0 || typeof value["damAssetCode"] === "string" && value["damAssetCode"].length > 0);
|
|
1679
2331
|
}
|
|
1680
2332
|
function isThemeAssetKey(key) {
|
|
1681
2333
|
if (key.includes("\\") || key.includes("\0")) return false;
|
|
1682
2334
|
const segments = key.split("/");
|
|
1683
2335
|
return segments[0] === "assets" && segments.length === 2 && segments[1] !== void 0 && segments[1].length > 0 && segments[1] !== "." && segments[1] !== "..";
|
|
1684
2336
|
}
|
|
1685
|
-
function isRecord$
|
|
2337
|
+
function isRecord$4(value) {
|
|
1686
2338
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1687
2339
|
}
|
|
1688
2340
|
//#endregion
|
|
@@ -1692,6 +2344,81 @@ function isStylesheetKey(key) {
|
|
|
1692
2344
|
return STYLESHEET_KEY_PATTERN.test(key);
|
|
1693
2345
|
}
|
|
1694
2346
|
//#endregion
|
|
2347
|
+
//#region src/theme/member-dependencies.ts
|
|
2348
|
+
/** The same file graph is used for uploads and incoming omission protection.
|
|
2349
|
+
* Rails remains responsible for resolving company-specific roles and handles. */
|
|
2350
|
+
function memberDependencyPaths(document, availablePaths, deletedPaths = [], changedPaths = availablePaths) {
|
|
2351
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
2352
|
+
for (const entry of [...records(document.templates), ...records(document.menu_references)]) if (typeof entry.path === "string") dependencies.add(entry.path);
|
|
2353
|
+
for (const site of records(document.sites)) {
|
|
2354
|
+
for (const page of records(site.pages)) if (typeof page.template_path === "string") dependencies.add(page.template_path);
|
|
2355
|
+
const layout = site.layout_name;
|
|
2356
|
+
if (layout === "none") continue;
|
|
2357
|
+
if (typeof layout === "string" && layout && !["member", "theme"].includes(layout)) {
|
|
2358
|
+
dependencies.add(`layouts/${layout}.liquid`);
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const fallback = [
|
|
2362
|
+
layout,
|
|
2363
|
+
"member",
|
|
2364
|
+
"theme"
|
|
2365
|
+
].filter((name) => typeof name === "string" && name.length > 0).map((name) => `layouts/${name}.liquid`).find((path) => availablePaths.has(path) || deletedPaths.includes(path));
|
|
2366
|
+
if (fallback) dependencies.add(fallback);
|
|
2367
|
+
}
|
|
2368
|
+
for (const path of dependencies) {
|
|
2369
|
+
if (!path.endsWith("/index.liquid")) continue;
|
|
2370
|
+
for (const suffix of ["variables.json", "styles.css"]) {
|
|
2371
|
+
const sibling = path.replace(/index\.liquid$/, suffix);
|
|
2372
|
+
if ((suffix === "styles.css" ? changedPaths : availablePaths).has(sibling) || deletedPaths.includes(sibling)) dependencies.add(sibling);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return dependencies;
|
|
2376
|
+
}
|
|
2377
|
+
function records(entries) {
|
|
2378
|
+
return (Array.isArray(entries) ? entries : []).filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
|
|
2379
|
+
}
|
|
2380
|
+
//#endregion
|
|
2381
|
+
//#region src/theme/member-configuration.ts
|
|
2382
|
+
const MEMBER_CONFIGURATION_KEY = "members_sites.json";
|
|
2383
|
+
/** Apply member selectors and their document together, before losing old references. */
|
|
2384
|
+
function planThemeUploads(themeRoot, changedFiles, deletedPaths = []) {
|
|
2385
|
+
const localFiles = themeRoot.files();
|
|
2386
|
+
const manifest = localFiles.find((file) => file.relativePath === MEMBER_CONFIGURATION_KEY);
|
|
2387
|
+
if (!manifest || themeRoot.ignore.ignore("members_sites.json")) return individualUploads(changedFiles);
|
|
2388
|
+
const content = manifest.read();
|
|
2389
|
+
const document = JSON.parse(content);
|
|
2390
|
+
if (Array.isArray(document)) return individualUploads(changedFiles);
|
|
2391
|
+
if (!isRecord$3(document)) throw new Error("members_sites.json must contain a JSON object.");
|
|
2392
|
+
const dependencies = memberDependencyPaths(document, new Set(localFiles.map((file) => file.relativePath)), deletedPaths, new Set(changedFiles.map((file) => file.relativePath)));
|
|
2393
|
+
for (const path of deletedPaths) if (dependencies.has(path)) throw new Error(`Member dependency is still declared and cannot be deleted: ${path}`);
|
|
2394
|
+
if (!changedFiles.some((file) => file.relativePath === "members_sites.json" || dependencies.has(file.relativePath))) return changedFiles.map((file) => ({ file }));
|
|
2395
|
+
const availableFiles = new Map(localFiles.map((file) => [file.relativePath, file]));
|
|
2396
|
+
const memberFiles = [...dependencies].map((path) => {
|
|
2397
|
+
const file = availableFiles.get(path);
|
|
2398
|
+
if (!file || !file.isText || themeRoot.ignore.ignore(path)) throw new Error(`Member dependency must be an available text file: ${path}`);
|
|
2399
|
+
return file;
|
|
2400
|
+
});
|
|
2401
|
+
const uploads = changedFiles.filter((file) => file.relativePath !== "members_sites.json" && !dependencies.has(file.relativePath)).map((file) => ({ file }));
|
|
2402
|
+
uploads.push({
|
|
2403
|
+
file: manifest,
|
|
2404
|
+
memberConfiguration: {
|
|
2405
|
+
content,
|
|
2406
|
+
files: memberFiles
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
return uploads;
|
|
2410
|
+
}
|
|
2411
|
+
function individualUploads(files) {
|
|
2412
|
+
return [...files].sort((left, right) => Number(left.relativePath === MEMBER_CONFIGURATION_KEY) - Number(right.relativePath === MEMBER_CONFIGURATION_KEY)).map((file) => ({ file }));
|
|
2413
|
+
}
|
|
2414
|
+
function isRecord$3(entry) {
|
|
2415
|
+
return typeof entry === "object" && entry !== null && !Array.isArray(entry);
|
|
2416
|
+
}
|
|
2417
|
+
/** Removing an older file is not an instruction to erase a member site. */
|
|
2418
|
+
function assertMemberConfigurationRetained(deletedPaths) {
|
|
2419
|
+
if (deletedPaths.includes("members_sites.json")) throw new Error("Keep members_sites.json. To remove member configuration, upload an explicit empty versioned document instead of deleting or renaming the file.");
|
|
2420
|
+
}
|
|
2421
|
+
//#endregion
|
|
1695
2422
|
//#region src/theme/syncer.ts
|
|
1696
2423
|
const ASSET_REFERENCE_CONCURRENCY = 6;
|
|
1697
2424
|
/**
|
|
@@ -1712,6 +2439,7 @@ var PushConflictError = class extends Error {
|
|
|
1712
2439
|
};
|
|
1713
2440
|
var Syncer = class {
|
|
1714
2441
|
checksumIndex = /* @__PURE__ */ new Map();
|
|
2442
|
+
memberAcknowledgments = /* @__PURE__ */ new Map();
|
|
1715
2443
|
rawRemoteResources = /* @__PURE__ */ new Map();
|
|
1716
2444
|
remoteResourceGroups = /* @__PURE__ */ new Map();
|
|
1717
2445
|
remoteResourceIndex = /* @__PURE__ */ new Map();
|
|
@@ -1894,7 +2622,7 @@ var Syncer = class {
|
|
|
1894
2622
|
}
|
|
1895
2623
|
async fetchThemeAssetMetadata(sourceThemeId) {
|
|
1896
2624
|
const body = await getThemeAssets(this.api, sourceThemeId);
|
|
1897
|
-
if (!isRecord$
|
|
2625
|
+
if (!isRecord$2(body) || !Array.isArray(body["file_resources"])) throw new Error("Theme assets response did not include file_resources");
|
|
1898
2626
|
const assets = /* @__PURE__ */ new Map();
|
|
1899
2627
|
for (const value of body["file_resources"]) {
|
|
1900
2628
|
const asset = parseThemeAssetMetadata(value);
|
|
@@ -2063,6 +2791,33 @@ var Syncer = class {
|
|
|
2063
2791
|
if (isThemeAssetKey(file.relativePath)) this.externalizeBinaryFile(file, resource, opts.pendingAsset);
|
|
2064
2792
|
return null;
|
|
2065
2793
|
}
|
|
2794
|
+
/** Canonical member bytes accepted by the server during this transfer. */
|
|
2795
|
+
acknowledgedMemberContents() {
|
|
2796
|
+
return this.memberAcknowledgments;
|
|
2797
|
+
}
|
|
2798
|
+
/** Upload one member document and its changed dependencies as one transaction. */
|
|
2799
|
+
async uploadMemberConfiguration(upload, baseSha) {
|
|
2800
|
+
if (!upload.memberConfiguration) throw new Error("A member configuration upload is required.");
|
|
2801
|
+
const { content, files } = upload.memberConfiguration;
|
|
2802
|
+
const capturedFiles = files.map((file) => ({
|
|
2803
|
+
file,
|
|
2804
|
+
content: file.read()
|
|
2805
|
+
}));
|
|
2806
|
+
const resource = await this.putResource({
|
|
2807
|
+
key: MEMBER_CONFIGURATION_KEY,
|
|
2808
|
+
content,
|
|
2809
|
+
member_files: capturedFiles.map(({ file, content: capturedContent }) => ({
|
|
2810
|
+
key: file.relativePath,
|
|
2811
|
+
content: capturedContent
|
|
2812
|
+
}))
|
|
2813
|
+
}, baseSha, capturedFiles.map(({ file }) => file.relativePath));
|
|
2814
|
+
this.setRemoteResource(resource);
|
|
2815
|
+
if (resource.key && resource.content != null) this.memberAcknowledgments.set(resource.key, typeof resource.content === "string" ? resource.content : JSON.stringify(resource.content));
|
|
2816
|
+
return [{
|
|
2817
|
+
file: upload.file,
|
|
2818
|
+
content
|
|
2819
|
+
}, ...capturedFiles];
|
|
2820
|
+
}
|
|
2066
2821
|
/**
|
|
2067
2822
|
* Wraps the generated `updateThemeResource` client with the two
|
|
2068
2823
|
* merge-aware Phase 003 additions: sending `base_sha` on the request
|
|
@@ -2085,12 +2840,21 @@ var Syncer = class {
|
|
|
2085
2840
|
* "server rejected because of stale base" from generic upload
|
|
2086
2841
|
* failures.
|
|
2087
2842
|
*/
|
|
2088
|
-
async putResource(resource, baseSha) {
|
|
2843
|
+
async putResource(resource, baseSha, expectedMemberKeys) {
|
|
2089
2844
|
const body = { application_theme_resource: resource };
|
|
2090
2845
|
if (baseSha) body["base_sha"] = baseSha;
|
|
2091
2846
|
try {
|
|
2092
2847
|
const response = await updateThemeResource(this.api, this.themeId, body);
|
|
2848
|
+
if (expectedMemberKeys) {
|
|
2849
|
+
const acknowledged = new Map((response.member_resources ?? []).map((entry) => [entry.key, entry]));
|
|
2850
|
+
const manifest = response.application_theme_resource;
|
|
2851
|
+
if (manifest?.key !== "members_sites.json" || manifest.content == null || !manifest.checksum || expectedMemberKeys.some((key) => acknowledged.get(key)?.content == null || !acknowledged.get(key)?.checksum)) throw new Error("Fluid did not acknowledge every bundled member file. Update the server before retrying this push.");
|
|
2852
|
+
}
|
|
2093
2853
|
if (response.content_version_sha) this.lastKnownRemoteSha = response.content_version_sha;
|
|
2854
|
+
for (const memberResource of response.member_resources ?? []) {
|
|
2855
|
+
this.setRemoteResource(memberResource);
|
|
2856
|
+
if (memberResource.key && memberResource.content != null) this.memberAcknowledgments.set(memberResource.key, typeof memberResource.content === "string" ? memberResource.content : JSON.stringify(memberResource.content));
|
|
2857
|
+
}
|
|
2094
2858
|
return response.application_theme_resource ?? {
|
|
2095
2859
|
key: typeof resource["key"] === "string" ? resource["key"] : "",
|
|
2096
2860
|
checksum: null
|
|
@@ -2125,10 +2889,9 @@ var Syncer = class {
|
|
|
2125
2889
|
* close together or splits one push across several commits. The client
|
|
2126
2890
|
* knows; this says so.
|
|
2127
2891
|
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
* push.
|
|
2892
|
+
* Returns false rather than throwing when the request fails. Callers must
|
|
2893
|
+
* retain their previous baseline until finalization succeeds; the server
|
|
2894
|
+
* can reject an incomplete member graph even after individual files land.
|
|
2132
2895
|
*
|
|
2133
2896
|
* Returns whether Fluid took the request, because a caller tracking edit
|
|
2134
2897
|
* boundaries needs to know. A failed ask leaves the previous edit
|
|
@@ -2141,7 +2904,7 @@ var Syncer = class {
|
|
|
2141
2904
|
if (response.content_version_sha) this.lastKnownRemoteSha = response.content_version_sha;
|
|
2142
2905
|
return true;
|
|
2143
2906
|
} catch (e) {
|
|
2144
|
-
console.warn(` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}).
|
|
2907
|
+
console.warn(` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}). Retry the push to finalize it.`);
|
|
2145
2908
|
return false;
|
|
2146
2909
|
}
|
|
2147
2910
|
}
|
|
@@ -2247,6 +3010,7 @@ var Syncer = class {
|
|
|
2247
3010
|
}[category] ?? "files"}/${assetCode}`;
|
|
2248
3011
|
}
|
|
2249
3012
|
async deleteRemoteFile(relativePath, baseSha) {
|
|
3013
|
+
assertMemberConfigurationRetained([relativePath]);
|
|
2250
3014
|
this.assetManifest.reload();
|
|
2251
3015
|
if (this.assetManifest.has(relativePath)) return;
|
|
2252
3016
|
const body = { application_theme_resource: { key: relativePath } };
|
|
@@ -2430,6 +3194,10 @@ var Syncer = class {
|
|
|
2430
3194
|
else await this.fetchChecksums();
|
|
2431
3195
|
if (!opts.skipPreflight) await this.preflightPush(opts.baseSha);
|
|
2432
3196
|
let baseSha = opts.baseSha ?? null;
|
|
3197
|
+
const localPaths = new Set(localFiles.map((file) => file.relativePath));
|
|
3198
|
+
if (opts.delete) for (const key of this.assetManifest.keys()) localPaths.add(key);
|
|
3199
|
+
const toDelete = opts.delete ? this.remoteKeys().filter((key) => key !== "members_sites.json" && this.canDeleteRemoteResource(key) && !localPaths.has(key) && !this.themeRoot.ignore.ignore(key)) : [];
|
|
3200
|
+
const plannedUploads = planThemeUploads(this.themeRoot, localFiles.filter((f) => f.exists && this.hasChanged(f)), toDelete);
|
|
2433
3201
|
if (opts.linkManagedAssets) {
|
|
2434
3202
|
result.linked = await this.linkManagedAssets(opts.linkManagedAssets);
|
|
2435
3203
|
baseSha = this.lastKnownRemoteSha ?? baseSha;
|
|
@@ -2437,31 +3205,34 @@ var Syncer = class {
|
|
|
2437
3205
|
this.assetManifest.reload();
|
|
2438
3206
|
this.ensureManagedAssetsAreResolved();
|
|
2439
3207
|
}
|
|
2440
|
-
const toUpload =
|
|
3208
|
+
const toUpload = plannedUploads.filter(({ file, memberConfiguration }) => memberConfiguration || file.exists && this.hasChanged(file));
|
|
3209
|
+
const uploadCount = toUpload.reduce((count, upload) => count + 1 + (upload.memberConfiguration?.files.length ?? 0), 0);
|
|
2441
3210
|
let done = 0;
|
|
2442
|
-
for (const
|
|
3211
|
+
for (const upload of toUpload) {
|
|
3212
|
+
const { file } = upload;
|
|
3213
|
+
if (file.relativePath === "members_sites.json" && result.errors.length > 0) {
|
|
3214
|
+
result.errors.push("Skipped members_sites.json because a dependency upload failed.");
|
|
3215
|
+
break;
|
|
3216
|
+
}
|
|
2443
3217
|
try {
|
|
2444
|
-
await this.
|
|
3218
|
+
if (upload.memberConfiguration) await this.uploadMemberConfiguration(upload, baseSha);
|
|
3219
|
+
else await this.uploadFile(file, baseSha, { pendingAsset: opts.pendingBinaryAssets });
|
|
2445
3220
|
baseSha = this.lastKnownRemoteSha;
|
|
2446
|
-
result.uploaded
|
|
3221
|
+
result.uploaded += 1 + (upload.memberConfiguration?.files.length ?? 0);
|
|
2447
3222
|
} catch (e) {
|
|
2448
3223
|
if (e instanceof PushConflictError) throw e;
|
|
2449
3224
|
result.errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);
|
|
2450
3225
|
}
|
|
2451
|
-
|
|
3226
|
+
done += 1 + (upload.memberConfiguration?.files.length ?? 0);
|
|
3227
|
+
opts.onProgress?.(done, uploadCount);
|
|
2452
3228
|
}
|
|
2453
|
-
if (opts.delete) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
result.deleted++;
|
|
2461
|
-
} catch (e) {
|
|
2462
|
-
if (e instanceof PushConflictError) throw e;
|
|
2463
|
-
result.errors.push(`Delete ${key}: ${formatError(e)}`);
|
|
2464
|
-
}
|
|
3229
|
+
if (opts.delete && result.errors.length === 0) for (const key of toDelete) try {
|
|
3230
|
+
await this.deleteRemoteFile(key, baseSha);
|
|
3231
|
+
baseSha = this.lastKnownRemoteSha;
|
|
3232
|
+
result.deleted++;
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
if (e instanceof PushConflictError) throw e;
|
|
3235
|
+
result.errors.push(`Delete ${key}: ${formatError(e)}`);
|
|
2465
3236
|
}
|
|
2466
3237
|
return result;
|
|
2467
3238
|
}
|
|
@@ -2586,7 +3357,7 @@ function uploadedAssetMetadata(file, resource) {
|
|
|
2586
3357
|
};
|
|
2587
3358
|
}
|
|
2588
3359
|
function parseThemeAssetMetadata(value) {
|
|
2589
|
-
if (!isRecord$
|
|
3360
|
+
if (!isRecord$2(value)) return void 0;
|
|
2590
3361
|
const filename = nonEmptyString(value["filename"]);
|
|
2591
3362
|
const url = nonEmptyString(value["url"]);
|
|
2592
3363
|
const contentType = nonEmptyString(value["content_type"]);
|
|
@@ -2608,7 +3379,7 @@ function parseThemeAssetMetadata(value) {
|
|
|
2608
3379
|
};
|
|
2609
3380
|
}
|
|
2610
3381
|
function createdFileResourceId(value) {
|
|
2611
|
-
if (!isRecord$
|
|
3382
|
+
if (!isRecord$2(value) || !isRecord$2(value["file_resource"])) return;
|
|
2612
3383
|
return positiveInteger(value["file_resource"]["id"]);
|
|
2613
3384
|
}
|
|
2614
3385
|
function positiveInteger(value) {
|
|
@@ -2623,7 +3394,7 @@ function nonEmptyString(value) {
|
|
|
2623
3394
|
function optionalString(value) {
|
|
2624
3395
|
return typeof value === "string" ? value : void 0;
|
|
2625
3396
|
}
|
|
2626
|
-
function isRecord$
|
|
3397
|
+
function isRecord$2(value) {
|
|
2627
3398
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2628
3399
|
}
|
|
2629
3400
|
//#endregion
|
|
@@ -2809,6 +3580,7 @@ function timestamp() {
|
|
|
2809
3580
|
}
|
|
2810
3581
|
async function startDevServer(api, theme, themeRoot, opts, onReady) {
|
|
2811
3582
|
const sse = new SSEStream();
|
|
3583
|
+
const memberPreview = new MemberPreview(api, theme, opts.reloadMode);
|
|
2812
3584
|
const syncer = new Syncer(api, theme.id, themeRoot);
|
|
2813
3585
|
let remoteStateSafe = true;
|
|
2814
3586
|
const invalidateRemoteState = () => {
|
|
@@ -2919,30 +3691,40 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
|
|
|
2919
3691
|
await syncInFlight;
|
|
2920
3692
|
}
|
|
2921
3693
|
if (remoteWritesBlocked) return;
|
|
3694
|
+
let uploads;
|
|
2922
3695
|
try {
|
|
2923
3696
|
assertNoCaseCollisions(themeRoot.files().map((file) => file.relativePath));
|
|
3697
|
+
assertMemberConfigurationRetained(removed.filter((file) => !themeRoot.ignore.ignore(file.relativePath)).map((file) => file.relativePath));
|
|
3698
|
+
uploads = planThemeUploads(themeRoot, [...modified, ...added], removed.filter((file) => !themeRoot.ignore.ignore(file.relativePath)).map((file) => file.relativePath));
|
|
2924
3699
|
} catch (error) {
|
|
2925
3700
|
console.error(`\n[Watcher] Sync blocked: ${String(error)}`);
|
|
2926
3701
|
return;
|
|
2927
3702
|
}
|
|
2928
|
-
const changed = [...modified, ...added];
|
|
2929
3703
|
let wroteRemote = false;
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3704
|
+
const memberTransfer = uploads.some((upload) => upload.memberConfiguration || upload.file.relativePath === "members_sites.json");
|
|
3705
|
+
for (const upload of uploads) {
|
|
3706
|
+
const { file } = upload;
|
|
3707
|
+
const uploadFiles = [file, ...upload.memberConfiguration?.files ?? []];
|
|
3708
|
+
if (opts.validate) for (const candidate of uploadFiles) {
|
|
3709
|
+
if (!candidate.isLiquid) continue;
|
|
3710
|
+
for (const diagnostic of candidate.validateSchema()) {
|
|
3711
|
+
const prefix = diagnostic.severity === "error" ? "Schema error" : "Schema warning";
|
|
3712
|
+
console.warn(`\n[${prefix}] ${candidate.relativePath}: ${diagnostic.message}`);
|
|
2936
3713
|
}
|
|
2937
3714
|
}
|
|
2938
|
-
pendingUpdates.add(
|
|
3715
|
+
for (const pendingFile of uploadFiles) pendingUpdates.add(pendingFile.relativePath);
|
|
2939
3716
|
try {
|
|
2940
|
-
const
|
|
3717
|
+
const uploadedFiles = upload.memberConfiguration ? await syncer.uploadMemberConfiguration(upload, syncer.remoteSha()) : [{
|
|
3718
|
+
file,
|
|
3719
|
+
content: await syncer.uploadFile(file, syncer.remoteSha(), { pendingAsset: true })
|
|
3720
|
+
}];
|
|
2941
3721
|
wroteRemote = true;
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3722
|
+
for (const { file: uploadedFile, content: uploadedContent } of uploadedFiles) {
|
|
3723
|
+
console.log(` ✓ synced ${uploadedFile.relativePath} (${timestamp()})`);
|
|
3724
|
+
if (isStylesheetKey(uploadedFile.relativePath)) console.warn(` ⚠ ${uploadedFile.relativePath}: the storefront inlines this CSS from a published asset, so it serves the previous bytes until the stylesheet asset sync finishes`);
|
|
3725
|
+
if (uploadedFile.isLiquid && uploadedContent !== null && hasUnbalancedLiquidDelimiters(uploadedContent)) console.warn(` ⚠ ${uploadedFile.relativePath}: unbalanced liquid delimiters — the storefront may silently serve stale content for this section`);
|
|
3726
|
+
if (uploadedFile.isLiquid && uploadedContent !== null) for (const diagnostic of findLiquidBlockTagDiagnostics(uploadedContent)) console.warn(` ⚠ ${uploadedFile.relativePath}: ${diagnostic.message}`);
|
|
3727
|
+
}
|
|
2946
3728
|
} catch (e) {
|
|
2947
3729
|
if (e instanceof PushConflictError) {
|
|
2948
3730
|
blockRemoteWrites(e);
|
|
@@ -2950,8 +3732,11 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
|
|
|
2950
3732
|
}
|
|
2951
3733
|
invalidateRemoteState();
|
|
2952
3734
|
console.error(`\n[Watcher] Upload failed: ${file.relativePath}: ${e}`);
|
|
3735
|
+
remoteWritesBlocked = wroteRemote || memberTransfer;
|
|
3736
|
+
if (remoteWritesBlocked) console.error("[Watcher] Restart theme dev to retry the incomplete transfer.");
|
|
3737
|
+
return;
|
|
2953
3738
|
} finally {
|
|
2954
|
-
pendingUpdates.delete(
|
|
3739
|
+
for (const pendingFile of uploadFiles) pendingUpdates.delete(pendingFile.relativePath);
|
|
2955
3740
|
}
|
|
2956
3741
|
}
|
|
2957
3742
|
if (remoteWritesBlocked) return;
|
|
@@ -2967,15 +3752,32 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
|
|
|
2967
3752
|
break;
|
|
2968
3753
|
}
|
|
2969
3754
|
invalidateRemoteState();
|
|
3755
|
+
remoteWritesBlocked = wroteRemote || memberTransfer;
|
|
3756
|
+
console.error(`[Watcher] Delete failed: ${file.relativePath}: ${error}`);
|
|
3757
|
+
if (remoteWritesBlocked) console.error("[Watcher] Restart theme dev to retry the incomplete transfer.");
|
|
3758
|
+
return;
|
|
2970
3759
|
}
|
|
2971
3760
|
}
|
|
2972
3761
|
if (remoteWritesBlocked) return;
|
|
2973
|
-
if (wroteRemote)
|
|
3762
|
+
if (wroteRemote) {
|
|
3763
|
+
recordRemoteState();
|
|
3764
|
+
try {
|
|
3765
|
+
await memberPreview.refreshAfterSync();
|
|
3766
|
+
} catch {
|
|
3767
|
+
console.error("[Member preview] Could not refresh routes after sync. The next member request will retry.");
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
2974
3770
|
if (removed.length > 0) sse.broadcast(JSON.stringify({ reload_page: true }));
|
|
2975
|
-
else if (
|
|
3771
|
+
else if (uploads.length > 0) sse.broadcast(JSON.stringify({ modified: uploads.flatMap(({ file, memberConfiguration }) => [file, ...memberConfiguration?.files ?? []].map((updatedFile) => updatedFile.relativePath)) }));
|
|
2976
3772
|
scheduleSync();
|
|
2977
3773
|
});
|
|
2978
3774
|
const server = http.createServer(async (req, res) => {
|
|
3775
|
+
if (!isLocalPreviewRequest(req, opts.host)) {
|
|
3776
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
3777
|
+
res.end("Local preview requests only.");
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
if (await memberPreview.handle(req, res)) return;
|
|
2979
3781
|
if (req.url === "/hot-reload") {
|
|
2980
3782
|
sse.add(res);
|
|
2981
3783
|
return;
|
|
@@ -3092,7 +3894,7 @@ async function devRemoteStateFromSourceShadow(themeRoot, shadow, themeId, remote
|
|
|
3092
3894
|
}
|
|
3093
3895
|
}
|
|
3094
3896
|
function parseDocument(document) {
|
|
3095
|
-
if (!isRecord(document) || document["version"] !== BASELINE_VERSION) throw new Error(`expected version ${BASELINE_VERSION}`);
|
|
3897
|
+
if (!isRecord$1(document) || document["version"] !== BASELINE_VERSION) throw new Error(`expected version ${BASELINE_VERSION}`);
|
|
3096
3898
|
const themeId = document["themeId"];
|
|
3097
3899
|
const remoteSha = document["remoteSha"];
|
|
3098
3900
|
const assetManifestSha = document["assetManifestSha"];
|
|
@@ -3111,7 +3913,7 @@ function parseDocument(document) {
|
|
|
3111
3913
|
};
|
|
3112
3914
|
}
|
|
3113
3915
|
function parseResource(resource) {
|
|
3114
|
-
if (!isRecord(resource)) throw new Error("invalid resource");
|
|
3916
|
+
if (!isRecord$1(resource)) throw new Error("invalid resource");
|
|
3115
3917
|
const key = resource["key"];
|
|
3116
3918
|
const checksum = resource["checksum"];
|
|
3117
3919
|
const contentPresent = resource["contentPresent"];
|
|
@@ -3133,7 +3935,7 @@ function parseResource(resource) {
|
|
|
3133
3935
|
url
|
|
3134
3936
|
};
|
|
3135
3937
|
}
|
|
3136
|
-
function isRecord(candidate) {
|
|
3938
|
+
function isRecord$1(candidate) {
|
|
3137
3939
|
return typeof candidate === "object" && candidate !== null;
|
|
3138
3940
|
}
|
|
3139
3941
|
function isNonEmptyString(candidate) {
|
|
@@ -3817,7 +4619,9 @@ function createDevCommand() {
|
|
|
3817
4619
|
console.log(`\n Dev server: ${address}`);
|
|
3818
4620
|
console.log(` Web editor: ${editorUrl}`);
|
|
3819
4621
|
console.log("\n Watching for file changes…\n");
|
|
3820
|
-
if (opts.navigate)
|
|
4622
|
+
if (opts.navigate) navigateTheme(api, theme.id, address).catch((error) => {
|
|
4623
|
+
console.error(error instanceof Error ? error.message : "Could not open the preview navigator.");
|
|
4624
|
+
});
|
|
3821
4625
|
});
|
|
3822
4626
|
await new Promise(() => {});
|
|
3823
4627
|
});
|
|
@@ -3911,17 +4715,25 @@ function containsConflictMarker(buf) {
|
|
|
3911
4715
|
return buf.includes(CONFLICT_START) && buf.includes(CONFLICT_MID) && buf.includes(CONFLICT_END);
|
|
3912
4716
|
}
|
|
3913
4717
|
/**
|
|
3914
|
-
* Commit the
|
|
3915
|
-
*
|
|
3916
|
-
*
|
|
4718
|
+
* Commit the pushed state to shadow HEAD. Server-acknowledged contents
|
|
4719
|
+
* override local bytes for normalized member files, including files edited
|
|
4720
|
+
* or deleted locally during upload. Local files remain untouched so those
|
|
4721
|
+
* later changes still differ from the next pull's merge base.
|
|
3917
4722
|
*/
|
|
3918
|
-
async function commitPushedState(themeRoot, shadow, message) {
|
|
4723
|
+
async function commitPushedState(themeRoot, shadow, message, acknowledgedContents) {
|
|
3919
4724
|
const entries = [];
|
|
3920
4725
|
const assetManifest = new ThemeAssetManifest(themeRoot.root);
|
|
3921
|
-
const
|
|
4726
|
+
const recordedKeys = /* @__PURE__ */ new Set();
|
|
4727
|
+
for (const [path, content] of new Map(acknowledgedContents)) {
|
|
4728
|
+
entries.push({
|
|
4729
|
+
path,
|
|
4730
|
+
sha: await shadow.writeBlob(content)
|
|
4731
|
+
});
|
|
4732
|
+
recordedKeys.add(path);
|
|
4733
|
+
}
|
|
3922
4734
|
for (const file of themeRoot.files()) {
|
|
3923
|
-
if (!file.exists) continue;
|
|
3924
|
-
|
|
4735
|
+
if (!file.exists || recordedKeys.has(file.relativePath)) continue;
|
|
4736
|
+
recordedKeys.add(file.relativePath);
|
|
3925
4737
|
const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();
|
|
3926
4738
|
entries.push({
|
|
3927
4739
|
path: file.relativePath,
|
|
@@ -3930,7 +4742,7 @@ async function commitPushedState(themeRoot, shadow, message) {
|
|
|
3930
4742
|
}
|
|
3931
4743
|
let managedAssetSentinelSha;
|
|
3932
4744
|
for (const key of assetManifest.keys()) {
|
|
3933
|
-
if (
|
|
4745
|
+
if (recordedKeys.has(key)) continue;
|
|
3934
4746
|
managedAssetSentinelSha ??= await shadow.writeBlob(MANAGED_ASSET_SHADOW_SENTINEL);
|
|
3935
4747
|
entries.push({
|
|
3936
4748
|
path: key,
|
|
@@ -4220,6 +5032,7 @@ function createPushCommand() {
|
|
|
4220
5032
|
}
|
|
4221
5033
|
}
|
|
4222
5034
|
const { changed, deleted } = await diffAgainstShadow(themeRoot, shadow);
|
|
5035
|
+
if (!opts.nodelete) assertMemberConfigurationRetained(deleted);
|
|
4223
5036
|
const managedAssetCount = new ThemeAssetManifest(themeRoot.root).keys().length;
|
|
4224
5037
|
if (changed.length === 0 && deleted.length === 0 && managedAssetCount === 0) {
|
|
4225
5038
|
console.log("Nothing to push — local matches the last synced state.");
|
|
@@ -4250,6 +5063,7 @@ function createPushCommand() {
|
|
|
4250
5063
|
}
|
|
4251
5064
|
throw e;
|
|
4252
5065
|
}
|
|
5066
|
+
const uploads = planThemeUploads(themeRoot, changed, opts.nodelete ? [] : deleted);
|
|
4253
5067
|
let linked = 0;
|
|
4254
5068
|
try {
|
|
4255
5069
|
linked = await syncer.linkManagedAssets({ replace: true });
|
|
@@ -4273,12 +5087,18 @@ function createPushCommand() {
|
|
|
4273
5087
|
let deletedCount = 0;
|
|
4274
5088
|
const errors = [];
|
|
4275
5089
|
let progress = 0;
|
|
4276
|
-
const total =
|
|
4277
|
-
for (const
|
|
5090
|
+
const total = uploads.reduce((count, upload) => count + 1 + (upload.memberConfiguration?.files.length ?? 0), 0) + (opts.nodelete ? 0 : deleted.length);
|
|
5091
|
+
for (const upload of uploads) {
|
|
5092
|
+
const { file } = upload;
|
|
5093
|
+
if (file.relativePath === "members_sites.json" && errors.length > 0) {
|
|
5094
|
+
errors.push("Skipped members_sites.json because a dependency upload failed.");
|
|
5095
|
+
break;
|
|
5096
|
+
}
|
|
4278
5097
|
try {
|
|
4279
|
-
await syncer.
|
|
5098
|
+
if (upload.memberConfiguration) await syncer.uploadMemberConfiguration(upload, baseSha);
|
|
5099
|
+
else await syncer.uploadFile(file, baseSha);
|
|
4280
5100
|
baseSha = syncer.remoteSha() ?? baseSha;
|
|
4281
|
-
uploaded
|
|
5101
|
+
uploaded += 1 + (upload.memberConfiguration?.files.length ?? 0);
|
|
4282
5102
|
} catch (e) {
|
|
4283
5103
|
if (e instanceof PushConflictError) {
|
|
4284
5104
|
spinner.stop();
|
|
@@ -4287,9 +5107,10 @@ function createPushCommand() {
|
|
|
4287
5107
|
}
|
|
4288
5108
|
errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);
|
|
4289
5109
|
}
|
|
4290
|
-
|
|
5110
|
+
progress += 1 + (upload.memberConfiguration?.files.length ?? 0);
|
|
5111
|
+
spinner.text = `Pushing ${progress}/${total} files…`;
|
|
4291
5112
|
}
|
|
4292
|
-
if (!opts.nodelete) for (const key of deleted) {
|
|
5113
|
+
if (!opts.nodelete && errors.length === 0) for (const key of deleted) {
|
|
4293
5114
|
try {
|
|
4294
5115
|
await syncer.deleteRemoteFile(key, baseSha);
|
|
4295
5116
|
baseSha = syncer.remoteSha() ?? baseSha;
|
|
@@ -4309,7 +5130,9 @@ function createPushCommand() {
|
|
|
4309
5130
|
} catch (error) {
|
|
4310
5131
|
errors.push(`Save asset provenance: ${formatError(error)}`);
|
|
4311
5132
|
}
|
|
4312
|
-
if (uploaded > 0 || deletedCount > 0 || linked > 0)
|
|
5133
|
+
if (errors.length === 0 && (uploaded > 0 || deletedCount > 0 || linked > 0)) {
|
|
5134
|
+
if (!await syncer.requestSync()) errors.push("Fluid did not accept sync finalization. Retry the push before publishing.");
|
|
5135
|
+
}
|
|
4313
5136
|
if (errors.length) {
|
|
4314
5137
|
spinner.warn(`Pushed with ${errors.length} error(s).`);
|
|
4315
5138
|
for (const err of errors) console.error(` ${err}`);
|
|
@@ -4323,7 +5146,14 @@ function createPushCommand() {
|
|
|
4323
5146
|
status: "deleted",
|
|
4324
5147
|
path: key
|
|
4325
5148
|
}))];
|
|
4326
|
-
|
|
5149
|
+
try {
|
|
5150
|
+
await commitPushedState(themeRoot, shadow, syncCommitSubject("Push", await getActor(), /* @__PURE__ */ new Date(), pushedChanges), syncer.acknowledgedMemberContents());
|
|
5151
|
+
} catch (error) {
|
|
5152
|
+
console.error(`Theme content was uploaded to the server, but the local restore point and sync baseline could not be saved: ${error instanceof Error ? error.message : String(error)}. Do not repeat the push with --force. Review the remote theme and run a normal pull to recover the baseline.`);
|
|
5153
|
+
if (opts.publish) console.error("Publishing was not attempted.");
|
|
5154
|
+
process.exitCode = 1;
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
4327
5157
|
}
|
|
4328
5158
|
await persistConfig();
|
|
4329
5159
|
/**
|
|
@@ -4358,7 +5188,7 @@ function createPushCommand() {
|
|
|
4358
5188
|
});
|
|
4359
5189
|
} catch {}
|
|
4360
5190
|
}
|
|
4361
|
-
if (opts.publish) {
|
|
5191
|
+
if (opts.publish && errors.length === 0) {
|
|
4362
5192
|
const pubSpinner = ora("Publishing theme…").start();
|
|
4363
5193
|
try {
|
|
4364
5194
|
await publishApplicationTheme(api, theme.id);
|
|
@@ -4410,6 +5240,50 @@ async function canPullThemeInBackground(root, themeId) {
|
|
|
4410
5240
|
}
|
|
4411
5241
|
}
|
|
4412
5242
|
//#endregion
|
|
5243
|
+
//#region src/theme/member-transfer.ts
|
|
5244
|
+
/** Missing/ignored is not empty. Return paths to retain without acknowledging
|
|
5245
|
+
* their local bytes as remotely accepted. Used before pull writes or pruning. */
|
|
5246
|
+
function planOmittedMemberFiles(input) {
|
|
5247
|
+
if (input.incomingPaths.has("members_sites.json") && !input.ignored("members_sites.json")) return /* @__PURE__ */ new Set();
|
|
5248
|
+
const protectedPaths = /* @__PURE__ */ new Set();
|
|
5249
|
+
for (const content of input.previousDocuments) {
|
|
5250
|
+
if (content === null) continue;
|
|
5251
|
+
protectedPaths.add(MEMBER_CONFIGURATION_KEY);
|
|
5252
|
+
if (content.byteLength > 2 * 1024 * 1024) throw new Error("Cannot preserve member files: members_sites.json exceeds 2 MB.");
|
|
5253
|
+
const document = JSON.parse(content.toString("utf8"));
|
|
5254
|
+
if (!isRecord(document) || document.version !== 2) throw new Error("Cannot preserve member files: members_sites.json must be a version 2 document.");
|
|
5255
|
+
for (const field of [
|
|
5256
|
+
"sites",
|
|
5257
|
+
"menus",
|
|
5258
|
+
"menu_references"
|
|
5259
|
+
]) if (!Array.isArray(document[field])) throw new Error(`Cannot preserve member files: members_sites.json ${field} must be an array.`);
|
|
5260
|
+
assertDependencyShape(document);
|
|
5261
|
+
if (document.sites.length > 0) {
|
|
5262
|
+
for (const path of input.availablePaths) if (/^(sections|blocks|components|snippets|navbar|footer)\//.test(path)) protectedPaths.add(path);
|
|
5263
|
+
}
|
|
5264
|
+
for (const path of memberDependencyPaths(document, input.availablePaths)) {
|
|
5265
|
+
if (!isThemeResourceKey(path) || path.includes("\\") || [...path].some((character) => character.charCodeAt(0) < 32) || path === "config/members_sites.json") throw new Error(`Cannot preserve member files: invalid dependency path ${path}.`);
|
|
5266
|
+
protectedPaths.add(path);
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
return new Set([...protectedPaths].filter((path) => !input.incomingPaths.has(path) || input.ignored(path)));
|
|
5270
|
+
}
|
|
5271
|
+
function isRecord(value) {
|
|
5272
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5273
|
+
}
|
|
5274
|
+
/** Reject incomplete dependency declarations rather than silently pruning files. */
|
|
5275
|
+
function assertDependencyShape(document) {
|
|
5276
|
+
const entries = (value) => {
|
|
5277
|
+
if (!Array.isArray(value) || !value.every(isRecord)) throw new Error("Cannot preserve member files: malformed dependency declaration.");
|
|
5278
|
+
return value;
|
|
5279
|
+
};
|
|
5280
|
+
for (const entry of [...entries(document.templates ?? []), ...entries(document.menu_references)]) if (typeof entry.path !== "string" || !entry.path) throw new Error("Cannot preserve member files: dependency path is missing.");
|
|
5281
|
+
for (const site of entries(document.sites)) {
|
|
5282
|
+
if (site.layout_name !== null && typeof site.layout_name !== "string") throw new Error("Cannot preserve member files: invalid layout name.");
|
|
5283
|
+
for (const page of entries(site.pages)) if (page.template_path !== null && typeof page.template_path !== "string") throw new Error("Cannot preserve member files: invalid page template path.");
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
//#endregion
|
|
4413
5287
|
//#region src/theme/merge-pull.ts
|
|
4414
5288
|
/** `write(..., RESOLVES_CONFLICT)`: this write is the chosen side of a conflict. */
|
|
4415
5289
|
const RESOLVES_CONFLICT = true;
|
|
@@ -4499,6 +5373,21 @@ async function mergePull(input) {
|
|
|
4499
5373
|
onProgress?.(++done, remote.length);
|
|
4500
5374
|
}
|
|
4501
5375
|
input.backgroundGuard?.assertUnchanged();
|
|
5376
|
+
let retainedMemberPaths;
|
|
5377
|
+
try {
|
|
5378
|
+
if (remoteKeys.has("members_sites.json") && !themeRoot.ignore.ignore("members_sites.json") && !remoteContent.has("members_sites.json")) throw new Error("members_sites.json was listed but could not be downloaded; previous member state retained.");
|
|
5379
|
+
retainedMemberPaths = planOmittedMemberFiles({
|
|
5380
|
+
previousDocuments: [readIfExists(themeRoot.file(MEMBER_CONFIGURATION_KEY).absolutePath), await shadow.blobAtHead(MEMBER_CONFIGURATION_KEY)],
|
|
5381
|
+
availablePaths: new Set([...themeRoot.files().map((file) => file.relativePath), ...await shadow.headPaths()]),
|
|
5382
|
+
incomingPaths: remoteKeys,
|
|
5383
|
+
ignored: (path) => themeRoot.ignore.ignore(path)
|
|
5384
|
+
});
|
|
5385
|
+
} catch (error) {
|
|
5386
|
+
result.errors.push(errMsg(error));
|
|
5387
|
+
result.baselinePreserved = true;
|
|
5388
|
+
return result;
|
|
5389
|
+
}
|
|
5390
|
+
for (const path of retainedMemberPaths) remoteContent.delete(path);
|
|
4502
5391
|
for (const [key, remoteBuf] of remoteContent) {
|
|
4503
5392
|
const file = themeRoot.file(key);
|
|
4504
5393
|
if (!file.absolutePath.startsWith(themeRoot.root + sep)) {
|
|
@@ -4559,7 +5448,7 @@ async function mergePull(input) {
|
|
|
4559
5448
|
for (const { key, file, content, record } of pendingWrites) if (tryWrite(key, file, content)) record();
|
|
4560
5449
|
}
|
|
4561
5450
|
if (doDelete && await shadow.hasHead()) for (const file of themeRoot.files()) {
|
|
4562
|
-
if (remoteKeys.has(file.relativePath)) continue;
|
|
5451
|
+
if (remoteKeys.has(file.relativePath) || retainedMemberPaths.has(file.relativePath)) continue;
|
|
4563
5452
|
if (isStylesheetKey(file.relativePath)) continue;
|
|
4564
5453
|
const baseBuf = await shadow.blobAtHead(file.relativePath);
|
|
4565
5454
|
if (!baseBuf) continue;
|
|
@@ -4574,37 +5463,43 @@ async function mergePull(input) {
|
|
|
4574
5463
|
if (error instanceof BackgroundPullChangedError) throw error;
|
|
4575
5464
|
}
|
|
4576
5465
|
}
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
5466
|
+
try {
|
|
5467
|
+
const commitEntries = [];
|
|
5468
|
+
for (const [key, buf] of remoteContent) {
|
|
5469
|
+
if (unwrittenKeys.has(key)) continue;
|
|
5470
|
+
commitEntries.push({
|
|
5471
|
+
path: key,
|
|
5472
|
+
sha: await shadow.writeBlob(buf)
|
|
5473
|
+
});
|
|
5474
|
+
}
|
|
5475
|
+
for (const key of new Set([...remoteKeys, ...retainedMemberPaths])) {
|
|
5476
|
+
if (input.skipRemoteKeys?.has(key) && !retainedMemberPaths.has(key)) continue;
|
|
5477
|
+
if (remoteContent.has(key) && !unwrittenKeys.has(key)) continue;
|
|
5478
|
+
const prevBlob = await shadow.blobAtHead(key);
|
|
5479
|
+
if (prevBlob == null) continue;
|
|
5480
|
+
commitEntries.push({
|
|
5481
|
+
path: key,
|
|
5482
|
+
sha: await shadow.writeBlob(prevBlob)
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
const assetManifest = new ThemeAssetManifest(themeRoot.root);
|
|
5486
|
+
let managedAssetSentinelSha;
|
|
5487
|
+
for (const key of input.skipRemoteKeys ?? []) {
|
|
5488
|
+
if (!remoteKeys.has(key) || !assetManifest.has(key)) continue;
|
|
5489
|
+
managedAssetSentinelSha ??= await shadow.writeBlob(MANAGED_ASSET_SHADOW_SENTINEL);
|
|
5490
|
+
commitEntries.push({
|
|
5491
|
+
path: key,
|
|
5492
|
+
sha: managedAssetSentinelSha
|
|
5493
|
+
});
|
|
5494
|
+
}
|
|
5495
|
+
if (commitEntries.length > 0 || remoteKeys.size === 0 && await shadow.hasHead()) {
|
|
5496
|
+
input.backgroundGuard?.assertUnchanged();
|
|
5497
|
+
await shadow.commitState(commitEntries, syncCommitSubject("Pull", input.actor ?? null));
|
|
5498
|
+
}
|
|
5499
|
+
} catch (error) {
|
|
5500
|
+
if (error instanceof BackgroundPullChangedError) throw error;
|
|
5501
|
+
result.baselinePreserved = true;
|
|
5502
|
+
result.errors.push(`Downloaded changes have already been applied locally, but the local restore point and sync baseline could not be saved: ${error instanceof Error ? error.message : String(error)}. Review the files and retry a normal pull before pushing; do not use --force to bypass the missing baseline.`);
|
|
4608
5503
|
}
|
|
4609
5504
|
return result;
|
|
4610
5505
|
}
|
|
@@ -4726,7 +5621,8 @@ function createPullCommand() {
|
|
|
4726
5621
|
spinner.warn(`Pulled with ${result.errors.length} error(s): ${parts.join(", ")}.`);
|
|
4727
5622
|
for (const e of result.errors) console.error(` ${e}`);
|
|
4728
5623
|
process.exitCode = 1;
|
|
4729
|
-
}
|
|
5624
|
+
}
|
|
5625
|
+
if (result.conflicts.length > 0) {
|
|
4730
5626
|
spinner.warn(`${result.conflicts.length} conflict(s) — resolve markers before pushing: ${parts.join(", ")}.`);
|
|
4731
5627
|
console.log();
|
|
4732
5628
|
for (const c of result.conflicts) console.log(` ${chalk.yellow("CONFLICT")} ${c}`);
|
|
@@ -4734,7 +5630,8 @@ function createPullCommand() {
|
|
|
4734
5630
|
console.log(` Edit each file above to reconcile the ${chalk.cyan("<<<<<<<")} / ${chalk.cyan(">>>>>>>")} markers,`);
|
|
4735
5631
|
console.log(` then run ${chalk.cyan("fluid theme push")} once your resolution is in place.`);
|
|
4736
5632
|
console.log();
|
|
4737
|
-
} else spinner.succeed(parts.join(", ") || "Already up to date.");
|
|
5633
|
+
} else if (result.errors.length === 0) spinner.succeed(parts.join(", ") || "Already up to date.");
|
|
5634
|
+
if (result.baselinePreserved) return;
|
|
4738
5635
|
const remoteSha = syncer.remoteSha();
|
|
4739
5636
|
backgroundGuard?.assertUnchanged();
|
|
4740
5637
|
const updateConfig = () => writeThemeConfig(absoluteRoot, {
|
|
@@ -4881,196 +5778,6 @@ function createInitCommand() {
|
|
|
4881
5778
|
});
|
|
4882
5779
|
}
|
|
4883
5780
|
//#endregion
|
|
4884
|
-
//#region src/commands/navigate.ts
|
|
4885
|
-
function localSuggest(input, choices) {
|
|
4886
|
-
if (!input) return choices;
|
|
4887
|
-
const lower = input.toLowerCase();
|
|
4888
|
-
return choices.filter((c) => c.title.toLowerCase().includes(lower));
|
|
4889
|
-
}
|
|
4890
|
-
const THEMEABLE_TYPE_MAP = {
|
|
4891
|
-
"/home": "home_page",
|
|
4892
|
-
"/home/shop": "shop_page",
|
|
4893
|
-
"/home/join": "join_page",
|
|
4894
|
-
"/cart": "cart_page",
|
|
4895
|
-
"/home/blog": "post_page",
|
|
4896
|
-
"/home/categories": "category_page",
|
|
4897
|
-
"/home/collections": "collection_page"
|
|
4898
|
-
};
|
|
4899
|
-
const STATIC_ROUTES = [
|
|
4900
|
-
{
|
|
4901
|
-
label: "Home",
|
|
4902
|
-
path: "/home"
|
|
4903
|
-
},
|
|
4904
|
-
{
|
|
4905
|
-
label: "Shop",
|
|
4906
|
-
path: "/home/shop"
|
|
4907
|
-
},
|
|
4908
|
-
{
|
|
4909
|
-
label: "Join / Sign Up",
|
|
4910
|
-
path: "/home/join"
|
|
4911
|
-
},
|
|
4912
|
-
{
|
|
4913
|
-
label: "Cart",
|
|
4914
|
-
path: "/cart"
|
|
4915
|
-
},
|
|
4916
|
-
{
|
|
4917
|
-
label: "Blog",
|
|
4918
|
-
path: "/home/blog"
|
|
4919
|
-
},
|
|
4920
|
-
{
|
|
4921
|
-
label: "Categories (all)",
|
|
4922
|
-
path: "/home/categories"
|
|
4923
|
-
},
|
|
4924
|
-
{
|
|
4925
|
-
label: "Collections (all)",
|
|
4926
|
-
path: "/home/collections"
|
|
4927
|
-
}
|
|
4928
|
-
];
|
|
4929
|
-
const RESOURCE_ROUTES = [
|
|
4930
|
-
{
|
|
4931
|
-
label: "Category",
|
|
4932
|
-
type: "category",
|
|
4933
|
-
template: "/home/categories/%s",
|
|
4934
|
-
fallback: "/home/categories"
|
|
4935
|
-
},
|
|
4936
|
-
{
|
|
4937
|
-
label: "Collection",
|
|
4938
|
-
type: "collection",
|
|
4939
|
-
template: "/home/collections/%s",
|
|
4940
|
-
fallback: "/home/collections"
|
|
4941
|
-
},
|
|
4942
|
-
{
|
|
4943
|
-
label: "Product",
|
|
4944
|
-
type: "product",
|
|
4945
|
-
template: "/home/products/%s",
|
|
4946
|
-
fallback: "/home/shop"
|
|
4947
|
-
},
|
|
4948
|
-
{
|
|
4949
|
-
label: "Library",
|
|
4950
|
-
type: "library",
|
|
4951
|
-
template: "/home/libraries/%s",
|
|
4952
|
-
fallback: "/home/libraries"
|
|
4953
|
-
},
|
|
4954
|
-
{
|
|
4955
|
-
label: "Post",
|
|
4956
|
-
type: "post",
|
|
4957
|
-
template: "/home/posts/%s",
|
|
4958
|
-
fallback: "/home/blog"
|
|
4959
|
-
},
|
|
4960
|
-
{
|
|
4961
|
-
label: "Media",
|
|
4962
|
-
type: "medium",
|
|
4963
|
-
template: "/home/media/%s",
|
|
4964
|
-
fallback: "/home/media"
|
|
4965
|
-
},
|
|
4966
|
-
{
|
|
4967
|
-
label: "Enrollment Pack",
|
|
4968
|
-
type: "enrollment_pack",
|
|
4969
|
-
template: "/home/enrollments/%s",
|
|
4970
|
-
fallback: "/home/join"
|
|
4971
|
-
},
|
|
4972
|
-
{
|
|
4973
|
-
label: "Page",
|
|
4974
|
-
type: "page",
|
|
4975
|
-
template: "/home/pages/%s",
|
|
4976
|
-
fallback: "/home/pages"
|
|
4977
|
-
}
|
|
4978
|
-
];
|
|
4979
|
-
async function fetchTemplatesForType(api, themeId, themeableType) {
|
|
4980
|
-
const params = new URLSearchParams({
|
|
4981
|
-
application_theme_id: String(themeId),
|
|
4982
|
-
themeable_type: themeableType,
|
|
4983
|
-
published: "true"
|
|
4984
|
-
});
|
|
4985
|
-
return (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
|
|
4986
|
-
}
|
|
4987
|
-
async function selectTemplate(api, themeId, themeableType, onCancel) {
|
|
4988
|
-
const templates = await fetchTemplatesForType(api, themeId, themeableType);
|
|
4989
|
-
if (templates.length <= 1) return null;
|
|
4990
|
-
const { templateId } = await prompts({
|
|
4991
|
-
type: "autocomplete",
|
|
4992
|
-
name: "templateId",
|
|
4993
|
-
message: "Select a template",
|
|
4994
|
-
choices: templates.map((t) => ({
|
|
4995
|
-
title: `${t.name}${t.default ? " (default)" : ""}`,
|
|
4996
|
-
value: t.id
|
|
4997
|
-
})),
|
|
4998
|
-
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
4999
|
-
}, { onCancel });
|
|
5000
|
-
return templateId ?? null;
|
|
5001
|
-
}
|
|
5002
|
-
function createNavigateCommand() {
|
|
5003
|
-
return new Command("navigate").description("Interactively navigate to a route in the dev server browser").option("--host <host>", "Dev server host", "127.0.0.1").option("--port <port>", "Dev server port", "9292").option("-t, --theme <id>", "Theme ID (defaults to active dev theme)").action(async (opts) => {
|
|
5004
|
-
requireToken();
|
|
5005
|
-
const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();
|
|
5006
|
-
if (!themeId) {
|
|
5007
|
-
console.error("No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.");
|
|
5008
|
-
process.exit(1);
|
|
5009
|
-
}
|
|
5010
|
-
const address = `http://${opts.host}:${opts.port}`;
|
|
5011
|
-
const choices = [...STATIC_ROUTES.map((r) => ({
|
|
5012
|
-
title: r.label,
|
|
5013
|
-
value: r.path
|
|
5014
|
-
})), ...RESOURCE_ROUTES.map((r) => ({
|
|
5015
|
-
title: `${r.label} (select specific)`,
|
|
5016
|
-
value: {
|
|
5017
|
-
resourceType: r.type,
|
|
5018
|
-
template: r.template,
|
|
5019
|
-
fallback: r.fallback,
|
|
5020
|
-
label: r.label
|
|
5021
|
-
}
|
|
5022
|
-
}))];
|
|
5023
|
-
const onCancel = () => process.exit(130);
|
|
5024
|
-
const { dest } = await prompts({
|
|
5025
|
-
type: "autocomplete",
|
|
5026
|
-
name: "dest",
|
|
5027
|
-
message: "Select a route",
|
|
5028
|
-
choices,
|
|
5029
|
-
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
5030
|
-
}, { onCancel });
|
|
5031
|
-
if (!dest) return;
|
|
5032
|
-
const api = createApiClient();
|
|
5033
|
-
let path;
|
|
5034
|
-
let themeableType;
|
|
5035
|
-
if (typeof dest === "string") {
|
|
5036
|
-
path = dest;
|
|
5037
|
-
themeableType = THEMEABLE_TYPE_MAP[dest];
|
|
5038
|
-
} else {
|
|
5039
|
-
themeableType = dest.resourceType;
|
|
5040
|
-
const resources = (await getApplicationThemeAvailableThemeables(api, themeId, {
|
|
5041
|
-
themeable: dest.resourceType,
|
|
5042
|
-
per_page: 50
|
|
5043
|
-
})).available_themeables ?? [];
|
|
5044
|
-
if (!resources.length) {
|
|
5045
|
-
console.log(`No ${dest.label} resources found, using listing page.`);
|
|
5046
|
-
path = dest.fallback;
|
|
5047
|
-
} else {
|
|
5048
|
-
const resourceChoices = resources.map((r) => ({
|
|
5049
|
-
title: r.title ?? r.slug ?? "Untitled",
|
|
5050
|
-
value: r.slug
|
|
5051
|
-
}));
|
|
5052
|
-
const { slug } = await prompts({
|
|
5053
|
-
type: "autocomplete",
|
|
5054
|
-
name: "slug",
|
|
5055
|
-
message: `Select a ${dest.label.toLowerCase()}`,
|
|
5056
|
-
choices: resourceChoices,
|
|
5057
|
-
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
5058
|
-
}, { onCancel });
|
|
5059
|
-
path = dest.template.replace("%s", slug);
|
|
5060
|
-
}
|
|
5061
|
-
}
|
|
5062
|
-
let templateParam = "";
|
|
5063
|
-
if (themeableType) {
|
|
5064
|
-
const templateId = await selectTemplate(api, themeId, themeableType, onCancel);
|
|
5065
|
-
if (templateId) templateParam = `?theme_template_id=${templateId}`;
|
|
5066
|
-
}
|
|
5067
|
-
const url = `${address}${path}${templateParam}`;
|
|
5068
|
-
console.log(`\nNavigating to: ${url}\n`);
|
|
5069
|
-
const open = (await import("open")).default;
|
|
5070
|
-
await open(url);
|
|
5071
|
-
});
|
|
5072
|
-
}
|
|
5073
|
-
//#endregion
|
|
5074
5781
|
//#region src/skills/install.ts
|
|
5075
5782
|
function listSkillNames(skillsDir) {
|
|
5076
5783
|
if (!existsSync(skillsDir)) return [];
|