@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
|
@@ -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
|
-
}
|
package/src/theme/mime-type.ts
DELETED
|
@@ -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
|
-
}
|