@fluid-app/fluid-cli-theme-dev 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +14 -0
  2. package/dist/index.mjs +159 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +6 -2
  5. package/.turbo/turbo-build.log +0 -16
  6. package/.turbo/turbo-typecheck.log +0 -4
  7. package/jest.config.cjs +0 -21
  8. package/jest.mocks/fluid-cli.ts +0 -33
  9. package/src/__tests__/plugin-state.test.ts +0 -186
  10. package/src/api.ts +0 -28
  11. package/src/commands/dev.ts +0 -186
  12. package/src/commands/init.ts +0 -51
  13. package/src/commands/lint.ts +0 -186
  14. package/src/commands/navigate.ts +0 -259
  15. package/src/commands/pull.ts +0 -242
  16. package/src/commands/push.ts +0 -220
  17. package/src/commands/theme.ts +0 -23
  18. package/src/index.ts +0 -12
  19. package/src/plugin-state.ts +0 -171
  20. package/src/theme/dev-server/hot-reload.ts +0 -65
  21. package/src/theme/dev-server/index.ts +0 -145
  22. package/src/theme/dev-server/proxy.ts +0 -125
  23. package/src/theme/dev-server/sse.ts +0 -43
  24. package/src/theme/dev-server/watcher.ts +0 -54
  25. package/src/theme/file.ts +0 -104
  26. package/src/theme/fluid-ignore.ts +0 -64
  27. package/src/theme/mime-type.ts +0 -45
  28. package/src/theme/root.ts +0 -54
  29. package/src/theme/syncer.ts +0 -338
  30. package/src/theme-config.ts +0 -34
  31. package/src/theme-picker.ts +0 -164
  32. package/src/workspace.ts +0 -71
  33. package/tsconfig.json +0 -10
  34. package/tsdown.config.ts +0 -19
  35. /package/{skills → dist/skills}/themes-review/SKILL.md +0 -0
  36. /package/{skills → dist/skills}/themes-review/references/blocks-vs-sections.md +0 -0
  37. /package/{skills → dist/skills}/themes-review/references/css-js-hygiene.md +0 -0
  38. /package/{skills → dist/skills}/themes-review/references/dead-code.md +0 -0
  39. /package/{skills → dist/skills}/themes-review/references/dynamism.md +0 -0
  40. /package/{skills → dist/skills}/themes-review/references/editor-attributes.md +0 -0
  41. /package/{skills → dist/skills}/themes-review/references/examples.md +0 -0
  42. /package/{skills → dist/skills}/themes-review/references/fairshare-attributes.md +0 -0
  43. /package/{skills → dist/skills}/themes-review/references/global-settings.md +0 -0
  44. /package/{skills → dist/skills}/themes-review/references/liquid-correctness.md +0 -0
  45. /package/{skills → dist/skills}/themes-review/references/navigation.md +0 -0
  46. /package/{skills → dist/skills}/themes-review/references/performance.md +0 -0
  47. /package/{skills → dist/skills}/themes-review/references/security-accessibility.md +0 -0
  48. /package/{skills → dist/skills}/themes-review/references/setting-types.md +0 -0
