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.
Files changed (91) hide show
  1. package/bin/portosaurus.mjs +14 -327
  2. package/package.json +16 -11
  3. package/src/cli/build.mjs +43 -0
  4. package/src/cli/dev.mjs +31 -0
  5. package/src/cli/init.mjs +135 -0
  6. package/src/cli/serve.mjs +30 -0
  7. package/src/core/buildDocuConfig.mjs +664 -0
  8. package/src/core/{themePlugin.mjs → plugins/themePlugin.mjs} +1 -1
  9. package/src/template/.github/workflows/deploy.yml +52 -0
  10. package/src/template/.nojekyll +0 -0
  11. package/src/template/README.md +58 -0
  12. package/src/template/blog/authors.yml +1 -1
  13. package/src/template/blog/welcome.md +1 -1
  14. package/src/template/config.js +40 -23
  15. package/src/template/package.json +20 -0
  16. package/src/template/static/img/svg/icon-blog.svg +2 -0
  17. package/src/template/static/img/svg/icon-note.svg +2 -0
  18. package/src/{components → theme/components}/AboutSection/index.js +22 -13
  19. package/src/{components → theme/components}/AboutSection/styles.module.css +59 -48
  20. package/src/{components → theme/components}/ContactSection/index.js +31 -24
  21. package/src/{components → theme/components}/ContactSection/styles.module.css +31 -26
  22. package/src/{components → theme/components}/ExperienceSection/index.js +12 -7
  23. package/src/{components → theme/components}/ExperienceSection/styles.module.css +23 -20
  24. package/src/{components → theme/components}/HeroSection/index.js +9 -11
  25. package/src/{components → theme/components}/HeroSection/styles.module.css +44 -32
  26. package/src/{components → theme/components}/NoteIndex/index.js +10 -3
  27. package/src/{components → theme/components}/Preview/components/PreviewHeader.js +14 -8
  28. package/src/{components → theme/components}/Preview/components/Triggers/Pv.js +32 -7
  29. package/src/{components → theme/components}/Preview/components/Triggers/SrcPv.js +1 -5
  30. package/src/theme/components/Preview/index.js +3 -0
  31. package/src/{components → theme/components}/ProjectsSection/index.js +279 -224
  32. package/src/{components → theme/components}/ProjectsSection/styles.module.css +21 -17
  33. package/src/{components → theme/components}/ScrollToTop/index.js +18 -21
  34. package/src/{components → theme/components}/ScrollToTop/styles.module.css +10 -9
  35. package/src/theme/components/SocialLinks/index.js +125 -0
  36. package/src/{components → theme/components}/SocialLinks/styles.module.css +9 -7
  37. package/src/{components → theme/components}/Tooltip/index.js +4 -1
  38. package/src/theme/config/iconMappings.js +465 -0
  39. package/src/theme/config/metaTags.js +239 -0
  40. package/src/theme/config/prism.js +179 -0
  41. package/src/theme/config/sidebar.js +17 -0
  42. package/src/{css → theme/css}/bootstrap.css +0 -1
  43. package/src/theme/css/catppuccin.css +618 -0
  44. package/src/{css → theme/css}/custom.css +3 -9
  45. package/src/{css → theme/css}/tasks.css +43 -37
  46. package/src/theme/{MDXComponents.js → overrides/MDXComponents.js} +3 -3
  47. package/src/theme/{Root.js → overrides/Root.js} +2 -4
  48. package/src/{pages → theme/pages}/index.js +23 -39
  49. package/src/theme/pages/notes.js +83 -0
  50. package/src/{pages → theme/pages}/tasks.js +115 -56
  51. package/src/{core/client-utils → theme/utils}/HashNavigation.js +60 -49
  52. package/src/{core/client-utils → theme/utils}/updateTitle.js +21 -25
  53. package/src/{core/build-utils → utils/build}/cssUtils.mjs +5 -3
  54. package/src/{core/build-utils → utils/build}/generateFavicon.mjs +44 -12
  55. package/src/{core/build-utils → utils/build}/generateRobotsTxt.mjs +4 -3
  56. package/src/{core/build-utils → utils/build}/iconExtractor.mjs +7 -3
  57. package/src/utils/build/imageDownloader.mjs +159 -0
  58. package/src/{core/build-utils → utils/build}/imageProcessor.mjs +5 -6
  59. package/src/utils/helpers.mjs +153 -0
  60. package/src/utils/logger.mjs +53 -0
  61. package/src/utils/packageManager.mjs +88 -0
  62. package/src/components/Preview/index.js +0 -3
  63. package/src/components/SocialLinks/index.js +0 -130
  64. package/src/config/iconMappings.js +0 -329
  65. package/src/config/metaTags.js +0 -240
  66. package/src/config/prism.js +0 -179
  67. package/src/config/sidebar.js +0 -20
  68. package/src/core/build-utils/imageDownloader.mjs +0 -98
  69. package/src/core/createDocuConf.mjs +0 -490
  70. package/src/core/defaults.mjs +0 -67
  71. package/src/core/logger.mjs +0 -17
  72. package/src/core/packageManager.mjs +0 -72
  73. package/src/css/catppuccin.css +0 -632
  74. package/src/pages/notes.js +0 -87
  75. /package/src/template/notes/{welcome.md → welcome.mdx} +0 -0
  76. /package/src/{components → theme/components}/NoteIndex/styles.module.css +0 -0
  77. /package/src/{components → theme/components}/Preview/components/FeedbackStates.js +0 -0
  78. /package/src/{components → theme/components}/Preview/components/FileTabs.js +0 -0
  79. /package/src/{components → theme/components}/Preview/components/Triggers/index.js +0 -0
  80. /package/src/{components → theme/components}/Preview/components/ViewerWindow.js +0 -0
  81. /package/src/{components → theme/components}/Preview/hooks/useDeepLinkHash.js +0 -0
  82. /package/src/{components → theme/components}/Preview/hooks/useDockLayout.js +0 -0
  83. /package/src/{components → theme/components}/Preview/hooks/useFileFetch.js +0 -0
  84. /package/src/{components → theme/components}/Preview/renderers/CodeRenderer.js +0 -0
  85. /package/src/{components → theme/components}/Preview/renderers/ImageRenderer.js +0 -0
  86. /package/src/{components → theme/components}/Preview/renderers/PdfRenderer.js +0 -0
  87. /package/src/{components → theme/components}/Preview/renderers/WebRenderer.js +0 -0
  88. /package/src/{components → theme/components}/Preview/state/index.js +0 -0
  89. /package/src/{components → theme/components}/Preview/styles.module.css +0 -0
  90. /package/src/{components → theme/components}/Preview/utils/index.js +0 -0
  91. /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
- console.error("[ERROR] Failed to process manifest:", err.message);
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
- console.log("\n[INFO] Generating favicons...");
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(context.siteDir, ".docusaurus", "portosaurus", "cache");
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
- console.log(`[INFO] Generating favicon assets from ${finalImagePath}`);
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
- console.log(
163
- `[SUCCESS] Generated ${imageCount} favicon images and ${fileCount} support files\n`,
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
- console.warn(`[WARNING] Favicon generation skipped: ${error.message}`);
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
- console.log("[INFO] Generating robots.txt...");
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
- console.log(`[SUCCESS] Generated robots.txt at ${robotsPath}`);
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(__dirname, `../../assets/img/svg/icon-${iconName}.svg`);
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
- console.warn(`[WARNING] Source icon not found: ${srcPath}`);
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
- console.log(`[INFO] Generated SVG icon: ${destPath}`);
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
- console.error(`[ERROR] Image processing failed: ${err.message}`);
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
+ }
@@ -1,3 +0,0 @@
1
- export { PreviewProvider, usePreview } from './state/index.js';
2
- export { default as ViewerWindow } from './components/ViewerWindow.js';
3
- export { Pv, SrcPv } from './components/Triggers/index.js';