@fluid-app/fluid-cli-theme-dev 0.1.2 → 0.1.4

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @fluid-app/fluid-cli-theme-dev@0.1.2 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
2
+ > @fluid-app/fluid-cli-theme-dev@0.1.4 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,11 +8,9 @@
8
8
  ℹ target: node18
9
9
  ℹ tsconfig: tsconfig.json
10
10
  ℹ Build start
11
- ℹ dist/index.mjs 38.99 kB │ gzip: 11.20 kB
12
- ℹ dist/index.mjs.map 86.91 kB │ gzip: 21.69 kB
13
- ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
- ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
- ℹ 4 files, total: 126.20 kB
16
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
17
-
18
- ✔ Build complete in 4554ms
11
+ ℹ dist/index.mjs  41.47 kB │ gzip: 11.70 kB
12
+ ℹ dist/index.mjs.map 110.82 kB │ gzip: 24.52 kB
13
+ ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
+ ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
+ ℹ 4 files, total: 152.60 kB
16
+ ✔ Build complete in 1219ms
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @fluid-app/fluid-cli-theme-dev
2
+
3
+ Fluid CLI plugin for theme development. Adds `fluid theme` commands for local dev server, push, pull, and scaffolding.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @fluid-app/fluid-cli @fluid-app/fluid-cli-theme-dev
9
+ ```
10
+
11
+ Requires `@fluid-app/fluid-cli` as the core CLI.
12
+
13
+ ## Authentication
14
+
15
+ Log in before using any theme commands:
16
+
17
+ ```bash
18
+ fluid login
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `fluid theme dev`
24
+
25
+ Start a local dev server that proxies your storefront with hot reload:
26
+
27
+ ```bash
28
+ fluid theme dev
29
+ ```
30
+
31
+ The dev server will:
32
+
33
+ 1. Create (or reuse) a development theme
34
+ 2. Upload all local files to the dev theme
35
+ 3. Watch for file changes and sync them automatically
36
+ 4. Proxy requests to `{company}.fluid.app` with local file overrides
37
+
38
+ | Flag | Default | Description |
39
+ | -------------------------- | ----------- | ----------------------------------------- |
40
+ | `--host <host>` | `127.0.0.1` | Local server host |
41
+ | `--port <port>` | `9292` | Local server port |
42
+ | `-t, --theme <name-or-id>` | auto | Use a specific theme instead of dev theme |
43
+ | `--live-reload <mode>` | `full-page` | Reload mode: `full-page` or `off` |
44
+ | `--navigate` | off | Open browser navigator after start |
45
+ | `--root <path>` | `.` | Theme root directory |
46
+
47
+ ### `fluid theme push`
48
+
49
+ Upload local theme files to a remote theme:
50
+
51
+ ```bash
52
+ fluid theme push # Interactive theme selection
53
+ fluid theme push --theme "My Theme" # By name
54
+ fluid theme push --theme 42 # By ID
55
+ fluid theme push --publish # Push and publish
56
+ fluid theme push --nodelete # Keep remote files not present locally
57
+ ```
58
+
59
+ ### `fluid theme pull`
60
+
61
+ Download a remote theme to your local directory:
62
+
63
+ ```bash
64
+ fluid theme pull # Interactive theme selection
65
+ fluid theme pull --theme "My Theme" # By name or ID
66
+ fluid theme pull --nodelete # Keep local files not present on remote
67
+ ```
68
+
69
+ ### `fluid theme init`
70
+
71
+ Scaffold a new theme from the base template:
72
+
73
+ ```bash
74
+ fluid theme init my-theme
75
+ cd my-theme
76
+ fluid theme dev
77
+ ```
78
+
79
+ ### `fluid theme navigate`
80
+
81
+ Interactively select a route and open it in the browser (requires a running dev server):
82
+
83
+ ```bash
84
+ fluid theme navigate
85
+ ```
86
+
87
+ ## Theme Directory Structure
88
+
89
+ A valid theme directory must contain at least one of: `templates/`, `assets/`, or `config/`.
90
+
91
+ Use a `.fluidignore` file (same syntax as `.gitignore`) to exclude files from syncing.
92
+
93
+ ## Development
94
+
95
+ For contributors working in `fluid-mono`:
96
+
97
+ ```bash
98
+ pnpm --filter @fluid-app/fluid-cli-theme-dev build
99
+ node packages/cli/core/dist/bin/fluid.mjs theme --help
100
+ ```
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { getAuthToken, readConfig, updateConfig } from "@fluid-app/fluid-cli";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
4
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { createHash } from "node:crypto";
6
6
  import http from "node:http";
