@portosaur/cli 0.6.3 → 0.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portosaur/cli",
3
- "version": "0.6.3",
3
+ "version": "0.9.1",
4
4
  "description": "CLI for Portosaur - The static Personal portfolio site generator.",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "soymadip",
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "types": "./src/index.d.ts",
28
28
  "dependencies": {
29
- "@portosaur/core": "^0.6.3",
30
- "@portosaur/logger": "^0.6.3",
31
- "@portosaur/wizard": "^0.6.3",
29
+ "@portosaur/core": "^0.9.1",
30
+ "@portosaur/logger": "^0.9.1",
31
+ "@portosaur/wizard": "^0.9.1",
32
32
  "commander": "^13.1.0",
33
33
  "js-yaml": "^4.1.1"
34
34
  }
@@ -5,151 +5,198 @@ import { logger } from "@portosaur/logger";
5
5
  /**
6
6
  * Portosaur Discovery-Based Schema Generator
7
7
  *
8
- * This command "discovers" the configuration schema by parsing the actual
9
- * Docusaurus config building logic. Every key accessed via the `get()` helper
10
- * is recorded and mapped to a JSON schema.
8
+ * Discovers the configuration schema by parsing every key accessed via the
9
+ * `get()` and `rawGet()` helpers in docusaurusConfig.mjs.
10
+ *
11
+ * It parses all arguments in the call:
12
+ * `get("key1", "key2", "default")`
13
+ * - "key1" and "key2" are discovered as schema properties
14
+ * - "default" is recorded as the default value
11
15
  */
