@portosaur/cli 0.8.0 → 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 +2 -18
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
3
|
# Portosaur Project Configuration
|
|
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: null # Defaults to {{homepage.hero.title}}
|
|
9
|
-
tagline: null # Defaults to {{homepage.hero.desc}}
|
|
10
|
-
|
|
11
8
|
favicon: null # By default, generates from {{home_page.hero.profile_pic}}
|
|
12
|
-
# social_card: "{{portoRoot}}/assets/img/social-card.jpeg"
|
|
13
9
|
|
|
14
10
|
# Auto set if deploying in Github/GitLab Pages
|
|
15
11
|
url: "auto"
|
|
@@ -17,20 +13,8 @@ site:
|
|
|
17
13
|
|
|
18
14
|
# UI/UX & Theme Behavior
|
|
19
15
|
theme:
|
|
20
|
-
appearance:
|
|
21
|
-
default_theme: dark # dark | light
|
|
22
|
-
show_theme_switch: true
|
|
23
|
-
disable_project_link: false
|
|
24
|
-
|
|
25
|
-
footer:
|
|
26
|
-
enable: true
|
|
27
|
-
message: null
|
|
28
|
-
disable_project_link: false
|
|
29
|
-
|
|
30
16
|
navigation:
|
|
31
|
-
breadcrumbs: true
|
|
32
17
|
collapsable_sidebar: true
|
|
33
|
-
hide_navbar_on_scroll: true
|
|
34
18
|
|
|
35
19
|
# Page Content & Sections
|
|
36
20
|
home_page:
|
|
@@ -142,7 +126,7 @@ tasks:
|
|
|
142
126
|
status: "done"
|
|
143
127
|
desc: "Implementing the core framework."
|
|
144
128
|
|
|
145
|
-
# Functional Tools
|
|
129
|
+
# Functional Tools [TODO]
|
|
146
130
|
tools:
|
|
147
131
|
link_shortener:
|
|
148
132
|
enable: false
|