7
7
  import https from "node:https";
@@ -377,7 +377,7 @@ var ThemeRoot = class {
377
377
  }
378
378
  file(pathOrFile) {
379
379
  if (pathOrFile instanceof ThemeFile) return pathOrFile;
380
- return new ThemeFile(join(this.root, pathOrFile), this.root);
380
+ return new ThemeFile(isAbsolute(pathOrFile) ? pathOrFile : join(this.root, pathOrFile), this.root);
381
381
  }
382
382
  glob(dir) {
383
383
  const results = [];
@@ -610,6 +610,92 @@ function watchTheme(root, handler) {
610
610
  return () => watcher.close();
611
611
  }
612
612
  //#endregion
613
+ //#region ../../api-clients/themes/src/namespaces/v0.ts
614
+ /**
615
+ * List application themes
616
+ * Get all application themes with optional filters
617
+ *
618
+ * @param client - Fetch client instance
619
+ * @param [params] - params
620
+ */
621
+ async function listApplicationThemes(client, params) {
622
+ return client.get(`/api/application_themes`, params);
623
+ }
624
+ /**
625
+ * Create an application theme
626
+ *
627
+ *
628
+ * @param client - Fetch client instance
629
+ * @param body - body
630
+ */
631
+ async function createApplicationTheme(client, body) {
632
+ return client.post(`/api/application_themes`, body);
633
+ }
634
+ /**
635
+ * Get an application theme
636
+ *
637
+ *
638
+ * @param client - Fetch client instance
639
+ * @param id - id
640
+ * @param [params] - params
641
+ */
642
+ async function getApplicationTheme(client, id, params) {
643
+ return client.get(`/api/application_themes/${id}`, params);
644
+ }
645
+ /**
646
+ * Returns available themeables for a given type scoped to the theme's company
647
+ *
648
+ *
649
+ * @param client - Fetch client instance
650
+ * @param id - id
651
+ * @param [params] - params
652
+ */
653
+ async function getApplicationThemeAvailableThemeables(client, id, params) {
654
+ return client.get(`/api/application_themes/${id}/available_themeables`, params);
655
+ }
656
+ /**
657
+ * Publishes the theme
658
+ *
659
+ *
660
+ * @param client - Fetch client instance
661
+ * @param id - id
662
+ */
663
+ async function publishApplicationTheme(client, id) {
664
+ return client.post(`/api/application_themes/${id}/publish`);
665
+ }
666
+ /**
667
+ * Lists all theme resources
668
+ *
669
+ *
670
+ * @param client - Fetch client instance
671
+ * @param application_theme_id - application_theme_id
672
+ */
673
+ async function listThemeResources(client, application_theme_id) {
674
+ return client.get(`/api/application_themes/${application_theme_id}/resources`);
675
+ }
676
+ /**
677
+ * Updates a theme resource
678
+ *
679
+ *
680
+ * @param client - Fetch client instance
681
+ * @param application_theme_id - application_theme_id
682
+ * @param body - body
683
+ */
684
+ async function updateThemeResource(client, application_theme_id, body) {
685
+ return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
686
+ }
687
+ /**
688
+ * Deletes a theme resource
689
+ *
690
+ *
691
+ * @param client - Fetch client instance
692
+ * @param application_theme_id - application_theme_id
693
+ * @param body - body
694
+ */
695
+ async function deleteThemeResource(client, application_theme_id, body) {
696
+ return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
697
+ }
698
+ //#endregion
613
699
  //#region src/theme/syncer.ts
614
700
  var Syncer = class {
615
701
  checksums = /* @__PURE__ */ new Map();
@@ -619,11 +705,11 @@ var Syncer = class {
619
705
  this.themeRoot = themeRoot;
620
706
  }
621
707
  async fetchChecksums() {
622
- const body = await this.api.get(`/api/application_themes/${this.themeId}/resources`);
708
+ const body = await listThemeResources(this.api, this.themeId);
623
709
  this.updateChecksums(body.application_theme_resources ?? []);
624
710
  }
625
711
  updateChecksums(resources) {
626
- for (const r of resources) if (r.key) this.checksums.set(r.key, r.checksum);
712
+ for (const r of resources) if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);
627
713
  for (const key of this.checksums.keys()) if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);
628
714
  }
