@fluid-app/fluid-cli-theme-dev 0.1.21 → 0.1.23
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 +14 -0
- package/dist/index.mjs +155 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -4
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-typecheck.log +0 -4
- package/jest.config.cjs +0 -21
- package/jest.mocks/fluid-cli.ts +0 -33
- package/src/__tests__/plugin-state.test.ts +0 -186
- package/src/api.ts +0 -28
- package/src/commands/dev.ts +0 -186
- package/src/commands/init.ts +0 -51
- package/src/commands/lint.ts +0 -186
- package/src/commands/navigate.ts +0 -259
- package/src/commands/pull.ts +0 -242
- package/src/commands/push.ts +0 -220
- package/src/commands/theme.ts +0 -23
- package/src/index.ts +0 -12
- package/src/plugin-state.ts +0 -171
- package/src/theme/dev-server/hot-reload.ts +0 -65
- package/src/theme/dev-server/index.ts +0 -145
- package/src/theme/dev-server/proxy.ts +0 -125
- package/src/theme/dev-server/sse.ts +0 -43
- package/src/theme/dev-server/watcher.ts +0 -54
- package/src/theme/file.ts +0 -104
- package/src/theme/fluid-ignore.ts +0 -64
- package/src/theme/mime-type.ts +0 -45
- package/src/theme/root.ts +0 -54
- package/src/theme/syncer.ts +0 -338
- package/src/theme-config.ts +0 -34
- package/src/theme-picker.ts +0 -164
- package/src/workspace.ts +0 -71
- package/tsconfig.json +0 -10
- package/tsdown.config.ts +0 -19
- /package/{skills → dist/skills}/themes-review/SKILL.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/blocks-vs-sections.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/css-js-hygiene.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/dead-code.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/dynamism.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/editor-attributes.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/examples.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/fairshare-attributes.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/global-settings.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/liquid-correctness.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/navigation.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/performance.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/security-accessibility.md +0 -0
- /package/{skills → dist/skills}/themes-review/references/setting-types.md +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-theme-dev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
5
9
|
"type": "module",
|
|
6
10
|
"main": "./dist/index.mjs",
|
|
7
11
|
"types": "./dist/index.d.mts",
|
|
@@ -34,10 +38,10 @@
|
|
|
34
38
|
"jest": "^29.7.0",
|
|
35
39
|
"tsdown": "^0.21.0",
|
|
36
40
|
"typescript": "^5",
|
|
37
|
-
"@fluid-app/api-client-core": "0.1.0",
|
|
38
|
-
"@fluid-app/themes-api-client": "0.1.0",
|
|
39
41
|
"@fluid-app/theme-schema": "0.1.0",
|
|
40
|
-
"@fluid-app/
|
|
42
|
+
"@fluid-app/themes-api-client": "0.1.0",
|
|
43
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
44
|
+
"@fluid-app/api-client-core": "0.1.0"
|
|
41
45
|
},
|
|
42
46
|
"engines": {
|
|
43
47
|
"node": ">=18.0.0"
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @fluid-app/fluid-cli-theme-dev@0.1.21 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
|
|
3
|
-
> tsdown
|
|
4
|
-
|
|
5
|
-
[34mℹ[39m tsdown [2mv0.21.0[22m powered by rolldown [2mv1.0.0-rc.7[22m
|
|
6
|
-
[34mℹ[39m config file: [4m/home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev/tsdown.config.ts[24m
|
|
7
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
|
-
[34mℹ[39m target: [34mnode24[39m
|
|
9
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
|
-
[34mℹ[39m Build start
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 76.86 kB[22m [2m│ gzip: 21.05 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22mindex.mjs.map [2m196.02 kB[22m [2m│ gzip: 46.38 kB[22m
|
|
13
|
-
[34mℹ[39m [2mdist/[22mindex.d.mts.map [2m 0.11 kB[22m [2m│ gzip: 0.12 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.19 kB[22m [2m│ gzip: 0.16 kB[22m
|
|
15
|
-
[34mℹ[39m 4 files, total: 273.18 kB
|
|
16
|
-
[32m✔[39m Build complete in [32m1315ms[39m
|
package/jest.config.cjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
testEnvironment: "node",
|
|
3
|
-
testMatch: ["**/src/**/__tests__/**/*.test.ts"],
|
|
4
|
-
transform: {
|
|
5
|
-
"^.+\\.(t|j)sx?$": [
|
|
6
|
-
"@swc/jest",
|
|
7
|
-
{
|
|
8
|
-
module: {
|
|
9
|
-
type: "commonjs",
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
],
|
|
13
|
-
},
|
|
14
|
-
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
15
|
-
moduleNameMapper: {
|
|
16
|
-
// The real package only ships ESM, which Jest can't resolve in CJS mode;
|
|
17
|
-
// map it to a stateful in-memory mock for plugin-state tests.
|
|
18
|
-
"^@fluid-app/fluid-cli$": "<rootDir>/jest.mocks/fluid-cli.ts",
|
|
19
|
-
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
20
|
-
},
|
|
21
|
-
};
|
package/jest.mocks/fluid-cli.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jest mock for @fluid-app/fluid-cli.
|
|
3
|
-
*
|
|
4
|
-
* The real package only exports ESM (.mjs), which Jest can't resolve in CJS
|
|
5
|
-
* mode. plugin-state.ts only uses `readConfig`/`updateConfig`, so this mock
|
|
6
|
-
* backs them with an in-memory config. Tests seed/reset state through the
|
|
7
|
-
* normal `updateConfig` surface, so no test-only exports are needed.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
interface FakeConfig {
|
|
11
|
-
activeProfile: string | null;
|
|
12
|
-
profiles: Record<string, unknown>;
|
|
13
|
-
plugins: Record<string, unknown>;
|
|
14
|
-
enabledPlugins: string[] | null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let config: FakeConfig = {
|
|
18
|
-
activeProfile: null,
|
|
19
|
-
profiles: {},
|
|
20
|
-
plugins: {},
|
|
21
|
-
enabledPlugins: null,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function readConfig(): FakeConfig {
|
|
25
|
-
return config;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function updateConfig(
|
|
29
|
-
updater: (config: FakeConfig) => FakeConfig,
|
|
30
|
-
): FakeConfig {
|
|
31
|
-
config = updater(config);
|
|
32
|
-
return config;
|
|
33
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
readConfig,
|
|
4
|
-
updateConfig,
|
|
5
|
-
type FluidConfig,
|
|
6
|
-
} from "@fluid-app/fluid-cli";
|
|
7
|
-
import {
|
|
8
|
-
devThemeKey,
|
|
9
|
-
getDevTheme,
|
|
10
|
-
setDevTheme,
|
|
11
|
-
clearDevTheme,
|
|
12
|
-
setLastDevThemeId,
|
|
13
|
-
getLastDevThemeId,
|
|
14
|
-
} from "../plugin-state.js";
|
|
15
|
-
|
|
16
|
-
// `@fluid-app/fluid-cli` is mapped to an in-memory mock (jest.mocks/fluid-cli.ts)
|
|
17
|
-
// so these helpers never touch the real ~/.fluid/config.json. State is seeded
|
|
18
|
-
// and reset through the normal `updateConfig` surface.
|
|
19
|
-
jest.mock("node:fs", () => ({ existsSync: jest.fn() }));
|
|
20
|
-
const existsSyncMock = existsSync as unknown as jest.Mock;
|
|
21
|
-
|
|
22
|
-
const PLUGIN_KEY = "theme-dev";
|
|
23
|
-
|
|
24
|
-
function freshConfig(): FluidConfig {
|
|
25
|
-
return {
|
|
26
|
-
activeProfile: null,
|
|
27
|
-
profiles: {},
|
|
28
|
-
plugins: {},
|
|
29
|
-
enabledPlugins: null,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function setPluginState(state: Record<string, unknown>): void {
|
|
34
|
-
updateConfig((config) => ({
|
|
35
|
-
...config,
|
|
36
|
-
plugins: { ...config.plugins, [PLUGIN_KEY]: state },
|
|
37
|
-
}));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function pluginState(): Record<string, unknown> {
|
|
41
|
-
return readConfig().plugins[PLUGIN_KEY] as Record<string, unknown>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
updateConfig(() => freshConfig());
|
|
46
|
-
// Default: every stored project directory still exists (no pruning).
|
|
47
|
-
existsSyncMock.mockReset();
|
|
48
|
-
existsSyncMock.mockReturnValue(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("devThemeKey", () => {
|
|
52
|
-
it("combines company and theme root", () => {
|
|
53
|
-
expect(devThemeKey("acme", "/themes/store")).toBe("acme:/themes/store");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("falls back to 'default' when company is undefined", () => {
|
|
57
|
-
expect(devThemeKey(undefined, "/themes/store")).toBe(
|
|
58
|
-
"default:/themes/store",
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("setDevTheme / getDevTheme", () => {
|
|
64
|
-
it("stores and retrieves a dev theme per key", () => {
|
|
65
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
66
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("isolates dev themes across keys", () => {
|
|
70
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
71
|
-
setDevTheme("acme:/b", { id: 2, name: "B" });
|
|
72
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
|
|
73
|
-
expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns undefined for an unknown key", () => {
|
|
77
|
-
expect(getDevTheme("acme:/missing")).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("marks the most recently stored theme as lastDevThemeId", () => {
|
|
81
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
82
|
-
setDevTheme("acme:/b", { id: 2, name: "B" });
|
|
83
|
-
expect(getLastDevThemeId()).toBe(2);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("clearDevTheme", () => {
|
|
88
|
-
it("removes only the targeted key", () => {
|
|
89
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
90
|
-
setDevTheme("acme:/b", { id: 2, name: "B" });
|
|
91
|
-
clearDevTheme("acme:/a");
|
|
92
|
-
expect(getDevTheme("acme:/a")).toBeUndefined();
|
|
93
|
-
expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("is a no-op for an unknown key", () => {
|
|
97
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
98
|
-
clearDevTheme("acme:/missing");
|
|
99
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("clears lastDevThemeId when it pointed at the removed theme", () => {
|
|
103
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
104
|
-
expect(getLastDevThemeId()).toBe(1);
|
|
105
|
-
clearDevTheme("acme:/a");
|
|
106
|
-
expect(getLastDevThemeId()).toBeUndefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("leaves lastDevThemeId when it points at a different theme", () => {
|
|
110
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
111
|
-
setDevTheme("acme:/b", { id: 2, name: "B" });
|
|
112
|
-
clearDevTheme("acme:/a"); // most-recent is /b (id 2)
|
|
113
|
-
expect(getLastDevThemeId()).toBe(2);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("devThemes map stays bounded", () => {
|
|
118
|
-
it("prunes entries whose theme directory no longer exists on the next write", () => {
|
|
119
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
120
|
-
setDevTheme("acme:/b", { id: 2, name: "B" });
|
|
121
|
-
|
|
122
|
-
// /a is deleted; /b and the new /c still exist.
|
|
123
|
-
existsSyncMock.mockImplementation((p: string) => p !== "/a");
|
|
124
|
-
setDevTheme("acme:/c", { id: 3, name: "C" });
|
|
125
|
-
|
|
126
|
-
expect(getDevTheme("acme:/a")).toBeUndefined(); // pruned
|
|
127
|
-
expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
|
|
128
|
-
expect(getDevTheme("acme:/c")).toEqual({ id: 3, name: "C" });
|
|
129
|
-
expect(
|
|
130
|
-
Object.keys(pluginState()["devThemes"] as Record<string, unknown>),
|
|
131
|
-
).toEqual(["acme:/b", "acme:/c"]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("keeps the entry being written even if its own directory check fails", () => {
|
|
135
|
-
existsSyncMock.mockReturnValue(false);
|
|
136
|
-
setDevTheme("acme:/a", { id: 1, name: "A" });
|
|
137
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("setLastDevThemeId", () => {
|
|
142
|
-
it("updates the navigate pointer without recording a dev theme", () => {
|
|
143
|
-
setLastDevThemeId(123);
|
|
144
|
-
expect(getLastDevThemeId()).toBe(123);
|
|
145
|
-
expect(getDevTheme("acme:/a")).toBeUndefined();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("legacy migration", () => {
|
|
150
|
-
it("adopts a legacy global devThemeId for the first requesting key, then clears it", () => {
|
|
151
|
-
setPluginState({ devThemeId: 99, devThemeName: "Legacy" });
|
|
152
|
-
|
|
153
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 99, name: "Legacy" });
|
|
154
|
-
|
|
155
|
-
// Legacy fields are cleared so a second project can't adopt the same theme.
|
|
156
|
-
const state = pluginState();
|
|
157
|
-
expect(state["devThemeId"]).toBeUndefined();
|
|
158
|
-
expect(state["devThemeName"]).toBeUndefined();
|
|
159
|
-
expect(getDevTheme("acme:/b")).toBeUndefined();
|
|
160
|
-
expect(getLastDevThemeId()).toBe(99);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("synthesizes a name when the legacy name is absent", () => {
|
|
164
|
-
setPluginState({ devThemeId: 42 });
|
|
165
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 42, name: "Development #42" });
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("prefers an explicit per-key entry over the legacy id", () => {
|
|
169
|
-
setPluginState({
|
|
170
|
-
devThemeId: 99,
|
|
171
|
-
devThemes: { "acme:/a": { id: 7, name: "Real" } },
|
|
172
|
-
});
|
|
173
|
-
expect(getDevTheme("acme:/a")).toEqual({ id: 7, name: "Real" });
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("getLastDevThemeId", () => {
|
|
178
|
-
it("falls back to the legacy id before any per-project run", () => {
|
|
179
|
-
setPluginState({ devThemeId: 5 });
|
|
180
|
-
expect(getLastDevThemeId()).toBe(5);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("returns undefined when nothing is stored", () => {
|
|
184
|
-
expect(getLastDevThemeId()).toBeUndefined();
|
|
185
|
-
});
|
|
186
|
-
});
|
package/src/api.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createFetchClient,
|
|
3
|
-
type FetchClient,
|
|
4
|
-
} from "@fluid-app/api-client-core";
|
|
5
|
-
import { getAuthToken } from "@fluid-app/fluid-cli";
|
|
6
|
-
|
|
7
|
-
export type ApiClient = FetchClient;
|
|
8
|
-
|
|
9
|
-
/** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */
|
|
10
|
-
function getApiBase(): string {
|
|
11
|
-
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createApiClient(tokenOverride?: string): ApiClient {
|
|
15
|
-
return createFetchClient({
|
|
16
|
-
baseUrl: getApiBase(),
|
|
17
|
-
getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function requireToken(): string {
|
|
22
|
-
const token = getAuthToken();
|
|
23
|
-
if (!token) {
|
|
24
|
-
console.error("Not logged in. Run `fluid login` first.");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
return token;
|
|
28
|
-
}
|
package/src/commands/dev.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { requireToken, createApiClient } from "../api.js";
|
|
3
|
-
import { readThemeConfig } from "../theme-config.js";
|
|
4
|
-
import {
|
|
5
|
-
devThemeKey,
|
|
6
|
-
getDevTheme,
|
|
7
|
-
setDevTheme,
|
|
8
|
-
setLastDevThemeId,
|
|
9
|
-
clearDevTheme,
|
|
10
|
-
} from "../plugin-state.js";
|
|
11
|
-
import { ThemeRoot } from "../theme/root.js";
|
|
12
|
-
import { startDevServer } from "../theme/dev-server/index.js";
|
|
13
|
-
import { themes } from "@fluid-app/themes-api-client";
|
|
14
|
-
import { findTheme, type ApplicationTheme } from "../theme-picker.js";
|
|
15
|
-
import { findWorkspace, resolveThemeRootFromCwd } from "../workspace.js";
|
|
16
|
-
|
|
17
|
-
interface CompanyMe {
|
|
18
|
-
data: { company: { subdomain?: string; name?: string } };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function ensureDevTheme(
|
|
22
|
-
api: ReturnType<typeof createApiClient>,
|
|
23
|
-
projectKey: string,
|
|
24
|
-
identifier?: string,
|
|
25
|
-
): Promise<ApplicationTheme> {
|
|
26
|
-
if (identifier) {
|
|
27
|
-
const theme = await findTheme(api, identifier);
|
|
28
|
-
// Keep `navigate` pointed at whatever the dev server is actually serving.
|
|
29
|
-
setLastDevThemeId(theme.id);
|
|
30
|
-
return theme;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Reuse this project's stored dev theme if it still exists and is still a
|
|
34
|
-
// development theme (a published/promoted theme must not be edited in place).
|
|
35
|
-
const stored = getDevTheme(projectKey);
|
|
36
|
-
if (stored) {
|
|
37
|
-
try {
|
|
38
|
-
const body = await themes.getApplicationTheme(api, stored.id);
|
|
39
|
-
const existing = body.application_theme;
|
|
40
|
-
if (existing && existing.status === "development") {
|
|
41
|
-
console.log(`Using existing dev theme #${existing.id}`);
|
|
42
|
-
// Refresh the stored name and mark it most-recent for `navigate`.
|
|
43
|
-
setDevTheme(projectKey, { id: existing.id, name: existing.name });
|
|
44
|
-
return existing;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Theme no longer exists — fall through to create a new one.
|
|
48
|
-
}
|
|
49
|
-
// Stored theme is gone or no longer a dev theme; forget it.
|
|
50
|
-
clearDevTheme(projectKey);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Create a new development theme
|
|
54
|
-
const { hostname } = await import("node:os");
|
|
55
|
-
const host = hostname().split(".")[0] ?? "dev";
|
|
56
|
-
const name =
|
|
57
|
-
`Development (${host}-${Math.random().toString(36).slice(2, 8)})`.slice(
|
|
58
|
-
0,
|
|
59
|
-
50,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const body = await themes.createApplicationTheme(api, {
|
|
63
|
-
application_theme: { name, status: "development" },
|
|
64
|
-
});
|
|
65
|
-
const theme = body.application_theme;
|
|
66
|
-
setDevTheme(projectKey, { id: theme.id, name: theme.name });
|
|
67
|
-
console.log(`Created dev theme: ${theme.name} (#${theme.id})`);
|
|
68
|
-
return theme;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function createDevCommand(): Command {
|
|
72
|
-
return new Command("dev")
|
|
73
|
-
.description("Start the theme dev server with hot reload")
|
|
74
|
-
.option("--host <host>", "Local server host", "127.0.0.1")
|
|
75
|
-
.option("--port <port>", "Local server port", "9292")
|
|
76
|
-
.option(
|
|
77
|
-
"-t, --theme <name-or-id>",
|
|
78
|
-
"Use an existing theme instead of dev theme",
|
|
79
|
-
)
|
|
80
|
-
.option("-f, --force", "Skip schema validation on upload")
|
|
81
|
-
.option("--live-reload <mode>", "Reload mode: full-page | off", "full-page")
|
|
82
|
-
.option("--navigate", "Open browser navigator after server starts")
|
|
83
|
-
.option("--root <path>", "Theme root directory", ".")
|
|
84
|
-
.action(
|
|
85
|
-
async (opts: {
|
|
86
|
-
host: string;
|
|
87
|
-
port: string;
|
|
88
|
-
theme?: string;
|
|
89
|
-
force?: boolean;
|
|
90
|
-
liveReload: string;
|
|
91
|
-
navigate?: boolean;
|
|
92
|
-
root: string;
|
|
93
|
-
}) => {
|
|
94
|
-
requireToken();
|
|
95
|
-
|
|
96
|
-
// If no explicit --root and we're inside a workspace, resolve to the theme root
|
|
97
|
-
let rootPath = opts.root;
|
|
98
|
-
if (rootPath === ".") {
|
|
99
|
-
const workspace = findWorkspace();
|
|
100
|
-
if (workspace) {
|
|
101
|
-
rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const themeRoot = new ThemeRoot(rootPath);
|
|
106
|
-
if (!themeRoot.isValid()) {
|
|
107
|
-
console.error(`'${rootPath}' does not look like a theme directory.`);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const port = Number(opts.port);
|
|
112
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
113
|
-
console.error(
|
|
114
|
-
`Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
|
|
115
|
-
);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
|
|
120
|
-
const api = createApiClient();
|
|
121
|
-
const config = readThemeConfig(themeRoot.root);
|
|
122
|
-
|
|
123
|
-
// Use company from .fluid-theme.json if available, otherwise fetch
|
|
124
|
-
let company: string;
|
|
125
|
-
if (config?.company) {
|
|
126
|
-
company = config.company;
|
|
127
|
-
} else {
|
|
128
|
-
const companyRes = await api.get<CompanyMe>(
|
|
129
|
-
"/api/company/v1/companies/me",
|
|
130
|
-
);
|
|
131
|
-
company = companyRes.data?.company?.subdomain ?? "";
|
|
132
|
-
if (!company) {
|
|
133
|
-
console.error(
|
|
134
|
-
"Could not determine company subdomain. Make sure your token is valid.",
|
|
135
|
-
);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Always iterate on an isolated dev theme: reuse the stored one or
|
|
141
|
-
// create a fresh `development` theme. `--theme` is the explicit
|
|
142
|
-
// escape hatch for targeting an existing theme. We deliberately do NOT
|
|
143
|
-
// fall back to `.fluid-theme.json`'s themeId — that points at whatever
|
|
144
|
-
// was pulled (often the active/production theme), and the dev server's
|
|
145
|
-
// `delete: true` sync would overwrite it in place.
|
|
146
|
-
const projectKey = devThemeKey(company, themeRoot.root);
|
|
147
|
-
const theme = opts.theme
|
|
148
|
-
? await ensureDevTheme(api, projectKey, opts.theme)
|
|
149
|
-
: await ensureDevTheme(api, projectKey);
|
|
150
|
-
const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;
|
|
151
|
-
|
|
152
|
-
let stop: (() => void) | undefined;
|
|
153
|
-
|
|
154
|
-
const cleanup = () => {
|
|
155
|
-
stop?.();
|
|
156
|
-
process.exit(0);
|
|
157
|
-
};
|
|
158
|
-
process.on("SIGINT", cleanup);
|
|
159
|
-
process.on("SIGTERM", cleanup);
|
|
160
|
-
|
|
161
|
-
stop = await startDevServer(
|
|
162
|
-
api,
|
|
163
|
-
{
|
|
164
|
-
id: theme.id,
|
|
165
|
-
name: theme.name,
|
|
166
|
-
company,
|
|
167
|
-
editorUrl,
|
|
168
|
-
},
|
|
169
|
-
themeRoot,
|
|
170
|
-
{ host: opts.host, port, reloadMode, validate: !opts.force },
|
|
171
|
-
(address) => {
|
|
172
|
-
console.log(`\n Dev server: ${address}`);
|
|
173
|
-
console.log(` Web editor: ${editorUrl}`);
|
|
174
|
-
console.log("\n Watching for file changes…\n");
|
|
175
|
-
|
|
176
|
-
if (opts.navigate) {
|
|
177
|
-
import("open").then((m) => m.default(`${address}/home`));
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
// Keep process alive
|
|
183
|
-
await new Promise(() => {});
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { rmSync, existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import prompts from "prompts";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_CLONE_URL = "git@github.com:fluid-commerce/base-theme.git";
|
|
8
|
-
|
|
9
|
-
const SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;
|
|
10
|
-
|
|
11
|
-
export function createInitCommand(): Command {
|
|
12
|
-
return new Command("init")
|
|
13
|
-
.description("Initialize a new theme by cloning the base theme")
|
|
14
|
-
.argument("[name]", "Directory name for the new theme")
|
|
15
|
-
.option("-u, --clone-url <url>", "Git URL to clone from", DEFAULT_CLONE_URL)
|
|
16
|
-
.action(async (name: string | undefined, opts: { cloneUrl: string }) => {
|
|
17
|
-
if (!name) {
|
|
18
|
-
const res = await prompts(
|
|
19
|
-
{
|
|
20
|
-
type: "text",
|
|
21
|
-
name: "name",
|
|
22
|
-
message: "Theme name",
|
|
23
|
-
},
|
|
24
|
-
{ onCancel: () => process.exit(130) },
|
|
25
|
-
);
|
|
26
|
-
name = res.name as string;
|
|
27
|
-
if (!name) {
|
|
28
|
-
console.error("No name provided.");
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!SAFE_NAME_RE.test(name)) {
|
|
34
|
-
console.error(
|
|
35
|
-
`Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,
|
|
36
|
-
);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);
|
|
41
|
-
execFileSync("git", ["clone", opts.cloneUrl, name], { stdio: "inherit" });
|
|
42
|
-
|
|
43
|
-
for (const dir of [".git", ".github"]) {
|
|
44
|
-
const path = join(name, dir);
|
|
45
|
-
if (existsSync(path)) rmSync(path, { recursive: true, force: true });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log(`\nTheme initialized in ./${name}`);
|
|
49
|
-
console.log(`Next steps:\n cd ${name}\n fluid theme push`);
|
|
50
|
-
});
|
|
51
|
-
}
|