@fluid-app/fluid-cli-theme-dev 0.1.10 → 0.1.12
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/.turbo/turbo-build.log +6 -8
- package/dist/index.mjs +340 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/commands/dev.ts +36 -13
- package/src/commands/pull.ts +204 -6
- package/src/commands/push.ts +124 -6
- package/src/theme/dev-server/index.ts +22 -2
- package/src/theme/file.ts +28 -0
- package/src/theme/syncer.ts +35 -2
- package/src/theme-config.ts +34 -0
- package/src/workspace.ts +71 -0
- package/tsdown.config.ts +1 -1
package/src/theme/syncer.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface SyncResult {
|
|
|
11
11
|
downloaded: number;
|
|
12
12
|
deleted: number;
|
|
13
13
|
errors: string[];
|
|
14
|
+
validationFailed: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export class Syncer {
|
|
@@ -46,6 +47,11 @@ export class Syncer {
|
|
|
46
47
|
return [...this.checksums.keys()];
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** Snapshot of remote checksums (key → sha256). Available after fetchChecksums() or downloadAll(). */
|
|
51
|
+
remoteChecksums(): Record<string, string> {
|
|
52
|
+
return Object.fromEntries(this.checksums);
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
// ─── Upload ───────────────────────────────────────────────────────────────
|
|
50
56
|
|
|
51
57
|
async uploadFile(file: ThemeFile): Promise<void> {
|
|
@@ -196,6 +202,7 @@ export class Syncer {
|
|
|
196
202
|
async uploadTheme(
|
|
197
203
|
opts: {
|
|
198
204
|
delete?: boolean;
|
|
205
|
+
validate?: boolean;
|
|
199
206
|
onProgress?: (done: number, total: number) => void;
|
|
200
207
|
} = {},
|
|
201
208
|
): Promise<SyncResult> {
|
|
@@ -207,8 +214,25 @@ export class Syncer {
|
|
|
207
214
|
deleted: 0,
|
|
208
215
|
downloaded: 0,
|
|
209
216
|
errors: [],
|
|
217
|
+
validationFailed: false,
|
|
210
218
|
};
|
|
211
219
|
|
|
220
|
+
// Schema validation pass
|
|
221
|
+
if (opts.validate) {
|
|
222
|
+
for (const file of localFiles) {
|
|
223
|
+
if (!file.isLiquid) continue;
|
|
224
|
+
const diagnostics = file.validateSchema();
|
|
225
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
226
|
+
for (const d of errors) {
|
|
227
|
+
result.errors.push(`${file.relativePath}: ${d.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (result.errors.length > 0) {
|
|
231
|
+
result.validationFailed = true;
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
212
236
|
const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));
|
|
213
237
|
let done = 0;
|
|
214
238
|
for (const file of toUpload) {
|
|
@@ -242,19 +266,28 @@ export class Syncer {
|
|
|
242
266
|
async downloadTheme(
|
|
243
267
|
opts: {
|
|
244
268
|
delete?: boolean;
|
|
269
|
+
skip?: Set<string>;
|
|
245
270
|
onProgress?: (done: number, total: number) => void;
|
|
246
271
|
} = {},
|
|
247
|
-
): Promise<SyncResult> {
|
|
272
|
+
): Promise<SyncResult & { skipped: number }> {
|
|
248
273
|
const resources = await this.downloadAll();
|
|
249
|
-
const result: SyncResult = {
|
|
274
|
+
const result: SyncResult & { skipped: number } = {
|
|
250
275
|
uploaded: 0,
|
|
251
276
|
deleted: 0,
|
|
252
277
|
downloaded: 0,
|
|
278
|
+
skipped: 0,
|
|
253
279
|
errors: [],
|
|
280
|
+
validationFailed: false,
|
|
254
281
|
};
|
|
255
282
|
|
|
256
283
|
let done = 0;
|
|
257
284
|
for (const resource of resources) {
|
|
285
|
+
if (opts.skip?.has(resource.key)) {
|
|
286
|
+
result.skipped++;
|
|
287
|
+
opts.onProgress?.(++done, resources.length);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
258
291
|
const file = this.themeRoot.file(resource.key);
|
|
259
292
|
|
|
260
293
|
// Guard against path traversal from malicious API responses
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ThemeConfig {
|
|
5
|
+
themeId: number;
|
|
6
|
+
themeName: string;
|
|
7
|
+
company: string;
|
|
8
|
+
lastPulledAt: string | null;
|
|
9
|
+
checksums: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILE = ".fluid-theme.json";
|
|
13
|
+
|
|
14
|
+
function configPath(themeRoot: string): string {
|
|
15
|
+
return join(themeRoot, CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
|
|
19
|
+
export function readThemeConfig(themeRoot: string): ThemeConfig | null {
|
|
20
|
+
const path = configPath(themeRoot);
|
|
21
|
+
if (!existsSync(path)) return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(path, "utf-8");
|
|
24
|
+
return JSON.parse(raw) as ThemeConfig;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Write `.fluid-theme.json` to a theme directory. */
|
|
31
|
+
export function writeThemeConfig(themeRoot: string, config: ThemeConfig): void {
|
|
32
|
+
const path = configPath(themeRoot);
|
|
33
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
34
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface FluidWorkspace {
|
|
5
|
+
/** Absolute path to the workspace root (where .fluid-workspace.json lives) */
|
|
6
|
+
root: string;
|
|
7
|
+
/** Parsed workspace config */
|
|
8
|
+
config: WorkspaceConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface WorkspaceConfig {
|
|
12
|
+
type: string;
|
|
13
|
+
version: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const WORKSPACE_FILE = ".fluid-workspace.json";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walk up from `startDir` looking for `.fluid-workspace.json`.
|
|
20
|
+
* Returns the workspace info if found, or `null` if not in a workspace.
|
|
21
|
+
*/
|
|
22
|
+
export function findWorkspace(startDir?: string): FluidWorkspace | null {
|
|
23
|
+
let dir = resolve(startDir ?? process.cwd());
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line no-constant-condition
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = join(dir, WORKSPACE_FILE);
|
|
28
|
+
if (existsSync(candidate)) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
31
|
+
const config = JSON.parse(raw) as WorkspaceConfig;
|
|
32
|
+
return { root: dir, config };
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir) break; // reached filesystem root
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If cwd is already inside `{workspace}/local/{company}/...`, return that
|
|
47
|
+
* theme root directory. Otherwise return null.
|
|
48
|
+
*
|
|
49
|
+
* Examples (workspace root = /code/fluid-theme-dev):
|
|
50
|
+
* cwd = /code/fluid-theme-dev/local/acme-co → /code/fluid-theme-dev/local/acme-co
|
|
51
|
+
* cwd = /code/fluid-theme-dev/local/acme-co/templates → /code/fluid-theme-dev/local/acme-co
|
|
52
|
+
* cwd = /code/fluid-theme-dev → null
|
|
53
|
+
* cwd = /code/fluid-theme-dev/local → null
|
|
54
|
+
*/
|
|
55
|
+
export function resolveThemeRootFromCwd(
|
|
56
|
+
workspace: FluidWorkspace,
|
|
57
|
+
): string | null {
|
|
58
|
+
const cwd = resolve(process.cwd());
|
|
59
|
+
const localDir = join(workspace.root, "local");
|
|
60
|
+
const rel = relative(localDir, cwd);
|
|
61
|
+
|
|
62
|
+
// Not under local/ at all, or exactly at local/
|
|
63
|
+
if (rel.startsWith("..") || rel === ".") return null;
|
|
64
|
+
|
|
65
|
+
// rel is like "acme-co" or "acme-co/templates/subfolder"
|
|
66
|
+
// The theme root is the first segment: local/{company}
|
|
67
|
+
const firstSegment = rel.split(sep)[0];
|
|
68
|
+
if (!firstSegment) return null;
|
|
69
|
+
|
|
70
|
+
return join(localDir, firstSegment);
|
|
71
|
+
}
|