16
+
17
+ // ------- Helpers -------
18
+
19
+ function applyEntry(propertiesRoot, dotPath, leafSchema) {
20
+ const parts = dotPath.split(".");
21
+ let current = propertiesRoot;
22
+
23
+ parts.forEach((part, index) => {
24
+ const isLast = index === parts.length - 1;
25
+
26
+ if (!current[part]) {
27
+ current[part] = isLast
28
+ ? { ...leafSchema }
29
+ : { type: "object", additionalProperties: false, properties: {} };
30
+ } else if (!isLast && current[part].type !== "object") {
31
+ current[part] = {
32
+ type: "object",
33
+ additionalProperties: false,
34
+ properties: {},
35
+ };
36
+ }
37
+
38
+ if (!isLast) {
39
+ if (!current[part].properties) {
40
+ current[part].properties = {};
41
+ }
42
+ current = current[part].properties;
43
+ }
44
+ });
45
+ }
46
+
47
+ function isJsExpression(val) {
48
+ if (!val) return false;
49
+ return (
50
+ val.startsWith("`") ||
51
+ val.includes("${") ||
52
+ val.includes("()") ||
53
+ /^[a-zA-Z_$][a-zA-Z0-9_.]*$/.test(val) // bare identifier
54
+ );
55
+ }
56
+
57
+ // ------- Main -------
58
+
12
59
  export async function schemaCommand(options = {}) {
13
- // Resolve paths for the monorepo structure
14
60
  const pkgDir = path.resolve(import.meta.dirname, "../");
15
61
  const coreDir = path.resolve(pkgDir, "../../core");
16
62
 
17
63
  const SOURCE_FILE =
18
64
  typeof options.config === "string"
19
65
  ? path.resolve(process.cwd(), options.config)
20
- : path.resolve(coreDir, "src/index.mjs");
66
+ : path.resolve(coreDir, "src/generators/docusaurusConfig.mjs");
21
67
 
22
68
  const OUTPUT_FILE =
23
69
  typeof options.output === "string"
24
70
  ? path.resolve(process.cwd(), options.output)
25
- : path.resolve(pkgDir, "../../../configSchema.json");
71
+ : path.resolve(coreDir, "configSchema.json");
26
72
 
27
73
  const discoveredSchema = {
28
74
  $schema: "http://json-schema.org/draft-07/schema#",
29
75
  title: "Portosaur Project Configuration",
76
+ description: "Schema for config.yml — validated at build time by porto.",
30
77
  type: "object",
31
78
  properties: {},
32
79
  required: [],
33
- additionalProperties: true,
80
+ additionalProperties: false,
34
81
  };
35
82
 
36
- // State for Balanced Parenthesis Walker
37
- const getStartRegex = /get\(\s*["']([^"']+)["']\s*,\s*/g;
38
- let match;
39
-
40
83
  try {
41
84
  if (!fs.existsSync(SOURCE_FILE)) {
42
85
  throw new Error(`Source file not found: ${SOURCE_FILE}`);
43
86
  }
44
87
 
45
- logger.info(`Discovering keys from ${SOURCE_FILE}...`);
88
+ logger.info(`Discovering keys from: ${SOURCE_FILE}`);
46
89
  const sourceCode = fs.readFileSync(SOURCE_FILE, "utf8");
47
90
 
48
- // Discovery Loop
91
+ // match the start of `get(` or `rawGet(`
92
+ const getStartRegex = /\b(?:get|rawGet)\s*\(/g;
93
+ let match;
94
+
49
95
  while ((match = getStartRegex.exec(sourceCode)) !== null) {
50
- const keyPath = match[1];
51
96
  const startIdx = match.index + match[0].length;
52
97
 
53
- let braceCount = 1;
98
+ let depth = 1;
54
99
  let currentIdx = startIdx;
55
- let defaultValueRaw = "";
100
+ let argsRaw = [];
101
+ let currentArg = "";
56
102
  let inString = null;
57
103
  let inComment = null;
58
104
  let escaped = false;
59
105
 
60
- // Extract raw default value using balanced parenthesis walker
61
- while (braceCount > 0 && currentIdx < sourceCode.length) {
106
+ // Extract all arguments separated by commas
107
+ while (depth > 0 && currentIdx < sourceCode.length) {
62
108
  const char = sourceCode[currentIdx];
63
109
  const nextChar = sourceCode[currentIdx + 1];
64
110
 
65
111
  if (escaped) {
66
112
  escaped = false;
113
+ currentArg += char;
67
114
  } else if (char === "\\") {
68
115
  escaped = true;
116
+ currentArg += char;
69
117
  } else if (!inString && !inComment) {
70
- if (char === "'" || char === '"' || char === "`") inString = char;
71
- else if (char === "/" && nextChar === "/") inComment = "//";
72
- else if (char === "/" && nextChar === "*") inComment = "/*";
73
- else if (char === "(") braceCount++;
74
- else if (char === ")") braceCount--;
118
+ if (char === "'" || char === '"' || char === "`") {
119
+ inString = char;
120
+ currentArg += char;
121
+ } else if (char === "/" && nextChar === "/") {
122
+ inComment = "//";
123
+ } else if (char === "/" && nextChar === "*") {
124
+ inComment = "/*";
125
+ } else if (char === "(" || char === "[" || char === "{") {
126
+ depth++;
127
+ currentArg += char;
128
+ } else if (char === ")" || char === "]" || char === "}") {
129
+ depth--;
130
+ if (depth > 0) currentArg += char;
131
+ } else if (char === "," && depth === 1) {
132
+ argsRaw.push(currentArg.trim());
133
+ currentArg = "";
134
+ } else {
135
+ currentArg += char;
136
+ }
75
137
  } else if (inString) {
76
138
  if (char === inString) inString = null;
139
+ currentArg += char;
77
140
  } else if (inComment === "//") {
78
141
  if (char === "\n") inComment = null;
79
142
  } else if (inComment === "/*") {
80
143
  if (char === "*" && nextChar === "/") {
81
144
  inComment = null;
82
- defaultValueRaw += "*/";
83
- currentIdx += 2;
84
- continue;
145
+ currentIdx++;
85
146
  }
86
147
  }
87
-
88
- if (braceCount > 0) defaultValueRaw += char;
89
148
  currentIdx++;
90
149
  }
91
150
 
92
- // Extract Documentation Comments
151
+ if (currentArg.trim()) {
152
+ argsRaw.push(currentArg.trim());
153
+ }
154
+
155
+ if (argsRaw.length === 0) continue;
156
+
157
+ const defaultValueRaw = argsRaw.pop(); // Last argument is default
158
+
159
+ // All remaining arguments that are simple strings are key paths
160
+ const keys = [];
161
+ for (const arg of argsRaw) {
162
+ if (/^["'][^"']+["']$/.test(arg)) {
163
+ keys.push(arg.slice(1, -1));
164
+ }
165
+ }
166
+
167
+ if (keys.length === 0) continue;
168
+
169
+ // Extract comment
93
170
  const beforeMatch = sourceCode.slice(0, match.index);
94
- const currentLineStart = beforeMatch.lastIndexOf("\n");
95
- const linesBefore = sourceCode
96
- .slice(0, currentLineStart === -1 ? 0 : currentLineStart)
97
- .split("\n");
98
-
99
- let i = linesBefore.length - 1;
100
- let foundComment = [];
101
- let inDocBlock = false;
102
-
103
- while (i >= 0 && foundComment.length < 15) {
104
- let line = linesBefore[i].trim();
105
- if (!line && !inDocBlock) break;
106
- if (line.endsWith("*/")) inDocBlock = true;
107
-
108
- if (inDocBlock) {
109
- const content = line.replace(/^\/\*\*?|\*\/|\*/g, "").trim();
110
- if (content) foundComment.unshift(content);
111
- if (line.startsWith("/*") || line.startsWith("/**"))
112
- inDocBlock = false;
113
- } else if (line.startsWith("//")) {
114
- let content = line.replace(/^\/\/\s?/, "").trim();
171
+ const lines = beforeMatch.split("\n");
172
+
173
+ let description = "";
174
+ let i = lines.length - 1;
175
+ const commentLines = [];
176
+
177
+ while (i >= 0 && commentLines.length < 5) {
178
+ const line = lines[i].trim();
179
+ if (!line) break;
180
+ if (line.startsWith("//")) {
181
+ const content = line.replace(/^\/\/\s?/, "").trim();
115
182
  if (!content.match(/^(TODO|FIXME|NOTE|SECTION|---)/i)) {
116
- foundComment.unshift(content);
183
+ commentLines.unshift(content);
117
184
  }
118
- } else if (line && !line.startsWith("/") && !line.startsWith("*")) {
185
+ i--;
186
+ } else if (line.endsWith("*/") || line.startsWith("*")) {
187
+ break;
188
+ } else {
119
189
  break;
120
190
  }
121
- i--;
122
191
  }
123
- const description = foundComment.join("\n").trim();
192
+ description = commentLines.join(" ").trim();
193
+
194
+ // Type inference
195
+ const val = defaultValueRaw;
124
196
 
125
- // Basic Type Inference and Argument Parsing
126
197
  let type = "string";
127
198
  let defaultValue = undefined;
128
-
129
- let args = [];
130
- let currentArg = "";
131
- let depth = 0;
132
- let argInString = null;
133
-
134
- for (let i = 0; i < defaultValueRaw.length; i++) {
135
- const c = defaultValueRaw[i];
136
- if (!argInString) {
137
- if (c === "'" || c === '"' || c === "`") argInString = c;
138
- else if (c === "(" || c === "[" || c === "{") depth++;
139
- else if (c === ")" || c === "]" || c === "}") depth--;
140
- else if (c === "," && depth === 0) {
141
- args.push(currentArg.trim());
142
- currentArg = "";
143
- continue;
144
- }
145
- } else if (c === argInString && defaultValueRaw[i - 1] !== "\\") {
146
- argInString = null;
147
- }
148
- currentArg += c;
149
- }
150
- if (currentArg.trim()) args.push(currentArg.trim());
151
-
152
- let val = (args[args.length - 1] || "").trim();
199
+ let isFreeform = false;
153
200
 
154
201
  if (val === "true" || val === "false") {
155
202
  type = "boolean";
@@ -164,45 +211,29 @@ export async function schemaCommand(options = {}) {
164
211
  val.includes(".slice(")
165
212
  ) {
166
213
  type = "array";
214
+ } else if (val === "{}") {
215
+ type = "object";
216
+ isFreeform = true;
167
217
  } else if (val.startsWith("{")) {
168
218
  type = "object";
169
- } else if (val) {
170
- defaultValue = val.replace(/^["'`](.*)["'`]$/s, "$1").trim();
219
+ } else if (val && !isJsExpression(val)) {
220
+ defaultValue = val.replace(/^["'`](.*?)["'`]$/s, "$1").trim();
171
221
  }
172
222
 
173
- // Map to Discovered Schema Object
174
- const parts = keyPath.split(".");
175
- let current = discoveredSchema.properties;
176
-
177
- parts.forEach((part, index) => {
178
- const isLast = index === parts.length - 1;
223
+ const leafSchema = { type };
224
+ if (isFreeform) leafSchema.additionalProperties = true;
225
+ if (description) leafSchema.description = description;
226
+ if (defaultValue !== undefined) leafSchema.default = defaultValue;
179
227
 
180
- if (!current[part]) {
181
- if (isLast) {
182
- current[part] = { type };
183
- if (description) current[part].description = description;
184
- if (defaultValue !== undefined)
185
- current[part].default = defaultValue;
186
- } else {
187
- current[part] = { type: "object", properties: {} };
188
- }
189
- } else if (!isLast && current[part].type !== "object") {
190
- current[part] = { type: "object", properties: {} };
191
- }
192
-
193
- if (!isLast) {
194
- if (!current[part].properties) current[part].properties = {};
195
- current = current[part].properties;
196
- }
197
- });
228
+ for (const keyPath of keys) {
229
+ applyEntry(discoveredSchema.properties, keyPath, leafSchema);
230
+ }
198
231
  }
199
232
 
200
233
  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(discoveredSchema, null, 2));
201
- logger.success(
202
- `Successfully discovered and wrote schema to: ${OUTPUT_FILE}`,
203
- );
234
+ logger.success(`Schema written to: ${OUTPUT_FILE}`);
204
235
  } catch (error) {
205
- logger.error(`Discovery failed: ${error.message}`);
236
+ logger.error(`Schema generation failed: ${error.message}`);
206
237
  process.exit(1);
207
238
  }
208
239
  }
@@ -1,15 +1,11 @@
1
1
  # yaml-language-server: $schema=https://soymadip.github.io/portosaur/conf-schema.json
2
+ #
2
3
  # Portosaur Project Configuration
3
- # This file is filled with example values
4
- # Check Config Docs: https://soymadip.is-a.dev/portosaur/guide/config
4
+ # Check full Config Docs: https://soymadip.is-a.dev/portosaur/guide/config
5
5
 
6
6
  # Site Metadata & SEO
7
7
  site:
8
- title: "{{fullName}}"
9
- tagline: "Short description about you, your passion, your goals etc."
10
-
11
- favicon: "{{portoRoot}}/assets/img/svg/icon.svg"
12
- social_card: "{{portoRoot}}/assets/img/social-card.jpeg"
8
+ favicon: null # By default, generates from {{home_page.hero.profile_pic}}
13
9
 
14
10
  # Auto set if deploying in Github/GitLab Pages
15
11
  url: "auto"
@@ -17,21 +13,16 @@ site:
17
13
 
18
14
  # UI/UX & Theme Behavior
19
15
  theme:
20
- appearance:
21
- dark_mode: true
22
- disable_switch: false
23
-
24
16
  navigation:
25
17
  collapsable_sidebar: true
26
- hide_navbar_on_scroll: true
27
18
 
28
19
  # Page Content & Sections
29
20
  home_page:
30
21
  hero:
31
- title: null # Fallback to {{site.title}}
22
+ title: "{{fullName}}"
32
23
  profession: "Your Profession"
33
- desc: null # Fallback to {{site.tagline}}
34
- profile_pic: null # Fallback to {{site.favicon}}
24
+ desc: "Short description about you, your passion, your goals etc."
25
+ profile_pic: "{{portoRoot}}/assets/img/svg/icon.svg"
35
26
 
36
27
  social:
37
28
  - name: "LinkedIn"
@@ -135,7 +126,7 @@ tasks:
135
126
  status: "done"
136
127
  desc: "Implementing the core framework."
137
128
 
138
- # Functional Tools
129
+ # Functional Tools [TODO]
139
130
  tools:
140
131
  link_shortener:
141
132
  enable: false
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { spawn } from "child_process";
4
4
  import { createRequire } from "module";
5
- import { hasCommand } from "@portosaur/core";
5
+ import { hasCommand, getPortoDotDir } from "@portosaur/core";
6
6
 
7
7
  /**
8
8
  * Generates a static Docusaurus config file by evaluating the Portosaur config
@@ -15,7 +15,7 @@ import { hasCommand } from "@portosaur/core";
15
15
  * @returns {string} The path to the generated config file.
16
16
  */
17
17
  export function writeConfigShim(UserRoot, portoPaths, context = {}) {
18
- const dotDir = path.join(UserRoot, ".docusaurus", "portosaur");
18
+ const dotDir = getPortoDotDir(UserRoot);
19
19
 
20
20
  if (!fs.existsSync(dotDir)) {
21
21
  fs.mkdirSync(dotDir, { recursive: true });
@@ -174,6 +174,30 @@ export async function runDocusaurus(
174
174
 
175
175
  args.push(...extraArgs);
176
176
 
177
+ // Strip the hardcoded Docusaurus generator tag from its internal HTML templates
178
+ // so that only the Portosaur metadata tag is rendered in the final output.
179
+ try {
180
+ const coreLibDir = path.resolve(path.dirname(docusaurusBin), "../lib");
181
+ const templates = [
182
+ path.join(coreLibDir, "webpack/templates/dev.html.template.ejs"),
183
+ path.join(coreLibDir, "ssg/ssgTemplate.html.js"),
184
+ ];
185
+ for (const tpl of templates) {
186
+ if (fs.existsSync(tpl)) {
187
+ let content = fs.readFileSync(tpl, "utf8");
188
+ const newContent = content.replace(
189
+ /<meta name="generator" content="Docusaurus[^"]*">\n?/g,
190
+ "",
191
+ );
192
+ if (content !== newContent) {
193
+ fs.writeFileSync(tpl, newContent, "utf8");
194
+ }
195
+ }
196
+ }
197
+ } catch (err) {
198
+ // Ignore permissions or missing file errors
199
+ }
200
+
177
201
  // Use bun when available for faster builds, fall back to node.
178
202
  // If node isn't installed, fallback to bun gracefully.
179
203
  let runtime = "node";