629
715
  hasChanged(file) {
@@ -633,14 +719,13 @@ var Syncer = class {
633
719
  return [...this.checksums.keys()];
634
720
  }
635
721
  async uploadFile(file) {
636
- const path = `/api/application_themes/${this.themeId}/resources`;
637
- if (file.isText) await this.api.put(path, { application_theme_resource: {
722
+ if (file.isText) await updateThemeResource(this.api, this.themeId, { application_theme_resource: {
638
723
  key: file.relativePath,
639
724
  content: file.read()
640
725
  } });
641
- else await this.uploadBinaryFile(file, path);
726
+ else await this.uploadBinaryFile(file);
642
727
  }
643
- async uploadBinaryFile(file, resourcePath) {
728
+ async uploadBinaryFile(file) {
644
729
  const asset = (await this.api.post("/api/dam/assets", { placeholder_asset: {
645
730
  description: `Uploaded via Fluid CLI: ${file.name}`,
646
731
  mime_type: file.mime.name,
@@ -675,7 +760,7 @@ var Syncer = class {
675
760
  if (ikBody.height) backfillPayload["asset"]["height"] = ikBody.height;
676
761
  if (ikBody.width) backfillPayload["asset"]["width"] = ikBody.width;
677
762
  const backfillBody = await this.api.post("/api/dam/assets/backfill_imagekit", backfillPayload);
678
- await this.api.put(resourcePath, { application_theme_resource: {
763
+ await updateThemeResource(this.api, this.themeId, { application_theme_resource: {
679
764
  key: file.relativePath,
680
765
  dam_asset: {
681
766
  dam_asset_code: backfillBody.asset.code,
@@ -702,13 +787,13 @@ var Syncer = class {
702
787
  }[category] ?? "files"}/${assetCode}`;
703
788
  }
704
789
  async deleteRemoteFile(relativePath) {
705
- await this.api.delete(`/api/application_themes/${this.themeId}/resources`, { body: { application_theme_resource: { key: relativePath } } });
790
+ await deleteThemeResource(this.api, this.themeId, { application_theme_resource: { key: relativePath } });
706
791
  this.checksums.delete(relativePath);
707
792
  }
708
793
  async downloadAll() {
709
- const body = await this.api.get(`/api/application_themes/${this.themeId}/resources`);
710
- this.updateChecksums(body.application_theme_resources ?? []);
711
- return body.application_theme_resources ?? [];
794
+ const resources = (await listThemeResources(this.api, this.themeId)).application_theme_resources ?? [];
795
+ this.updateChecksums(resources);
796
+ return resources;
712
797
  }
713
798
  async downloadBinaryAsset(url) {
714
799
  const resp = await fetch(url);
@@ -767,7 +852,10 @@ var Syncer = class {
767
852
  if (resource.resource_type === "FileResource" && resource.url) {
768
853
  const buf = await this.downloadBinaryAsset(resource.url);
769
854
  file.write(buf);
770
- } else if (resource.content !== void 0) file.write(resource.content);
855
+ } else if (resource.content !== void 0 && resource.content !== null) {
856
+ const content = typeof resource.content === "string" ? resource.content : JSON.stringify(resource.content);
857
+ file.write(content);
858
+ }
771
859
  result.downloaded++;
772
860
  } catch (e) {
773
861
  result.errors.push(`Download ${resource.key}: ${e}`);
@@ -856,7 +944,7 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
856
944
  //#region src/commands/dev.ts
857
945
  async function ensureDevTheme(api, identifier) {
858
946
  if (identifier) {
859
- const body = await api.get("/api/application_themes");
947
+ const body = await listApplicationThemes(api);
860
948
  const found = (body.application_themes ?? []).find((t) => String(t.id) === identifier) ?? (body.application_themes ?? []).find((t) => t.name.toLowerCase() === identifier.toLowerCase());
861
949
  if (!found) {
862
950
  console.error(`Theme not found: ${identifier}`);
@@ -866,17 +954,16 @@ async function ensureDevTheme(api, identifier) {
866
954
  }
867
955
  const { devThemeId } = getPluginState();
868
956
  if (devThemeId) try {
869
- const body = await api.get(`/api/application_themes/${devThemeId}`);
957
+ const body = await getApplicationTheme(api, devThemeId);
870
958
  if (body.application_theme) {
871
959
  console.log(`Using existing dev theme #${devThemeId}`);
872
960
  return body.application_theme;
873
961
  }
874
962
  } catch {}
875
963
  const { hostname } = await import("node:os");
876
- const name = `Development (${hostname().split(".")[0] ?? "dev"}-${Math.random().toString(36).slice(2, 8)})`.slice(0, 50);
877
- const theme = (await api.post("/api/application_themes", { application_theme: {
878
- name,
879
- role: "development"
964
+ const theme = (await createApplicationTheme(api, { application_theme: {
965
+ name: `Development (${hostname().split(".")[0] ?? "dev"}-${Math.random().toString(36).slice(2, 8)})`.slice(0, 50),
966
+ status: "development"
880
967
  } })).application_theme;
881
968
  setPluginState({
882
969
  devThemeId: theme.id,
@@ -893,8 +980,18 @@ function createDevCommand() {
893
980
  console.error(`'${opts.root}' does not look like a theme directory.`);
894
981
  process.exit(1);
895
982
  }
983
+ const port = Number(opts.port);
984
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
985
+ console.error(`Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`);
986
+ process.exit(1);
987
+ }
896
988
  const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
897
989
  const api = createApiClient();
990
+ const company = (await api.get("/api/company/v1/companies/me")).data?.company?.subdomain;
991
+ if (!company) {
992
+ console.error("Could not determine company subdomain. Make sure your token is valid.");
993
+ process.exit(1);
994
+ }
898
995
  const theme = await ensureDevTheme(api, opts.theme);
899
996
  let stop;
900
997
  const cleanup = () => {
@@ -903,16 +1000,11 @@ function createDevCommand() {
903
1000
  };
904
1001
  process.on("SIGINT", cleanup);
905
1002
  process.on("SIGTERM", cleanup);
906
- const port = Number(opts.port);
907
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
908
- console.error(`Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`);
909
- process.exit(1);
910
- }
911
1003
  stop = await startDevServer(api, {
912
1004
  id: theme.id,
913
1005
  name: theme.name,
914
- company: theme.company,
915
- editorUrl: theme.editor_url
1006
+ company,
1007
+ editorUrl: theme.editor_url ?? void 0
916
1008
  }, themeRoot, {
917
1009
  host: opts.host,
918
1010
  port,
@@ -929,8 +1021,8 @@ function createDevCommand() {
929
1021
  //#endregion
930
1022
  //#region src/commands/push.ts
931
1023
  async function selectTheme(api) {
932
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
933
- if (!themes.length) {
1024
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1025
+ if (!themeList.length) {
934
1026
  console.error("No themes found.");
935
1027
  process.exit(1);
936
1028
  }
@@ -938,7 +1030,7 @@ async function selectTheme(api) {
938
1030
  type: "select",
939
1031
  name: "id",
940
1032
  message: "Select a theme to push to",
941
- choices: themes.map((t) => ({
1033
+ choices: themeList.map((t) => ({
942
1034
  title: `${t.name} (#${t.id})`,
943
1035
  value: t.id
944
1036
  }))
@@ -947,11 +1039,11 @@ async function selectTheme(api) {
947
1039
  console.error("No theme selected.");
948
1040
  process.exit(1);
949
1041
  }
950
- return themes.find((t) => t.id === id);
1042
+ return themeList.find((t) => t.id === id);
951
1043
  }
952
1044
  async function findTheme(api, identifier) {
953
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
954
- const found = themes.find((t) => String(t.id) === identifier) ?? themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1045
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1046
+ const found = themeList.find((t) => String(t.id) === identifier) ?? themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
955
1047
  if (!found) {
956
1048
  console.error(`No theme found with identifier: ${identifier}`);
957
1049
  process.exit(1);
@@ -983,7 +1075,7 @@ function createPushCommand() {
983
1075
  if (opts.publish) {
984
1076
  const pubSpinner = ora("Publishing theme…").start();
985
1077
  try {
986
- await api.post(`/api/application_themes/${theme.id}/publish`);
1078
+ await publishApplicationTheme(api, theme.id);
987
1079
  pubSpinner.succeed("Theme published.");
988
1080
  } catch (e) {
989
1081
  pubSpinner.fail(`Publish failed: ${e}`);
@@ -994,13 +1086,13 @@ function createPushCommand() {
994
1086
  //#endregion
995
1087
  //#region src/commands/pull.ts
996
1088
  async function selectOrFindTheme(api, identifier) {
997
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
998
- if (!themes.length) {
1089
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1090
+ if (!themeList.length) {
999
1091
  console.error("No themes found.");
1000
1092
  process.exit(1);
1001
1093
  }
1002
1094
  if (identifier) {
1003
- const found = themes.find((t) => String(t.id) === identifier) ?? themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1095
+ const found = themeList.find((t) => String(t.id) === identifier) ?? themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1004
1096
  if (!found) {
1005
1097
  console.error(`No theme found with identifier: ${identifier}`);
1006
1098
  process.exit(1);
@@ -1011,7 +1103,7 @@ async function selectOrFindTheme(api, identifier) {
1011
1103
  type: "select",
1012
1104
  name: "id",
1013
1105
  message: "Select a theme to pull",
1014
- choices: themes.map((t) => ({
1106
+ choices: themeList.map((t) => ({
1015
1107
  title: `${t.name} (#${t.id})`,
1016
1108
  value: t.id
1017
1109
  }))
@@ -1020,7 +1112,7 @@ async function selectOrFindTheme(api, identifier) {
1020
1112
  console.error("No theme selected.");
1021
1113
  process.exit(1);
1022
1114
  }
1023
- return themes.find((t) => t.id === id);
1115
+ return themeList.find((t) => t.id === id);
1024
1116
  }
1025
1117
  function createPullCommand() {
1026
1118
  return new Command("pull").description("Pull a remote theme to your local directory").option("-t, --theme <name-or-id>", "Theme name or ID to pull").option("-n, --nodelete", "Do not delete local files missing on remote").option("--root <path>", "Theme root directory", ".").action(async (opts) => {
@@ -1188,7 +1280,7 @@ function createNavigateCommand() {
1188
1280
  let path;
1189
1281
  if (typeof dest === "string") path = dest;
1190
1282
  else {
1191
- const resources = (await createApiClient().get(`/api/application_themes/${themeId}/available_themeables`, {
1283
+ const resources = (await getApplicationThemeAvailableThemeables(createApiClient(), themeId, {
1192
1284
  themeable: dest.resourceType,
1193
1285
  per_page: 50
1194
1286
  })).available_themeables ?? [];
@@ -1201,7 +1293,7 @@ function createNavigateCommand() {
1201
1293
  name: "slug",
1202
1294
  message: `Select a ${dest.label.toLowerCase()}`,
1203
1295
  choices: resources.map((r) => ({
1204
- title: r.title ?? r.slug,
1296
+ title: r.title ?? r.slug ?? "Untitled",
1205
1297
  value: r.slug
1206
1298
  }))
1207
1299
  }, { onCancel });