@@ -1,125 +0,0 @@
1
- import https from "node:https";
2
- import type { IncomingMessage, ServerResponse } from "node:http";
3
- import { injectHotReload } from "./hot-reload.js";
4
- import { getAuthToken } from "@fluid-app/fluid-cli";
5
-
6
- const HOP_BY_HOP = new Set([
7
- "connection",
8
- "keep-alive",
9
- "proxy-authenticate",
10
- "proxy-authorization",
11
- "te",
12
- "trailer",
13
- "transfer-encoding",
14
- "upgrade",
15
- "content-security-policy",
16
- ]);
17
-
18
- export interface ProxyOptions {
19
- company: string;
20
- themeId: number;
21
- reloadMode: "full-page" | "off";
22
- pendingFiles?: () => Array<{ relativePath: string; read: () => string }>;
23
- }
24
-
25
- export async function proxyRequest(
26
- req: IncomingMessage,
27
- res: ServerResponse,
28
- opts: ProxyOptions,
29
- ): Promise<void> {
30
- const companyHost = `${opts.company}.fluid.app`;
31
-
32
- const headers: Record<string, string> = {};
33
- for (const [k, v] of Object.entries(req.headers)) {
34
- if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === "string") {
35
- headers[k] = v;
36
- }
37
- }
38
- headers["host"] = companyHost;
39
- headers["x-fluid-theme"] = String(opts.themeId);
40
- headers["user-agent"] = "Fluid CLI";
41
- headers["accept-encoding"] = "identity";
42
-
43
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
44
- url.searchParams.set("_fd", "0");
45
- url.searchParams.set("pb", "0");
46
-
47
- const pending = opts.pendingFiles?.() ?? [];
48
- const isGet = req.method === "GET" || req.method === "HEAD";
49
- let method = req.method ?? "GET";
50
- let body: string | Buffer | undefined;
51
-
52
- if (pending.length > 0 && isGet) {
53
- method = "POST";
54
- const params = new URLSearchParams();
55
- params.set("_method", req.method ?? "GET");
56
- for (const f of pending) {
57
- params.set(`replace_templates[${f.relativePath}]`, f.read());
58
- }
59
- const token = getAuthToken();
60
- if (token) headers["authorization"] = `Bearer ${token}`;
61
- headers["content-type"] = "application/x-www-form-urlencoded";
62
- body = params.toString();
63
- headers["content-length"] = String(Buffer.byteLength(body));
64
- } else if (!isGet) {
65
- body = await readBody(req);
66
- if (body.length > 0) {
67
- headers["content-length"] = String(body.length);
68
- }
69
- }
70
-
71
- return new Promise((resolve, reject) => {
72
- const options: https.RequestOptions = {
73
- hostname: companyHost,
74
- port: 443,
75
- path: url.pathname + (url.search || ""),
76
- method,
77
- headers,
78
- };
79
-
80
- const proxyReq = https.request(options, (proxyRes) => {
81
- const contentType = proxyRes.headers["content-type"] ?? "";
82
- const isHtml = contentType.includes("text/html");
83
-
84
- const responseHeaders: Record<string, string | string[]> = {};
85
- for (const [k, v] of Object.entries(proxyRes.headers)) {
86
- if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {
87
- responseHeaders[k] = v as string | string[];
88
- }
89
- }
90
-
91
- if (isHtml) {
92
- const chunks: Buffer[] = [];
93
- proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk));
94
- proxyRes.on("end", () => {
95
- let html = Buffer.concat(chunks).toString("utf-8");
96
- html = injectHotReload(html, opts.reloadMode);
97
- responseHeaders["content-length"] = String(Buffer.byteLength(html));
98
- res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);
99
- res.end(html);
100
- resolve();
101
- });
102
- } else {
103
- res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);
104
- proxyRes.pipe(res);
105
- proxyRes.on("end", resolve);
106
- }
107
- });
108
-
109
- proxyReq.on("error", (err) => {
110
- reject(err);
111
- });
112
-
113
- if (body) proxyReq.write(body);
114
- proxyReq.end();
115
- });
116
- }
117
-
118
- function readBody(req: IncomingMessage): Promise<Buffer> {
119
- return new Promise((resolve, reject) => {
120
- const chunks: Buffer[] = [];
121
- req.on("data", (chunk: Buffer) => chunks.push(chunk));
122
- req.on("end", () => resolve(Buffer.concat(chunks)));
123
- req.on("error", reject);
124
- });
125
- }
@@ -1,43 +0,0 @@
1
- import type { ServerResponse } from "node:http";
2
-
3
- export class SSEStream {
4
- private responses = new Set<ServerResponse>();
5
-
6
- add(res: ServerResponse): void {
7
- res.writeHead(200, {
8
- "Content-Type": "text/event-stream",
9
- "Cache-Control": "no-cache",
10
- Connection: "keep-alive",
11
- "Access-Control-Allow-Origin": "*",
12
- });
13
- res.write(":\n\n");
14
- this.responses.add(res);
15
- res.on("close", () => this.responses.delete(res));
16
- }
17
-
18
- broadcast(data: string): void {
19
- const payload = `data: ${data}\n\n`;
20
- for (const res of this.responses) {
21
- try {
22
- res.write(payload);
23
- } catch {
24
- this.responses.delete(res);
25
- }
26
- }
27
- }
28
-
29
- close(): void {
30
- for (const res of this.responses) {
31
- try {
32
- res.end();
33
- } catch {
34
- // ignore
35
- }
36
- }
37
- this.responses.clear();
38
- }
39
-
40
- get size(): number {
41
- return this.responses.size;
42
- }
43
- }
@@ -1,54 +0,0 @@
1
- import { relative } from "node:path";
2
- import chokidar from "chokidar";
3
- import type { ThemeRoot } from "../root.js";
4
- import type { ThemeFile } from "../file.js";
5
-
6
- export type FileChangeHandler = (
7
- modified: ThemeFile[],
8
- added: ThemeFile[],
9
- removed: ThemeFile[],
10
- ) => Promise<void>;
11
-
12
- export function watchTheme(
13
- root: ThemeRoot,
14
- handler: FileChangeHandler,
15
- ): () => Promise<void> {
16
- const watcher = chokidar.watch(root.root, {
17
- ignoreInitial: true,
18
- ignored: (filePath: string) => {
19
- if (filePath.includes("node_modules")) return true;
20
- try {
21
- const rel = relative(root.root, filePath);
22
- const basename = rel.split(/[\\/]/).pop() ?? "";
23
- return basename.startsWith(".") || root.ignore.ignore(rel);
24
- } catch {
25
- return false;
26
- }
27
- },
28
- persistent: true,
29
- awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },
30
- });
31
-
32
- let pending = Promise.resolve();
33
- const enqueue = (fn: () => Promise<void>) => {
34
- pending = pending.then(fn).catch(() => {});
35
- };
36
-
37
- watcher.on("change", (filePath) => {
38
- const rel = relative(root.root, filePath);
39
- if (root.ignore.ignore(rel)) return;
40
- enqueue(() => handler([root.file(filePath)], [], []));
41
- });
42
-
43
- watcher.on("add", (filePath) => {
44
- const rel = relative(root.root, filePath);
45
- if (root.ignore.ignore(rel)) return;
46
- enqueue(() => handler([], [root.file(filePath)], []));
47
- });
48
-
49
- watcher.on("unlink", (filePath) => {
50
- enqueue(() => handler([], [], [root.file(filePath)]));
51
- });
52
-
53
- return () => watcher.close();
54
- }
package/src/theme/file.ts DELETED
@@ -1,104 +0,0 @@
1
- import {
2
- readFileSync,
3
- writeFileSync,
4
- mkdirSync,
5
- existsSync,
6
- statSync,
7
- } from "node:fs";
8
- import { extname, basename, relative, dirname } from "node:path";
9
- import { createHash } from "node:crypto";
10
- import { mimeTypeFor, type MimeType } from "./mime-type.js";
11
- import {
12
- validateSchemaText,
13
- type Diagnostic,
14
- type BlocksSchemaType,
15
- } from "@fluid-app/theme-schema";
16
-
17
- // Top-level theme folders that are not page templates. Everything else at the
18
- // top level (home_page, product, page, footer, navbar, …) is a page template.
19
- const NON_TEMPLATE_DIRS = new Set([
20
- "sections",
21
- "blocks",
22
- "components",
23
- "layouts",
24
- "config",
25
- "assets",
26
- "locales",
27
- ]);
28
-
29
- export class ThemeFile {
30
- readonly absolutePath: string;
31
- readonly relativePath: string;
32
- readonly mime: MimeType;
33
-
34
- constructor(absolutePath: string, root: string) {
35
- this.absolutePath = absolutePath;
36
- this.relativePath = relative(root, absolutePath);
37
- this.mime = mimeTypeFor(extname(absolutePath).toLowerCase());
38
- }
39
-
40
- get name(): string {
41
- return basename(this.absolutePath);
42
- }
43
-
44
- get isText(): boolean {
45
- return this.mime.isText;
46
- }
47
-
48
- get isLiquid(): boolean {
49
- return this.absolutePath.endsWith(".liquid");
50
- }
51
-
52
- get isJson(): boolean {
53
- return this.absolutePath.endsWith(".json");
54
- }
55
-
56
- get exists(): boolean {
57
- return existsSync(this.absolutePath);
58
- }
59
-
60
- read(): string {
61
- return readFileSync(this.absolutePath, "utf-8");
62
- }
63
-
64
- readBinary(): Buffer {
65
- return readFileSync(this.absolutePath);
66
- }
67
-
68
- write(content: string | Buffer): void {
69
- mkdirSync(dirname(this.absolutePath), { recursive: true });
70
- if (typeof content === "string") {
71
- writeFileSync(this.absolutePath, content, "utf-8");
72
- } else {
73
- writeFileSync(this.absolutePath, content);
74
- }
75
- }
76
-
77
- checksum(): string {
78
- const content = this.isText ? this.read() : this.readBinary();
79
- return createHash("sha256").update(content).digest("hex");
80
- }
81
-
82
- size(): number {
83
- return statSync(this.absolutePath).size;
84
- }
85
-
86
- get isTemplate(): boolean {
87
- // Page templates (home_page, product, footer, navbar, …) live in top-level
88
- // page-type folders and expect blocks as objects. The reserved categories
89
- // below either expect blocks as arrays (sections, blocks, components) or
90
- // carry no block schema (layouts, config, assets, locales).
91
- const parts = this.relativePath.split(/[/\\]/);
92
- return parts.length >= 2 && !NON_TEMPLATE_DIRS.has(parts[0]!);
93
- }
94
-
95
- validateSchema(): Diagnostic[] {
96
- if (!this.isLiquid) return [];
97
-
98
- const blocksSchemaType: BlocksSchemaType = this.isTemplate
99
- ? "object"
100
- : "array";
101
-
102
- return validateSchemaText(this.read(), { blocksSchemaType });
103
- }
104
- }
@@ -1,64 +0,0 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
- import { join, basename } from "node:path";
3
-
4
- const IGNORE_FILE = ".fluidignore";
5
-
6
- interface Pattern {
7
- negated: boolean;
8
- pattern: string;
9
- }
10
-
11
- export class FluidIgnore {
12
- private patterns: Pattern[];
13
-
14
- constructor(root: string) {
15
- this.patterns = this.parse(join(root, IGNORE_FILE));
16
- }
17
-
18
- ignore(relativePath: string): boolean {
19
- let result = false;
20
- for (const { negated, pattern } of this.patterns) {
21
- if (this.match(pattern, relativePath)) {
22
- result = !negated;
23
- }
24
- }
25
- return result;
26
- }
27
-
28
- private parse(filePath: string): Pattern[] {
29
- if (!existsSync(filePath)) return [];
30
- return readFileSync(filePath, "utf-8")
31
- .split("\n")
32
- .map((l) => l.trim())
33
- .filter((l) => l && !l.startsWith("#"))
34
- .map((l) => {
35
- const negated = l.startsWith("!");
36
- let pattern = negated ? l.slice(1) : l;
37
- if (pattern.startsWith("/")) pattern = pattern.slice(1);
38
- return { negated, pattern };
39
- });
40
- }
41
-
42
- private match(pattern: string, path: string): boolean {
43
- if (pattern.endsWith("/")) {
44
- return path.startsWith(pattern) || path === pattern.slice(0, -1);
45
- }
46
- if (pattern.includes("/")) {
47
- return this.fnmatch(pattern, path);
48
- }
49
- return this.fnmatch(pattern, path) || this.fnmatch(pattern, basename(path));
50
- }
51
-
52
- private fnmatch(pattern: string, str: string): boolean {
53
- const re = pattern
54
- .split("**")
55
- .map((p) =>
56
- p
57
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
58
- .replace(/\*/g, "[^/]*")
59
- .replace(/\?/g, "[^/]"),
60
- )
61
- .join(".*");
62
- return new RegExp(`^${re}$`).test(str);
63
- }
64
- }
@@ -1,45 +0,0 @@
1
- const TEXT_TYPES: Record<string, string> = {
2
- ".liquid": "text/x-liquid",
3
- ".json": "application/json",
4
- ".css": "text/css",
5
- ".js": "application/javascript",
6
- ".html": "text/html",
7
- ".txt": "text/plain",
8
- ".md": "text/markdown",
9
- ".svg": "image/svg+xml",
10
- };
11
-
12
- const BINARY_TYPES: Record<string, string> = {
13
- ".png": "image/png",
14
- ".jpg": "image/jpeg",
15
- ".jpeg": "image/jpeg",
16
- ".gif": "image/gif",
17
- ".webp": "image/webp",
18
- ".ico": "image/x-icon",
19
- ".woff": "font/woff",
20
- ".woff2": "font/woff2",
21
- ".ttf": "font/ttf",
22
- ".eot": "application/vnd.ms-fontobject",
23
- ".otf": "font/otf",
24
- ".pdf": "application/pdf",
25
- ".zip": "application/zip",
26
- ".mp4": "video/mp4",
27
- ".webm": "video/webm",
28
- ".mp3": "audio/mpeg",
29
- ".wav": "audio/wav",
30
- };
31
-
32
- export interface MimeType {
33
- name: string;
34
- isText: boolean;
35
- }
36
-
37
- export function mimeTypeFor(ext: string): MimeType {
38
- const text = TEXT_TYPES[ext];
39
- if (text) return { name: text, isText: true };
40
-
41
- const binary = BINARY_TYPES[ext];
42
- if (binary) return { name: binary, isText: false };
43
-
44
- return { name: "application/octet-stream", isText: false };
45
- }
package/src/theme/root.ts DELETED
@@ -1,54 +0,0 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import { isAbsolute, join, resolve } from "node:path";
3
- import { ThemeFile } from "./file.js";
4
- import { FluidIgnore } from "./fluid-ignore.js";
5
-
6
- const THEME_MARKERS = ["templates", "assets", "config"];
7
-
8
- export class ThemeRoot {
9
- readonly root: string;
10
- readonly ignore: FluidIgnore;
11
-
12
- constructor(root: string) {
13
- this.root = resolve(root);
14
- this.ignore = new FluidIgnore(this.root);
15
- }
16
-
17
- isValid(): boolean {
18
- return THEME_MARKERS.some((m) => {
19
- try {
20
- return statSync(join(this.root, m)).isDirectory();
21
- } catch {
22
- return false;
23
- }
24
- });
25
- }
26
-
27
- files(): ThemeFile[] {
28
- return this.glob(this.root).filter(
29
- (f) => !this.ignore.ignore(f.relativePath),
30
- );
31
- }
32
-
33
- file(pathOrFile: string | ThemeFile): ThemeFile {
34
- if (pathOrFile instanceof ThemeFile) return pathOrFile;
35
- const abs = isAbsolute(pathOrFile)
36
- ? pathOrFile
37
- : join(this.root, pathOrFile);
38
- return new ThemeFile(abs, this.root);
39
- }
40
-
41
- private glob(dir: string): ThemeFile[] {
42
- const results: ThemeFile[] = [];
43
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
44
- if (entry.name.startsWith(".")) continue;
45
- const full = join(dir, entry.name);
46
- if (entry.isDirectory()) {
47
- results.push(...this.glob(full));
48
- } else if (entry.isFile()) {
49
- results.push(new ThemeFile(full, this.root));
50
- }
51
- }
52
- return results;
53
- }
54
- }