portosaurus 2.0.2 → 2.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/bin/portosaurus.mjs +14 -327
- package/package.json +16 -11
- package/src/cli/build.mjs +43 -0
- package/src/cli/dev.mjs +31 -0
- package/src/cli/init.mjs +135 -0
- package/src/cli/serve.mjs +30 -0
- package/src/core/buildDocuConfig.mjs +664 -0
- package/src/core/{themePlugin.mjs → plugins/themePlugin.mjs} +1 -1
- package/src/template/.github/workflows/deploy.yml +52 -0
- package/src/template/.nojekyll +0 -0
- package/src/template/README.md +58 -0
- package/src/template/blog/authors.yml +1 -1
- package/src/template/blog/welcome.md +1 -1
- package/src/template/config.js +40 -23
- package/src/template/package.json +20 -0
- package/src/template/static/img/svg/icon-blog.svg +2 -0
- package/src/template/static/img/svg/icon-note.svg +2 -0
- package/src/{components → theme/components}/AboutSection/index.js +22 -13
- package/src/{components → theme/components}/AboutSection/styles.module.css +59 -48
- package/src/{components → theme/components}/ContactSection/index.js +31 -24
- package/src/{components → theme/components}/ContactSection/styles.module.css +31 -26
- package/src/{components → theme/components}/ExperienceSection/index.js +12 -7
- package/src/{components → theme/components}/ExperienceSection/styles.module.css +23 -20
- package/src/{components → theme/components}/HeroSection/index.js +9 -11
- package/src/{components → theme/components}/HeroSection/styles.module.css +44 -32
- package/src/{components → theme/components}/NoteIndex/index.js +10 -3
- package/src/{components → theme/components}/Preview/components/PreviewHeader.js +14 -8
- package/src/{components → theme/components}/Preview/components/Triggers/Pv.js +32 -7
- package/src/{components → theme/components}/Preview/components/Triggers/SrcPv.js +1 -5
- package/src/theme/components/Preview/index.js +3 -0
- package/src/{components → theme/components}/ProjectsSection/index.js +279 -224
- package/src/{components → theme/components}/ProjectsSection/styles.module.css +21 -17
- package/src/{components → theme/components}/ScrollToTop/index.js +18 -21
- package/src/{components → theme/components}/ScrollToTop/styles.module.css +10 -9
- package/src/theme/components/SocialLinks/index.js +125 -0
- package/src/{components → theme/components}/SocialLinks/styles.module.css +9 -7
- package/src/{components → theme/components}/Tooltip/index.js +4 -1
- package/src/theme/config/iconMappings.js +465 -0
- package/src/theme/config/metaTags.js +239 -0
- package/src/theme/config/prism.js +179 -0
- package/src/theme/config/sidebar.js +17 -0
- package/src/{css → theme/css}/bootstrap.css +0 -1
- package/src/theme/css/catppuccin.css +618 -0
- package/src/{css → theme/css}/custom.css +3 -9
- package/src/{css → theme/css}/tasks.css +43 -37
- package/src/theme/{MDXComponents.js → overrides/MDXComponents.js} +3 -3
- package/src/theme/{Root.js → overrides/Root.js} +2 -4
- package/src/{pages → theme/pages}/index.js +23 -39
- package/src/theme/pages/notes.js +83 -0
- package/src/{pages → theme/pages}/tasks.js +115 -56
- package/src/{core/client-utils → theme/utils}/HashNavigation.js +60 -49
- package/src/{core/client-utils → theme/utils}/updateTitle.js +21 -25
- package/src/{core/build-utils → utils/build}/cssUtils.mjs +5 -3
- package/src/{core/build-utils → utils/build}/generateFavicon.mjs +44 -12
- package/src/{core/build-utils → utils/build}/generateRobotsTxt.mjs +4 -3
- package/src/{core/build-utils → utils/build}/iconExtractor.mjs +7 -3
- package/src/utils/build/imageDownloader.mjs +159 -0
- package/src/{core/build-utils → utils/build}/imageProcessor.mjs +5 -6
- package/src/utils/helpers.mjs +153 -0
- package/src/utils/logger.mjs +53 -0
- package/src/utils/packageManager.mjs +88 -0
- package/src/components/Preview/index.js +0 -3
- package/src/components/SocialLinks/index.js +0 -130
- package/src/config/iconMappings.js +0 -329
- package/src/config/metaTags.js +0 -240
- package/src/config/prism.js +0 -179
- package/src/config/sidebar.js +0 -20
- package/src/core/build-utils/imageDownloader.mjs +0 -98
- package/src/core/createDocuConf.mjs +0 -490
- package/src/core/defaults.mjs +0 -67
- package/src/core/logger.mjs +0 -17
- package/src/core/packageManager.mjs +0 -72
- package/src/css/catppuccin.css +0 -632
- package/src/pages/notes.js +0 -87
- /package/src/template/notes/{welcome.md → welcome.mdx} +0 -0
- /package/src/{components → theme/components}/NoteIndex/styles.module.css +0 -0
- /package/src/{components → theme/components}/Preview/components/FeedbackStates.js +0 -0
- /package/src/{components → theme/components}/Preview/components/FileTabs.js +0 -0
- /package/src/{components → theme/components}/Preview/components/Triggers/index.js +0 -0
- /package/src/{components → theme/components}/Preview/components/ViewerWindow.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useDeepLinkHash.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useDockLayout.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useFileFetch.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/CodeRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/ImageRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/PdfRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/WebRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/state/index.js +0 -0
- /package/src/{components → theme/components}/Preview/styles.module.css +0 -0
- /package/src/{components → theme/components}/Preview/utils/index.js +0 -0
- /package/src/{components → theme/components}/Tooltip/styles.module.css +0 -0
|
@@ -6,6 +6,7 @@ import { downloadImage } from "./imageDownloader.mjs";
|
|
|
6
6
|
import { reshapeImage } from "./imageProcessor.mjs";
|
|
7
7
|
import { getCssVar } from "./cssUtils.mjs";
|
|
8
8
|
import { extractSvg } from "./iconExtractor.mjs";
|
|
9
|
+
import { logger } from "../logger.mjs";
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
|
|
@@ -40,7 +41,7 @@ function processManifest(manifestFile, outputDir, appVersion) {
|
|
|
40
41
|
);
|
|
41
42
|
return true;
|
|
42
43
|
} catch (err) {
|
|
43
|
-
|
|
44
|
+
logger.error(`Failed to process manifest: ${err.message}`);
|
|
44
45
|
fs.writeFileSync(
|
|
45
46
|
path.join(outputDir, manifestFile.name),
|
|
46
47
|
manifestFile.contents,
|
|
@@ -50,7 +51,7 @@ function processManifest(manifestFile, outputDir, appVersion) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export async function generateFavicons(context, options = {}) {
|
|
53
|
-
|
|
54
|
+
logger.info("Generating favicons...");
|
|
54
55
|
|
|
55
56
|
const { siteConfig } = context;
|
|
56
57
|
const profilePicUrl =
|
|
@@ -62,14 +63,42 @@ export async function generateFavicons(context, options = {}) {
|
|
|
62
63
|
|
|
63
64
|
const staticBaseDir = path.resolve(context.siteDir, "static");
|
|
64
65
|
const imgDir = path.join(staticBaseDir, "img", "svg");
|
|
65
|
-
const imgStaticPath = "/img/svg";
|
|
66
66
|
const outputDir = path.join(staticBaseDir, options.outputPath || "favicon");
|
|
67
67
|
|
|
68
|
+
// --- Smart Caching ---
|
|
69
|
+
const configHash = Buffer.from(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
profilePicUrl,
|
|
72
|
+
shape,
|
|
73
|
+
circular,
|
|
74
|
+
outputPath: options.outputPath,
|
|
75
|
+
}),
|
|
76
|
+
).toString("base64");
|
|
77
|
+
|
|
78
|
+
const hashFilePath = path.join(outputDir, ".favicon.hash");
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(hashFilePath)) {
|
|
81
|
+
const existingHash = fs.readFileSync(hashFilePath, "utf-8");
|
|
82
|
+
|
|
83
|
+
if (existingHash === configHash) {
|
|
84
|
+
// Check if critical files actually exist
|
|
85
|
+
if (fs.existsSync(path.join(outputDir, "favicon.ico"))) {
|
|
86
|
+
logger.success("Favicons are up to date, skipping generation.");
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ---------------------
|
|
92
|
+
|
|
68
93
|
// Use a consolidated cache directory inside .docusaurus
|
|
69
|
-
const cacheDir = path.join(
|
|
94
|
+
const cacheDir = path.join(
|
|
95
|
+
context.siteDir,
|
|
96
|
+
".docusaurus",
|
|
97
|
+
"portosaurus",
|
|
98
|
+
"cache",
|
|
99
|
+
);
|
|
70
100
|
createDirectoryIfNotExists(cacheDir);
|
|
71
101
|
|
|
72
|
-
const tempProfilePic = path.join(cacheDir, "profile_pic_src.png");
|
|
73
102
|
const reshapedImagePath = path.join(cacheDir, "profile_pic_reshaped.png");
|
|
74
103
|
|
|
75
104
|
const tempFiles = [];
|
|
@@ -113,12 +142,12 @@ export async function generateFavicons(context, options = {}) {
|
|
|
113
142
|
},
|
|
114
143
|
};
|
|
115
144
|
|
|
116
|
-
// 1. Download image with proxy support
|
|
145
|
+
// 1. Download image with proxy support + caching
|
|
117
146
|
const downloadedRes = await downloadImage(
|
|
118
147
|
profilePicUrl,
|
|
119
148
|
cacheDir,
|
|
120
149
|
"profile_pic_src.png",
|
|
121
|
-
{ proxies }
|
|
150
|
+
{ proxies, cacheDir: path.join(cacheDir, "downloads") },
|
|
122
151
|
);
|
|
123
152
|
tempFiles.push(downloadedRes);
|
|
124
153
|
|
|
@@ -135,7 +164,7 @@ export async function generateFavicons(context, options = {}) {
|
|
|
135
164
|
|
|
136
165
|
createDirectoryIfNotExists(outputDir);
|
|
137
166
|
|
|
138
|
-
|
|
167
|
+
logger.info(`Generating favicon assets from ${finalImagePath}`);
|
|
139
168
|
const response = await favicons(finalImagePath, configuration);
|
|
140
169
|
|
|
141
170
|
let imageCount = 0,
|
|
@@ -159,15 +188,18 @@ export async function generateFavicons(context, options = {}) {
|
|
|
159
188
|
}
|
|
160
189
|
}
|
|
161
190
|
|
|
162
|
-
|
|
163
|
-
`
|
|
191
|
+
logger.success(
|
|
192
|
+
`Generated ${imageCount} favicon images and ${fileCount} support files`,
|
|
164
193
|
);
|
|
165
|
-
|
|
194
|
+
|
|
195
|
+
// Write the hash file to enable smart caching next time
|
|
196
|
+
fs.writeFileSync(hashFilePath, configHash, "utf-8");
|
|
197
|
+
|
|
166
198
|
// Cleanup temporary files
|
|
167
199
|
tempFiles.forEach(cleanupFile);
|
|
168
200
|
return true;
|
|
169
201
|
} catch (error) {
|
|
170
|
-
|
|
202
|
+
logger.warn(`Favicon generation skipped: ${error.message}`);
|
|
171
203
|
tempFiles.forEach(cleanupFile);
|
|
172
204
|
// Don't throw - allow the build to continue
|
|
173
205
|
return false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { logger } from "../logger.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Generates a robots.txt file in the site's static directory.
|
|
@@ -12,7 +13,7 @@ export function generateRobotsTxt(context) {
|
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
logger.info("Generating robots.txt...");
|
|
16
17
|
|
|
17
18
|
const staticDir = path.resolve(context.siteDir, "static");
|
|
18
19
|
const robotsPath = path.join(staticDir, "robots.txt");
|
|
@@ -46,7 +47,7 @@ export function generateRobotsTxt(context) {
|
|
|
46
47
|
|
|
47
48
|
fs.mkdirSync(staticDir, { recursive: true });
|
|
48
49
|
fs.writeFileSync(robotsPath, content);
|
|
49
|
-
|
|
50
|
+
logger.success(`Generated robots.txt at ${robotsPath}`);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export default function robotsTxtPlugin(context, options) {
|
|
@@ -56,4 +57,4 @@ export default function robotsTxtPlugin(context, options) {
|
|
|
56
57
|
generateRobotsTxt(context);
|
|
57
58
|
},
|
|
58
59
|
};
|
|
59
|
-
}
|
|
60
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { logger } from "../logger.mjs";
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
|
|
@@ -8,11 +9,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
8
9
|
* Extracts specific SVGs from the internal assets and saves them to the project.
|
|
9
10
|
*/
|
|
10
11
|
export async function extractSvg(iconName, destDir, options = {}) {
|
|
11
|
-
const srcPath = path.resolve(
|
|
12
|
+
const srcPath = path.resolve(
|
|
13
|
+
__dirname,
|
|
14
|
+
`../../assets/img/svg/icon-${iconName}.svg`,
|
|
15
|
+
);
|
|
12
16
|
const destPath = path.join(destDir, `icon-${iconName}.svg`);
|
|
13
17
|
|
|
14
18
|
if (!fs.existsSync(srcPath)) {
|
|
15
|
-
|
|
19
|
+
logger.warn(`Source icon not found: ${srcPath}`);
|
|
16
20
|
return false;
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -26,6 +30,6 @@ export async function extractSvg(iconName, destDir, options = {}) {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
fs.writeFileSync(destPath, content);
|
|
29
|
-
|
|
33
|
+
logger.info(`Generated SVG icon: ${destPath}`);
|
|
30
34
|
return destPath;
|
|
31
35
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import https from "https";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { logger } from "../logger.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a short hash from a URL for use as a cache key.
|
|
10
|
+
*/
|
|
11
|
+
function getCacheKey(url) {
|
|
12
|
+
return crypto.createHash("md5").update(url).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Downloads an image from a URL and saves it to a local file.
|
|
17
|
+
* Handles redirects (up to 5 levels), falls back to proxies if needed,
|
|
18
|
+
* and supports URL-hash-based caching to avoid redundant downloads.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} url - The URL to download from.
|
|
21
|
+
* @param {string} destDir - Directory to save the downloaded file.
|
|
22
|
+
* @param {string} fileName - Name for the downloaded file.
|
|
23
|
+
* @param {Object} options - Additional options.
|
|
24
|
+
* @param {string[]} options.proxies - List of CORS proxy URLs.
|
|
25
|
+
* @param {number} options.redirectCount - Internal redirect counter.
|
|
26
|
+
* @param {string} options.cacheDir - Directory for caching downloads.
|
|
27
|
+
* @param {number} options.cacheTTL - Cache time-to-live in ms (default: 24h).
|
|
28
|
+
*/
|
|
29
|
+
export async function downloadImage(url, destDir, fileName, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
proxies = [],
|
|
32
|
+
redirectCount = 0,
|
|
33
|
+
cacheDir,
|
|
34
|
+
cacheTTL = 43200000, // 6 hours
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
if (redirectCount > 5) {
|
|
38
|
+
throw new Error("Too many redirects");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const destPath = path.join(destDir, fileName);
|
|
42
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
// ─── Cache Check ───────────────────────────────────────────
|
|
45
|
+
const forceDownload = process.env.PORTO_FORCE_DOWNLOAD === "1";
|
|
46
|
+
|
|
47
|
+
if (cacheDir && !forceDownload) {
|
|
48
|
+
const cacheKey = getCacheKey(url);
|
|
49
|
+
const cachedPath = path.join(cacheDir, `${cacheKey}-${fileName}`);
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(cachedPath)) {
|
|
52
|
+
const stats = fs.statSync(cachedPath);
|
|
53
|
+
const age = Date.now() - stats.mtimeMs;
|
|
54
|
+
|
|
55
|
+
if (age < cacheTTL) {
|
|
56
|
+
logger.info(`Using cached image (${Math.round(age / 60000)}m old)`);
|
|
57
|
+
fs.copyFileSync(cachedPath, destPath);
|
|
58
|
+
return destPath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Download ──────────────────────────────────────────────
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
66
|
+
|
|
67
|
+
protocol
|
|
68
|
+
.get(url, (response) => {
|
|
69
|
+
// Handle redirects
|
|
70
|
+
if (
|
|
71
|
+
[301, 302, 303, 307, 308].includes(response.statusCode) &&
|
|
72
|
+
response.headers.location
|
|
73
|
+
) {
|
|
74
|
+
const redirectUrl = new URL(
|
|
75
|
+
response.headers.location,
|
|
76
|
+
url,
|
|
77
|
+
).toString();
|
|
78
|
+
logger.info(
|
|
79
|
+
`Following redirect (${response.statusCode}) to: ${redirectUrl}`,
|
|
80
|
+
);
|
|
81
|
+
resolve(
|
|
82
|
+
downloadImage(redirectUrl, destDir, fileName, {
|
|
83
|
+
...options,
|
|
84
|
+
redirectCount: redirectCount + 1,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle errors and non-image content
|
|
91
|
+
if (response.statusCode !== 200) {
|
|
92
|
+
tryProxyFallback();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const contentType = response.headers["content-type"] || "";
|
|
97
|
+
if (!contentType.startsWith("image/") && !url.includes("raw=true")) {
|
|
98
|
+
logger.warn(`Expected image but got ${contentType} from ${url}`);
|
|
99
|
+
tryProxyFallback();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const file = fs.createWriteStream(destPath);
|
|
104
|
+
response.pipe(file);
|
|
105
|
+
|
|
106
|
+
file.on("finish", () => {
|
|
107
|
+
file.close((err) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
reject(err);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write to cache after successful download
|
|
114
|
+
if (cacheDir) {
|
|
115
|
+
try {
|
|
116
|
+
const cacheKey = getCacheKey(url);
|
|
117
|
+
const cachedPath = path.join(
|
|
118
|
+
cacheDir,
|
|
119
|
+
`${cacheKey}-${fileName}`,
|
|
120
|
+
);
|
|
121
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
122
|
+
fs.copyFileSync(destPath, cachedPath);
|
|
123
|
+
} catch {
|
|
124
|
+
// Cache write failure is non-fatal
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resolve(destPath);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
file.on("error", (err) => {
|
|
133
|
+
fs.unlink(destPath, () => reject(err));
|
|
134
|
+
});
|
|
135
|
+
})
|
|
136
|
+
.on("error", (err) => {
|
|
137
|
+
tryProxyFallback(err);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function tryProxyFallback(originalError) {
|
|
141
|
+
if (proxies.length > 0) {
|
|
142
|
+
const nextProxy = proxies[0];
|
|
143
|
+
const remainingProxies = proxies.slice(1);
|
|
144
|
+
const proxyUrl = `${nextProxy}${encodeURIComponent(url)}`;
|
|
145
|
+
logger.info(
|
|
146
|
+
`Download failed or invalid content. Retrying via proxy: ${nextProxy}`,
|
|
147
|
+
);
|
|
148
|
+
resolve(
|
|
149
|
+
downloadImage(proxyUrl, destDir, fileName, {
|
|
150
|
+
...options,
|
|
151
|
+
proxies: remainingProxies,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
reject(originalError || new Error(`Failed to download image: ${url}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import sharp from "sharp";
|
|
4
|
+
import { logger } from "../logger.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Reshapes an image into a circle or square with rounded corners.
|
|
@@ -19,7 +20,7 @@ export async function reshapeImage(inputPath, outputPath, shape = "circle") {
|
|
|
19
20
|
|
|
20
21
|
const image = sharp(inputPath);
|
|
21
22
|
const metadata = await image.metadata();
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
if (!metadata.width || !metadata.height) {
|
|
24
25
|
throw new Error("Could not extract image metadata (width/height)");
|
|
25
26
|
}
|
|
@@ -38,18 +39,16 @@ export async function reshapeImage(inputPath, outputPath, shape = "circle") {
|
|
|
38
39
|
// Create circular mask
|
|
39
40
|
const radius = size / 2;
|
|
40
41
|
const circleSvg = Buffer.from(
|
|
41
|
-
`<svg><circle cx="${radius}" cy="${radius}" r="${radius}" /></svg
|
|
42
|
+
`<svg><circle cx="${radius}" cy="${radius}" r="${radius}" /></svg>`,
|
|
42
43
|
);
|
|
43
44
|
|
|
44
|
-
pipeline = pipeline.composite([
|
|
45
|
-
{ input: circleSvg, blend: "dest-in" }
|
|
46
|
-
]);
|
|
45
|
+
pipeline = pipeline.composite([{ input: circleSvg, blend: "dest-in" }]);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
await pipeline.png().toFile(outputPath);
|
|
50
49
|
return outputPath;
|
|
51
50
|
} catch (err) {
|
|
52
|
-
|
|
51
|
+
logger.error(`Image processing failed: ${err.message}`);
|
|
53
52
|
throw err;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { logger } from "./logger.mjs";
|
|
6
|
+
import { getDocuCmd } from "./packageManager.mjs";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export const PortoRoot = path.resolve(__dirname, "../..");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write the ephemeral .portosaurus/docusaurus.config.mjs shim.
|
|
13
|
+
*/
|
|
14
|
+
export function writePortoConfigShim(UserRoot) {
|
|
15
|
+
const dotDir = path.join(UserRoot, ".docusaurus", "portosaurus");
|
|
16
|
+
fs.mkdirSync(dotDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
const configJsAbsolute = path.join(UserRoot, "config.js").replace(/\\/g, "/");
|
|
19
|
+
const createDocuConfAbsolute = path
|
|
20
|
+
.resolve(PortoRoot, "src/core/buildDocuConfig.mjs")
|
|
21
|
+
.replace(/\\/g, "/");
|
|
22
|
+
|
|
23
|
+
const shimContent = `// Auto-generated by Portosaurus CLI — do not edit
|
|
24
|
+
/** @type {import('@docusaurus/types').Config} */
|
|
25
|
+
export default async function getConfig() {
|
|
26
|
+
const { buildDocuConfig } = await import("file://${createDocuConfAbsolute}");
|
|
27
|
+
const { usrConf } = await import("file://${configJsAbsolute}");
|
|
28
|
+
return buildDocuConfig(usrConf, "${UserRoot.replace(/\\/g, "/")}");
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const shimPath = path.join(dotDir, "docusaurus.config.js");
|
|
33
|
+
fs.writeFileSync(shimPath, shimContent);
|
|
34
|
+
logger.debug(`Config shim written to ${shimPath}`);
|
|
35
|
+
|
|
36
|
+
return shimPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate that the current directory is a Portosaurus project.
|
|
41
|
+
*/
|
|
42
|
+
export function validateProject(UserRoot) {
|
|
43
|
+
const configPath = path.join(UserRoot, "config.js");
|
|
44
|
+
if (!fs.existsSync(configPath)) {
|
|
45
|
+
logger.log("");
|
|
46
|
+
logger.error(
|
|
47
|
+
"config.js not found. Are you in a Portosaurus project directory?",
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ensure essential user content directories exist.
|
|
55
|
+
*/
|
|
56
|
+
export function ensureContentDirs(UserRoot) {
|
|
57
|
+
for (const dir of ["notes", "blog", "static"]) {
|
|
58
|
+
fs.mkdirSync(path.join(UserRoot, dir), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const notesIndex = path.join(UserRoot, "notes", "index.mdx");
|
|
62
|
+
if (!fs.existsSync(notesIndex)) {
|
|
63
|
+
fs.writeFileSync(
|
|
64
|
+
notesIndex,
|
|
65
|
+
`---\nhide_table_of_contents: true\n---\n\nimport NoteCards from "portosaurus/src/theme/components/NoteIndex/index.js";\n\n# Notes\n\n<NoteCards />\n`,
|
|
66
|
+
);
|
|
67
|
+
logger.debug("Created default notes/index.mdx");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Spawn a Docusaurus command and wait for it to complete.
|
|
73
|
+
*/
|
|
74
|
+
export function runDocusaurus(command, extraArgs, UserRoot, configPath) {
|
|
75
|
+
const {
|
|
76
|
+
command: cmd,
|
|
77
|
+
args: pmArgs,
|
|
78
|
+
packageManager,
|
|
79
|
+
} = getDocuCmd(UserRoot);
|
|
80
|
+
|
|
81
|
+
logger.info(`Running docusaurus ${command} (via ${packageManager})`);
|
|
82
|
+
|
|
83
|
+
const finalArgs = [
|
|
84
|
+
...pmArgs,
|
|
85
|
+
command,
|
|
86
|
+
UserRoot,
|
|
87
|
+
"--config",
|
|
88
|
+
configPath,
|
|
89
|
+
...extraArgs,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const child = spawn(cmd, finalArgs, {
|
|
93
|
+
stdio: "inherit",
|
|
94
|
+
cwd: UserRoot,
|
|
95
|
+
env: { ...process.env, FORCE_COLOR: "true" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
child.on("error", (err) => {
|
|
100
|
+
logger.error(`Failed to run docusaurus ${command}: ${err.message}`);
|
|
101
|
+
reject(err);
|
|
102
|
+
});
|
|
103
|
+
child.on("close", (code) => {
|
|
104
|
+
if (code === 0) resolve();
|
|
105
|
+
else process.exit(code);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Recursively copy a directory and apply text replacements.
|
|
111
|
+
export function mirrorSync(src, dest, replacements = {}, ignores = []) {
|
|
112
|
+
const ignoreSet = new Set(ignores);
|
|
113
|
+
const textExtensions = new Set([
|
|
114
|
+
".js",
|
|
115
|
+
".mjs",
|
|
116
|
+
".json",
|
|
117
|
+
".md",
|
|
118
|
+
".mdx",
|
|
119
|
+
".yml",
|
|
120
|
+
".yaml",
|
|
121
|
+
".css",
|
|
122
|
+
".html",
|
|
123
|
+
".txt",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
127
|
+
|
|
128
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
129
|
+
if (ignoreSet.has(entry.name)) continue;
|
|
130
|
+
|
|
131
|
+
const srcPath = path.join(src, entry.name);
|
|
132
|
+
const destPath = path.join(dest, entry.name);
|
|
133
|
+
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
mirrorSync(srcPath, destPath, replacements, ignores);
|
|
136
|
+
} else {
|
|
137
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
138
|
+
|
|
139
|
+
// If replacements exist and it's a known text file, process it
|
|
140
|
+
if (Object.keys(replacements).length > 0 && textExtensions.has(ext)) {
|
|
141
|
+
let content = fs.readFileSync(srcPath, "utf8");
|
|
142
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
143
|
+
// Replace all occurrences of {{key}}
|
|
144
|
+
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
145
|
+
content = content.replace(regex, value);
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(destPath, content);
|
|
148
|
+
} else {
|
|
149
|
+
fs.copyFileSync(srcPath, destPath); // Safe copy for binaries (images, etc) or when no replacements needed
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createConsola } from "consola";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Logger
|
|
6
|
+
* Mimics the official Docusaurus CLI style for a familiar DX.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const consola = createConsola({
|
|
10
|
+
level: process.env.DEBUG || process.env.VERBOSE ? 4 : 3,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const format = (badge, color, ...args) => {
|
|
14
|
+
const prefix = color.bold(badge.padEnd(10));
|
|
15
|
+
const message = args
|
|
16
|
+
.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
|
|
17
|
+
.join(" ");
|
|
18
|
+
|
|
19
|
+
message.split("\n").forEach((line) => {
|
|
20
|
+
console.log(`${prefix} ${line}`);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const logger = {
|
|
25
|
+
log: (...args) => console.log(...args),
|
|
26
|
+
info: (...args) => format("[INFO]", chalk.cyan, ...args),
|
|
27
|
+
success: (...args) => format("[SUCCESS]", chalk.green, ...args),
|
|
28
|
+
warn: (...args) => format("[WARNING]", chalk.yellow, ...args),
|
|
29
|
+
error: (...args) => format("[ERROR]", chalk.red, ...args),
|
|
30
|
+
tip: (...args) => format("[TIP]", chalk.magenta, ...args),
|
|
31
|
+
|
|
32
|
+
debug: (...args) => {
|
|
33
|
+
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
34
|
+
format("[DEBUG]", chalk.gray, ...args);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
box: (msg, title = "Portosaurus") => {
|
|
39
|
+
consola.box({
|
|
40
|
+
message: msg,
|
|
41
|
+
title: chalk.magenta.bold(title),
|
|
42
|
+
style: {
|
|
43
|
+
borderColor: "magenta",
|
|
44
|
+
borderStyle: "rounded",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
start: (msg) => consola.start(msg),
|
|
50
|
+
ready: (msg) => consola.ready(msg),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default logger;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package manager detection and CLI utility (ESM)
|
|
3
|
+
*/
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a package manager object for the given directory.
|
|
9
|
+
*/
|
|
10
|
+
export function getPackageManager(dir) {
|
|
11
|
+
let name = null;
|
|
12
|
+
|
|
13
|
+
// Detect by lockfile
|
|
14
|
+
if (
|
|
15
|
+
fs.existsSync(path.join(dir, "bun.lock")) ||
|
|
16
|
+
fs.existsSync(path.join(dir, "bun.lockb"))
|
|
17
|
+
) {
|
|
18
|
+
name = "bun";
|
|
19
|
+
} else if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) {
|
|
20
|
+
name = "pnpm";
|
|
21
|
+
} else if (fs.existsSync(path.join(dir, "yarn.lock"))) {
|
|
22
|
+
name = "yarn";
|
|
23
|
+
} else if (fs.existsSync(path.join(dir, "package-lock.json"))) {
|
|
24
|
+
name = "npm";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Detect by environment
|
|
28
|
+
else if (process.env.npm_config_user_agent) {
|
|
29
|
+
if (process.env.npm_config_user_agent.includes("bun")) name = "bun";
|
|
30
|
+
else if (process.env.npm_config_user_agent.includes("pnpm")) name = "pnpm";
|
|
31
|
+
else if (process.env.npm_config_user_agent.includes("yarn")) name = "yarn";
|
|
32
|
+
else if (process.env.npm_config_user_agent.includes("npm")) name = "npm";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Detect by runtime
|
|
36
|
+
else if (typeof process !== "undefined" && process.versions?.bun) {
|
|
37
|
+
name = "bun";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!name) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Supported package manager (bun, pnpm, yarn, or npm) not detected.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const exec = {
|
|
47
|
+
npm: "npx",
|
|
48
|
+
bun: "bunx",
|
|
49
|
+
pnpm: "pnpm dlx",
|
|
50
|
+
yarn: "yarn dlx",
|
|
51
|
+
}[name];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
install: `${name} install`,
|
|
56
|
+
run: `${name} run`,
|
|
57
|
+
exec,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolves the command to run Docusaurus based on the package manager and project state.
|
|
63
|
+
*/
|
|
64
|
+
export function getDocuCmd(UserRoot) {
|
|
65
|
+
const pm = getPackageManager(UserRoot);
|
|
66
|
+
|
|
67
|
+
// Check for node_modules/.bin/docusaurus
|
|
68
|
+
const localDocusaurus = path.join(
|
|
69
|
+
UserRoot,
|
|
70
|
+
"node_modules",
|
|
71
|
+
".bin",
|
|
72
|
+
"docusaurus",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(localDocusaurus)) {
|
|
76
|
+
const local = {
|
|
77
|
+
bun: { command: "bun", args: ["docusaurus"] },
|
|
78
|
+
npm: { command: "npm", args: ["run", "docusaurus", "--"] },
|
|
79
|
+
pnpm: { command: "pnpm", args: ["docusaurus"] },
|
|
80
|
+
yarn: { command: "yarn", args: ["docusaurus"] },
|
|
81
|
+
}[pm.name];
|
|
82
|
+
|
|
83
|
+
return { ...local, packageManager: pm.name };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback to npx/bunx if not found locally
|
|
87
|
+
return { command: pm.exec, args: ["docusaurus"], packageManager: pm.name };
|
|
88
|
+
}
|