@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 +4 -4
- package/src/commands/schema.mjs +141 -110
- package/src/templates/config.yml +7 -16
- package/src/utils/runner.mjs +26 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portosaur/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
30
|
-
"@portosaur/logger": "^0.
|
|
31
|
-
"@portosaur/wizard": "^0.
|
|
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
|
}
|
package/src/commands/schema.mjs
CHANGED
|
@@ -5,151 +5,198 @@ import { logger } from "@portosaur/logger";
|
|
|
5
5
|
/**
|
|
6
6
|
* Portosaur Discovery-Based Schema Generator
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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/
|
|
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(
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
98
|
+
let depth = 1;
|
|
54
99
|
let currentIdx = startIdx;
|
|
55
|
-
let
|
|
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
|
|
61
|
-
while (
|
|
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 === "`")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
else if (char === "
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
183
|
+
commentLines.unshift(content);
|
|
117
184
|
}
|
|
118
|
-
|
|
185
|
+
i--;
|
|
186
|
+
} else if (line.endsWith("*/") || line.startsWith("*")) {
|
|
187
|
+
break;
|
|
188
|
+
} else {
|
|
119
189
|
break;
|
|
120
190
|
}
|
|
121
|
-
i--;
|
|
122
191
|
}
|
|
123
|
-
|
|
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(/^["'`](
|
|
219
|
+
} else if (val && !isJsExpression(val)) {
|
|
220
|
+
defaultValue = val.replace(/^["'`](.*?)["'`]$/s, "$1").trim();
|
|
171
221
|
}
|
|
172
222
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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(`
|
|
236
|
+
logger.error(`Schema generation failed: ${error.message}`);
|
|
206
237
|
process.exit(1);
|
|
207
238
|
}
|
|
208
239
|
}
|
package/src/templates/config.yml
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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:
|
|
22
|
+
title: "{{fullName}}"
|
|
32
23
|
profession: "Your Profession"
|
|
33
|
-
desc:
|
|
34
|
-
profile_pic:
|
|
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
|
package/src/utils/runner.mjs
CHANGED
|
@@ -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 =
|
|
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";
|