radiant-docs 0.1.0
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/dist/index.js +312 -0
- package/package.json +38 -0
- package/template/.vscode/extensions.json +4 -0
- package/template/.vscode/launch.json +11 -0
- package/template/astro.config.mjs +216 -0
- package/template/ec.config.mjs +51 -0
- package/template/package-lock.json +12546 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +9 -0
- package/template/src/assets/icons/check.svg +33 -0
- package/template/src/assets/icons/danger.svg +37 -0
- package/template/src/assets/icons/info.svg +36 -0
- package/template/src/assets/icons/lightbulb.svg +74 -0
- package/template/src/assets/icons/warning.svg +37 -0
- package/template/src/components/Header.astro +176 -0
- package/template/src/components/MdxPage.astro +49 -0
- package/template/src/components/OpenApiPage.astro +270 -0
- package/template/src/components/Search.astro +362 -0
- package/template/src/components/Sidebar.astro +19 -0
- package/template/src/components/SidebarDropdown.astro +149 -0
- package/template/src/components/SidebarGroup.astro +51 -0
- package/template/src/components/SidebarLink.astro +56 -0
- package/template/src/components/SidebarMenu.astro +46 -0
- package/template/src/components/SidebarSubgroup.astro +136 -0
- package/template/src/components/TableOfContents.astro +480 -0
- package/template/src/components/ThemeSwitcher.astro +84 -0
- package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
- package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
- package/template/src/components/endpoint/PlaygroundField.astro +54 -0
- package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
- package/template/src/components/endpoint/RequestSnippets.astro +308 -0
- package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
- package/template/src/components/endpoint/ResponseFields.astro +224 -0
- package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
- package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
- package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
- package/template/src/components/ui/Field.astro +69 -0
- package/template/src/components/ui/Tag.astro +5 -0
- package/template/src/components/ui/demo/CodeDemo.astro +15 -0
- package/template/src/components/ui/demo/Demo.astro +3 -0
- package/template/src/components/ui/demo/UiDisplay.astro +13 -0
- package/template/src/components/user/Accordian.astro +69 -0
- package/template/src/components/user/AccordianGroup.astro +13 -0
- package/template/src/components/user/Callout.astro +101 -0
- package/template/src/components/user/Step.astro +51 -0
- package/template/src/components/user/Steps.astro +9 -0
- package/template/src/components/user/Tab.astro +25 -0
- package/template/src/components/user/Tabs.astro +122 -0
- package/template/src/content.config.ts +11 -0
- package/template/src/entrypoint.ts +9 -0
- package/template/src/layouts/Layout.astro +92 -0
- package/template/src/lib/component-error.ts +163 -0
- package/template/src/lib/frontmatter-schema.ts +9 -0
- package/template/src/lib/oas.ts +24 -0
- package/template/src/lib/pagefind.ts +88 -0
- package/template/src/lib/routes.ts +316 -0
- package/template/src/lib/utils.ts +59 -0
- package/template/src/lib/validation.ts +1097 -0
- package/template/src/pages/[...slug].astro +77 -0
- package/template/src/styles/global.css +209 -0
- package/template/tsconfig.json +5 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { spawn, execSync } from "child_process";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs-extra";
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import envPaths from "env-paths";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
var __dirname = path.dirname(__filename);
|
|
13
|
+
var paths = envPaths("radiant", { suffix: "" });
|
|
14
|
+
var VERSION = "0.1.0";
|
|
15
|
+
var lastErrorMessage = "";
|
|
16
|
+
var lastErrorTime = 0;
|
|
17
|
+
var ERROR_DEDUPE_MS = 2e3;
|
|
18
|
+
var colors = {
|
|
19
|
+
reset: "\x1B[0m",
|
|
20
|
+
bright: "\x1B[1m",
|
|
21
|
+
dim: "\x1B[2m",
|
|
22
|
+
red: "\x1B[31m",
|
|
23
|
+
green: "\x1B[32m",
|
|
24
|
+
yellow: "\x1B[33m",
|
|
25
|
+
blue: "\x1B[34m",
|
|
26
|
+
cyan: "\x1B[36m",
|
|
27
|
+
magenta: "\x1B[35m"
|
|
28
|
+
};
|
|
29
|
+
var log = {
|
|
30
|
+
info: (msg) => console.log(`${colors.cyan}\u25CF${colors.reset} ${msg}`),
|
|
31
|
+
success: (msg) => console.log(`${colors.green}\u2713${colors.reset} ${msg}`),
|
|
32
|
+
error: (msg) => console.log(`${colors.red}\u2717${colors.reset} ${msg}`),
|
|
33
|
+
warn: (msg) => console.log(`${colors.yellow}\u26A0${colors.reset} ${msg}`),
|
|
34
|
+
file: (msg) => console.log(
|
|
35
|
+
`${colors.dim}\u21BB${colors.reset} ${colors.dim}${msg}${colors.reset}`
|
|
36
|
+
),
|
|
37
|
+
blank: () => console.log("")
|
|
38
|
+
};
|
|
39
|
+
function getUserDocsDir() {
|
|
40
|
+
return process.cwd();
|
|
41
|
+
}
|
|
42
|
+
function getProjectHash(docsDir) {
|
|
43
|
+
return crypto.createHash("md5").update(docsDir).digest("hex").slice(0, 8);
|
|
44
|
+
}
|
|
45
|
+
function getCacheDir(docsDir) {
|
|
46
|
+
const projectHash = getProjectHash(docsDir);
|
|
47
|
+
return path.join(paths.cache, projectHash);
|
|
48
|
+
}
|
|
49
|
+
function getTemplateDir() {
|
|
50
|
+
return path.join(__dirname, "../template");
|
|
51
|
+
}
|
|
52
|
+
function getContentDir(cacheDir) {
|
|
53
|
+
return path.join(cacheDir, "src/content/docs");
|
|
54
|
+
}
|
|
55
|
+
async function validateUserDocs(docsDir) {
|
|
56
|
+
const docsJsonPath = path.join(docsDir, "docs.json");
|
|
57
|
+
if (!fs.existsSync(docsJsonPath)) {
|
|
58
|
+
log.error("No docs.json found in current directory.");
|
|
59
|
+
log.info("Run this command from your documentation root folder.");
|
|
60
|
+
log.blank();
|
|
61
|
+
log.info(`Expected location: ${colors.dim}${docsJsonPath}${colors.reset}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(docsJsonPath, "utf-8");
|
|
66
|
+
JSON.parse(content);
|
|
67
|
+
} catch {
|
|
68
|
+
log.error("Invalid JSON in docs.json");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function setupCache(cacheDir) {
|
|
73
|
+
const templateDir = getTemplateDir();
|
|
74
|
+
const nodeModulesExists = fs.existsSync(path.join(cacheDir, "node_modules"));
|
|
75
|
+
const cacheExists = fs.existsSync(cacheDir);
|
|
76
|
+
if (!fs.existsSync(templateDir)) {
|
|
77
|
+
log.error(
|
|
78
|
+
"Framework template not found. The CLI package may be corrupted."
|
|
79
|
+
);
|
|
80
|
+
log.info(`Expected at: ${templateDir}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
let needsUpdate = !cacheExists;
|
|
84
|
+
if (cacheExists) {
|
|
85
|
+
try {
|
|
86
|
+
const templatePkg = await fs.readJson(
|
|
87
|
+
path.join(templateDir, "package.json")
|
|
88
|
+
);
|
|
89
|
+
const cachePkg = await fs.readJson(path.join(cacheDir, "package.json"));
|
|
90
|
+
if (templatePkg.version !== cachePkg.version) {
|
|
91
|
+
needsUpdate = true;
|
|
92
|
+
log.info("Framework update detected, refreshing cache...");
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
needsUpdate = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (needsUpdate) {
|
|
99
|
+
log.info("Setting up Radiant framework...");
|
|
100
|
+
if (cacheExists) {
|
|
101
|
+
await fs.remove(cacheDir);
|
|
102
|
+
}
|
|
103
|
+
await fs.copy(templateDir, cacheDir, {
|
|
104
|
+
filter: (src) => !src.includes("node_modules")
|
|
105
|
+
});
|
|
106
|
+
log.success("Framework copied to cache.");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return !nodeModulesExists;
|
|
110
|
+
}
|
|
111
|
+
async function installDependencies(cacheDir) {
|
|
112
|
+
log.info("Installing dependencies (first run only)...");
|
|
113
|
+
try {
|
|
114
|
+
execSync("npm install --silent --no-fund --no-audit", {
|
|
115
|
+
cwd: cacheDir,
|
|
116
|
+
stdio: "pipe"
|
|
117
|
+
});
|
|
118
|
+
log.success("Dependencies installed.");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log.error("Failed to install dependencies.");
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function syncContent(docsDir, contentDir) {
|
|
125
|
+
await fs.emptyDir(contentDir);
|
|
126
|
+
await fs.copy(docsDir, contentDir, {
|
|
127
|
+
filter: (src) => {
|
|
128
|
+
const basename = path.basename(src);
|
|
129
|
+
const relativePath = path.relative(docsDir, src);
|
|
130
|
+
if (basename.startsWith(".")) return false;
|
|
131
|
+
if (relativePath.includes("node_modules")) return false;
|
|
132
|
+
if (basename === "package.json" || basename === "package-lock.json")
|
|
133
|
+
return false;
|
|
134
|
+
if (basename.toLowerCase().startsWith("readme")) return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function runAstroSync(cacheDir) {
|
|
140
|
+
try {
|
|
141
|
+
execSync("npx astro sync", {
|
|
142
|
+
cwd: cacheDir,
|
|
143
|
+
stdio: "pipe"
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function startDevServer(cacheDir) {
|
|
149
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
150
|
+
cwd: cacheDir,
|
|
151
|
+
shell: true,
|
|
152
|
+
env: {
|
|
153
|
+
...process.env,
|
|
154
|
+
FORCE_COLOR: "1"
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
let serverStarted = false;
|
|
158
|
+
let serverUrl = "http://localhost:4321";
|
|
159
|
+
const processOutput = (data, isError = false) => {
|
|
160
|
+
const output = data.toString();
|
|
161
|
+
const lines = output.split("\n");
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const trimmedLine = line.trim();
|
|
164
|
+
if (!trimmedLine) continue;
|
|
165
|
+
const urlMatch = line.match(/http:\/\/localhost:\d+/);
|
|
166
|
+
if (urlMatch) {
|
|
167
|
+
serverUrl = urlMatch[0];
|
|
168
|
+
}
|
|
169
|
+
if (!serverStarted && (line.includes("watching for file changes") || line.includes("Local") && line.includes("localhost"))) {
|
|
170
|
+
serverStarted = true;
|
|
171
|
+
log.blank();
|
|
172
|
+
log.success("Dev server ready!");
|
|
173
|
+
console.log(` ${colors.bright}Local:${colors.reset} ${serverUrl}`);
|
|
174
|
+
log.blank();
|
|
175
|
+
log.info("Watching for changes...");
|
|
176
|
+
log.blank();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (line.includes("[USER_ERROR]")) {
|
|
180
|
+
const errorMsg = line.split("[USER_ERROR]:")[1]?.trim() || line;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
if (errorMsg === lastErrorMessage && now - lastErrorTime < ERROR_DEDUPE_MS) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
lastErrorMessage = errorMsg;
|
|
186
|
+
lastErrorTime = now;
|
|
187
|
+
log.blank();
|
|
188
|
+
log.error(errorMsg);
|
|
189
|
+
log.blank();
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
child.stdout?.on("data", (data) => processOutput(data, false));
|
|
195
|
+
child.stderr?.on("data", (data) => processOutput(data, true));
|
|
196
|
+
child.on("exit", (code) => {
|
|
197
|
+
if (code !== 0 && code !== null) {
|
|
198
|
+
log.blank();
|
|
199
|
+
log.error(`Dev server exited with code ${code}`);
|
|
200
|
+
process.exit(code);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return child;
|
|
204
|
+
}
|
|
205
|
+
function watchUserContent(docsDir, contentDir, _onSync) {
|
|
206
|
+
const watcher = chokidar.watch(docsDir, {
|
|
207
|
+
ignored: [
|
|
208
|
+
"**/node_modules/**",
|
|
209
|
+
"**/.git/**",
|
|
210
|
+
"**/package.json",
|
|
211
|
+
"**/package-lock.json",
|
|
212
|
+
/^\./
|
|
213
|
+
],
|
|
214
|
+
persistent: true,
|
|
215
|
+
ignoreInitial: true,
|
|
216
|
+
awaitWriteFinish: {
|
|
217
|
+
stabilityThreshold: 100,
|
|
218
|
+
pollInterval: 50
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
let debounceTimer = null;
|
|
222
|
+
const pendingChanges = /* @__PURE__ */ new Set();
|
|
223
|
+
const processChanges = async () => {
|
|
224
|
+
if (pendingChanges.size === 0) return;
|
|
225
|
+
for (const filepath of pendingChanges) {
|
|
226
|
+
const relativePath = path.relative(docsDir, filepath);
|
|
227
|
+
log.file(relativePath);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
await syncContent(docsDir, contentDir);
|
|
231
|
+
await _onSync();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.error(`Sync failed: ${error}`);
|
|
234
|
+
}
|
|
235
|
+
pendingChanges.clear();
|
|
236
|
+
};
|
|
237
|
+
watcher.on("all", (event, filepath) => {
|
|
238
|
+
if (event === "change" || event === "add" || event === "unlink") {
|
|
239
|
+
pendingChanges.add(filepath);
|
|
240
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
241
|
+
debounceTimer = setTimeout(processChanges, 300);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return watcher;
|
|
245
|
+
}
|
|
246
|
+
function showHelp() {
|
|
247
|
+
console.log(`
|
|
248
|
+
${colors.bright}${colors.cyan}\u25C6 Radiant${colors.reset} v${VERSION}
|
|
249
|
+
|
|
250
|
+
${colors.bright}Usage:${colors.reset}
|
|
251
|
+
radiant Start the dev server
|
|
252
|
+
radiant dev Start the dev server
|
|
253
|
+
radiant --help Show this help message
|
|
254
|
+
radiant --version Show version
|
|
255
|
+
|
|
256
|
+
${colors.bright}Description:${colors.reset}
|
|
257
|
+
Run this command from your documentation folder (containing docs.json)
|
|
258
|
+
to preview your docs locally with hot reload.
|
|
259
|
+
`);
|
|
260
|
+
}
|
|
261
|
+
async function run() {
|
|
262
|
+
const args = process.argv.slice(2);
|
|
263
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
264
|
+
showHelp();
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
268
|
+
console.log(VERSION);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
const command = args[0];
|
|
272
|
+
if (command && command !== "dev") {
|
|
273
|
+
log.error(`Unknown command: ${command}`);
|
|
274
|
+
showHelp();
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
log.blank();
|
|
278
|
+
console.log(
|
|
279
|
+
`${colors.bright}${colors.cyan}\u25C6 Radiant${colors.reset} ${colors.dim}v${VERSION}${colors.reset}`
|
|
280
|
+
);
|
|
281
|
+
log.blank();
|
|
282
|
+
const docsDir = getUserDocsDir();
|
|
283
|
+
const cacheDir = getCacheDir(docsDir);
|
|
284
|
+
const contentDir = getContentDir(cacheDir);
|
|
285
|
+
await validateUserDocs(docsDir);
|
|
286
|
+
const needsInstall = await setupCache(cacheDir);
|
|
287
|
+
if (needsInstall) {
|
|
288
|
+
await installDependencies(cacheDir);
|
|
289
|
+
}
|
|
290
|
+
log.info("Syncing your documentation...");
|
|
291
|
+
await syncContent(docsDir, contentDir);
|
|
292
|
+
await runAstroSync(cacheDir);
|
|
293
|
+
log.success("Content synced.");
|
|
294
|
+
log.info("Starting dev server...");
|
|
295
|
+
const devServer = startDevServer(cacheDir);
|
|
296
|
+
const watcher = watchUserContent(docsDir, contentDir, async () => {
|
|
297
|
+
await runAstroSync(cacheDir);
|
|
298
|
+
});
|
|
299
|
+
const cleanup = () => {
|
|
300
|
+
log.blank();
|
|
301
|
+
log.info("Shutting down...");
|
|
302
|
+
watcher.close();
|
|
303
|
+
devServer.kill();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
};
|
|
306
|
+
process.on("SIGINT", cleanup);
|
|
307
|
+
process.on("SIGTERM", cleanup);
|
|
308
|
+
}
|
|
309
|
+
run().catch((error) => {
|
|
310
|
+
log.error(error.message || String(error));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radiant-docs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for previewing Radiant documentation locally",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"radiant": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
11
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
12
|
+
"prepublishOnly": "npm run build && npm run bundle-template",
|
|
13
|
+
"bundle-template": "node scripts/bundle-template.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chokidar": "^4.0.3",
|
|
17
|
+
"env-paths": "^3.0.0",
|
|
18
|
+
"fs-extra": "^11.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/fs-extra": "^11.0.4",
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"template"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"documentation",
|
|
32
|
+
"docs",
|
|
33
|
+
"cli",
|
|
34
|
+
"radiant",
|
|
35
|
+
"astro"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { defineConfig } from "astro/config";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import mdx from "@astrojs/mdx";
|
|
5
|
+
import alpine from "@astrojs/alpinejs";
|
|
6
|
+
import icon from "astro-icon";
|
|
7
|
+
import expressiveCode from "astro-expressive-code";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { getConfig, validateMdxContent } from "./src/lib/validation";
|
|
11
|
+
|
|
12
|
+
if (process.env.npm_lifecycle_event === "build") {
|
|
13
|
+
// Run validation for build before Astro's internal validation
|
|
14
|
+
try {
|
|
15
|
+
await getConfig();
|
|
16
|
+
await validateMdxContent();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(error); // Log our [USER_ERROR] message
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Recursively copy all image files from src/content/docs to public
|
|
24
|
+
// This runs in both dev and prod to ensure user assets are always available
|
|
25
|
+
function copyContentAssets() {
|
|
26
|
+
const CWD = process.cwd();
|
|
27
|
+
const DOCS_DIR = path.join(CWD, "src/content/docs");
|
|
28
|
+
const PUBLIC_DIR = path.join(CWD, "public");
|
|
29
|
+
|
|
30
|
+
const imageExtensions = [
|
|
31
|
+
".svg",
|
|
32
|
+
".png",
|
|
33
|
+
".jpg",
|
|
34
|
+
".jpeg",
|
|
35
|
+
".webp",
|
|
36
|
+
".gif",
|
|
37
|
+
".avif",
|
|
38
|
+
".ico",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function shouldCopyFile(filePath) {
|
|
42
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
43
|
+
return imageExtensions.includes(ext);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build a set of all image file paths in src/content/docs
|
|
47
|
+
function collectImagePaths(dir, baseDir, pathSet) {
|
|
48
|
+
if (!fs.existsSync(dir)) return;
|
|
49
|
+
|
|
50
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const entryPath = path.join(dir, entry.name);
|
|
54
|
+
|
|
55
|
+
// Skip certain files/directories
|
|
56
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
57
|
+
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
collectImagePaths(entryPath, baseDir, pathSet);
|
|
60
|
+
} else if (entry.isFile() && shouldCopyFile(entryPath)) {
|
|
61
|
+
// Store relative path from baseDir
|
|
62
|
+
const relativePath = path.relative(baseDir, entryPath);
|
|
63
|
+
pathSet.add(relativePath.replace(/\\/g, "/")); // Normalize path separators
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Single-pass copy: returns true if any files were copied (or subdirectories contain files)
|
|
69
|
+
function copyDirectory(srcDir, destDir) {
|
|
70
|
+
if (!fs.existsSync(srcDir)) return false;
|
|
71
|
+
|
|
72
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
73
|
+
let hasCopiedFiles = false;
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
77
|
+
const destPath = path.join(destDir, entry.name);
|
|
78
|
+
|
|
79
|
+
// Skip certain files/directories
|
|
80
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
81
|
+
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
// Recursively copy subdirectory - only create if it contains images
|
|
84
|
+
const subDirHasFiles = copyDirectory(srcPath, destPath);
|
|
85
|
+
if (subDirHasFiles) {
|
|
86
|
+
hasCopiedFiles = true;
|
|
87
|
+
// Create directory only if we actually copied something into it
|
|
88
|
+
if (!fs.existsSync(destPath)) {
|
|
89
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (entry.isFile() && shouldCopyFile(srcPath)) {
|
|
93
|
+
// Ensure destination directory exists before copying
|
|
94
|
+
if (!fs.existsSync(destDir)) {
|
|
95
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
fs.copyFileSync(srcPath, destPath);
|
|
98
|
+
hasCopiedFiles = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return hasCopiedFiles;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Delete files in public that don't exist in the source set
|
|
106
|
+
function deleteOrphanedFiles(publicDir, baseDir, validPaths) {
|
|
107
|
+
if (!fs.existsSync(publicDir)) return;
|
|
108
|
+
|
|
109
|
+
const entries = fs.readdirSync(publicDir, { withFileTypes: true });
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const entryPath = path.join(publicDir, entry.name);
|
|
113
|
+
const relativePath = path
|
|
114
|
+
.relative(baseDir, entryPath)
|
|
115
|
+
.replace(/\\/g, "/");
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
// Recursively check subdirectories
|
|
119
|
+
deleteOrphanedFiles(entryPath, baseDir, validPaths);
|
|
120
|
+
} else if (entry.isFile() && shouldCopyFile(entryPath)) {
|
|
121
|
+
// Delete if not in valid paths set
|
|
122
|
+
if (!validPaths.has(relativePath)) {
|
|
123
|
+
fs.unlinkSync(entryPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cleanupEmptyDirs(dir, baseDir) {
|
|
130
|
+
if (!fs.existsSync(dir)) return;
|
|
131
|
+
|
|
132
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const entryPath = path.join(dir, entry.name);
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
cleanupEmptyDirs(entryPath, baseDir);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// After processing children, check if this directory is empty
|
|
142
|
+
const remainingEntries = fs.readdirSync(dir);
|
|
143
|
+
if (remainingEntries.length === 0 && dir !== baseDir) {
|
|
144
|
+
fs.rmdirSync(dir);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Main sync function
|
|
149
|
+
function syncAssets() {
|
|
150
|
+
if (!fs.existsSync(DOCS_DIR)) return;
|
|
151
|
+
|
|
152
|
+
// Step 1: Collect all valid image paths from source
|
|
153
|
+
const validImagePaths = new Set();
|
|
154
|
+
collectImagePaths(DOCS_DIR, DOCS_DIR, validImagePaths);
|
|
155
|
+
|
|
156
|
+
// Step 2: Copy all images from source to public
|
|
157
|
+
copyDirectory(DOCS_DIR, PUBLIC_DIR);
|
|
158
|
+
|
|
159
|
+
// Step 3: Delete orphaned files in public
|
|
160
|
+
deleteOrphanedFiles(PUBLIC_DIR, PUBLIC_DIR, validImagePaths);
|
|
161
|
+
|
|
162
|
+
// Step 4: Clean up empty directories
|
|
163
|
+
cleanupEmptyDirs(PUBLIC_DIR, PUBLIC_DIR);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
name: "copy-content-assets",
|
|
168
|
+
buildStart() {
|
|
169
|
+
// Always copy in both dev and prod
|
|
170
|
+
try {
|
|
171
|
+
if (fs.existsSync(DOCS_DIR)) {
|
|
172
|
+
syncAssets();
|
|
173
|
+
console.log("✅ Copied content assets to public folder");
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn("Failed to copy content assets:", error);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
// Also watch for changes in dev mode
|
|
180
|
+
configureServer(server) {
|
|
181
|
+
const watcher = server.watcher;
|
|
182
|
+
watcher.on("change", (file) => {
|
|
183
|
+
if (file.includes("src/content/docs") && shouldCopyFile(file)) {
|
|
184
|
+
const relativePath = path.relative(
|
|
185
|
+
path.join(CWD, "src/content/docs"),
|
|
186
|
+
file
|
|
187
|
+
);
|
|
188
|
+
const destPath = path.join(PUBLIC_DIR, relativePath);
|
|
189
|
+
const destDir = path.dirname(destPath);
|
|
190
|
+
|
|
191
|
+
if (!fs.existsSync(destDir)) {
|
|
192
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
fs.copyFileSync(file, destPath);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// https://astro.build/config
|
|
202
|
+
export default defineConfig({
|
|
203
|
+
vite: {
|
|
204
|
+
plugins: [tailwindcss(), copyContentAssets()],
|
|
205
|
+
},
|
|
206
|
+
output: "static",
|
|
207
|
+
build: {
|
|
208
|
+
format: "directory",
|
|
209
|
+
},
|
|
210
|
+
integrations: [
|
|
211
|
+
expressiveCode(),
|
|
212
|
+
mdx(),
|
|
213
|
+
alpine({ entrypoint: "/src/entrypoint" }),
|
|
214
|
+
icon(),
|
|
215
|
+
],
|
|
216
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineEcConfig } from "astro-expressive-code";
|
|
2
|
+
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
|
3
|
+
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
|
|
4
|
+
import { pluginFileIcons } from "@xt0rted/expressive-code-file-icons";
|
|
5
|
+
|
|
6
|
+
export default defineEcConfig({
|
|
7
|
+
themes: ["min-dark", "min-light"],
|
|
8
|
+
// Customize as needed:
|
|
9
|
+
styleOverrides: {
|
|
10
|
+
borderRadius: "12px",
|
|
11
|
+
borderColor: "var(--color-neutral-200)",
|
|
12
|
+
borderWidth: "1px",
|
|
13
|
+
codeFontSize: "13px",
|
|
14
|
+
frames: {
|
|
15
|
+
frameBoxShadowCssValue: "var(--shadow-xs)",
|
|
16
|
+
inlineButtonBorder: "var(--color-neutral-200)",
|
|
17
|
+
inlineButtonBorderOpacity: "1",
|
|
18
|
+
copyIcon: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/svg%3E")`,
|
|
19
|
+
},
|
|
20
|
+
lineNumbers: {
|
|
21
|
+
foreground: "var(--color-neutral-400)",
|
|
22
|
+
},
|
|
23
|
+
textMarkers: {
|
|
24
|
+
markBackground: "var(--color-neutral-100)",
|
|
25
|
+
},
|
|
26
|
+
collapsibleSections: {
|
|
27
|
+
closedBackgroundColor: "var(--color-neutral-200)",
|
|
28
|
+
openBackgroundColor: "var(--color-neutral-100)",
|
|
29
|
+
openBackgroundColorCollapsible: "var(--color-neutral-100)",
|
|
30
|
+
collapseIcon: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 22v-6'/%3E%3Cpath d='M12 8V2'/%3E%3Cpath d='M4 12H2'/%3E%3Cpath d='M10 12H8'/%3E%3Cpath d='M16 12h-2'/%3E%3Cpath d='M22 12h-2'/%3E%3Cpath d='m15 19-3-3-3 3'/%3E%3Cpath d='m15 5-3 3-3-3'/%3E%3C/svg%3E")`,
|
|
31
|
+
expandIcon: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 22v-6'/%3E%3Cpath d='M12 8V2'/%3E%3Cpath d='M4 12H2'/%3E%3Cpath d='M10 12H8'/%3E%3Cpath d='M16 12h-2'/%3E%3Cpath d='M22 12h-2'/%3E%3Cpath d='m15 19-3 3-3-3'/%3E%3Cpath d='m15 5-3-3-3 3'/%3E%3C/svg%3E")`,
|
|
32
|
+
closedTextColor: "var(--color-neutral-500)",
|
|
33
|
+
},
|
|
34
|
+
// codeFontFamily: '"JetBrains Mono", monospace',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
plugins: [
|
|
38
|
+
pluginLineNumbers(),
|
|
39
|
+
pluginCollapsibleSections(),
|
|
40
|
+
// @ts-ignore - plugin types not fully compatible
|
|
41
|
+
pluginFileIcons({
|
|
42
|
+
iconClass: "size-4",
|
|
43
|
+
titleClass: "!flex !items-center !gap-1",
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
defaultProps: {
|
|
47
|
+
wrap: false,
|
|
48
|
+
collapseStyle: "collapsible-auto",
|
|
49
|
+
showLineNumbers: false,
|
|
50
|
+
},
|
|
51
|
+
});
|