@aravindc26/velu 0.11.14 → 0.11.16
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
package/src/build.ts
CHANGED
|
@@ -9,11 +9,12 @@ import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
|
|
12
|
-
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
13
|
-
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
12
|
+
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
13
|
+
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
14
|
+
const CLI_PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
|
|
15
|
+
const PRIMARY_CONFIG_NAME = "docs.json";
|
|
16
|
+
const LEGACY_CONFIG_NAME = "velu.json";
|
|
17
|
+
const SOURCE_MIRROR_DIR = "velu-imports";
|
|
17
18
|
|
|
18
19
|
const SOURCE_MIRROR_EXTENSIONS = new Set([
|
|
19
20
|
".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
|
|
@@ -630,10 +631,24 @@ function resolveProjectDescription(config: VeluConfig): string {
|
|
|
630
631
|
return "";
|
|
631
632
|
}
|
|
632
633
|
|
|
634
|
+
function resolveCliVersion(): string {
|
|
635
|
+
try {
|
|
636
|
+
const raw = readFileSync(CLI_PACKAGE_JSON_PATH, "utf-8");
|
|
637
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
638
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
639
|
+
return parsed.version.trim();
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// ignore and fallback
|
|
643
|
+
}
|
|
644
|
+
return "unknown";
|
|
645
|
+
}
|
|
646
|
+
|
|
633
647
|
function writeProjectConstFile(config: VeluConfig, outDir: string) {
|
|
634
648
|
const constPayload = {
|
|
635
649
|
name: resolveProjectName(config),
|
|
636
650
|
description: resolveProjectDescription(config),
|
|
651
|
+
version: resolveCliVersion(),
|
|
637
652
|
};
|
|
638
653
|
|
|
639
654
|
const constPath = join(outDir, "public", "const.json");
|
package/src/cli.ts
CHANGED
|
@@ -35,6 +35,7 @@ function printHelp() {
|
|
|
35
35
|
velu — documentation site generator
|
|
36
36
|
|
|
37
37
|
Usage:
|
|
38
|
+
velu version Print Velu CLI version
|
|
38
39
|
velu init Scaffold a new docs project with example files
|
|
39
40
|
velu lint Validate docs.json (or velu.json) and check referenced pages
|
|
40
41
|
velu run [--port N] Build site and start dev server (default: 4321)
|
|
@@ -42,6 +43,7 @@ function printHelp() {
|
|
|
42
43
|
velu paths Output navigation paths and source files as JSON (grouped by language)
|
|
43
44
|
|
|
44
45
|
Options:
|
|
46
|
+
--version Show Velu CLI version
|
|
45
47
|
--port <number> Port for the dev server (default: 4321)
|
|
46
48
|
--help Show this help message
|
|
47
49
|
|
|
@@ -49,6 +51,24 @@ function printHelp() {
|
|
|
49
51
|
`);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function getCliVersion(): string {
|
|
55
|
+
try {
|
|
56
|
+
const pkgPath = join(PACKAGE_ROOT, "package.json");
|
|
57
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
58
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
59
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
60
|
+
return parsed.version.trim();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore and fallback
|
|
64
|
+
}
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printVersion() {
|
|
69
|
+
console.log(getCliVersion());
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
// ── init ────────────────────────────────────────────────────────────────────────
|
|
53
73
|
|
|
54
74
|
function init(targetDir: string) {
|
|
@@ -495,6 +515,11 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
495
515
|
process.exit(0);
|
|
496
516
|
}
|
|
497
517
|
|
|
518
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
519
|
+
printVersion();
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
498
523
|
const docsDir = process.cwd();
|
|
499
524
|
|
|
500
525
|
// init doesn't require docs.json
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
buildPublicFeedbackPayload,
|
|
5
|
+
PUBLIC_API_BASE_URL,
|
|
6
|
+
PUBLIC_FEEDBACK_ENDPOINT_PATH,
|
|
7
|
+
resolvePublicFeedbackEndpoint,
|
|
8
|
+
submitPublicFeedback,
|
|
9
|
+
} from './page-feedback-api';
|
|
10
|
+
|
|
11
|
+
test('buildPublicFeedbackPayload returns contract body with optional fields', () => {
|
|
12
|
+
const payload = buildPublicFeedbackPayload({
|
|
13
|
+
pageUrl: 'https://docs.example.com/guide',
|
|
14
|
+
helpful: true,
|
|
15
|
+
reasonText: 'The guide worked as expected',
|
|
16
|
+
details: ' Extra details ',
|
|
17
|
+
email: ' user@example.com ',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(payload, {
|
|
21
|
+
page_url: 'https://docs.example.com/guide',
|
|
22
|
+
helpful: true,
|
|
23
|
+
reason_text: 'The guide worked as expected',
|
|
24
|
+
details: 'Extra details',
|
|
25
|
+
email: 'user@example.com',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('resolvePublicFeedbackEndpoint appends the fixed endpoint path', () => {
|
|
30
|
+
assert.equal(
|
|
31
|
+
resolvePublicFeedbackEndpoint(),
|
|
32
|
+
`${PUBLIC_API_BASE_URL}${PUBLIC_FEEDBACK_ENDPOINT_PATH}`,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('submitPublicFeedback posts the expected payload and headers', async () => {
|
|
37
|
+
let receivedUrl = '';
|
|
38
|
+
let receivedInit: RequestInit | undefined;
|
|
39
|
+
|
|
40
|
+
const fetchImpl: typeof fetch = async (input, init) => {
|
|
41
|
+
receivedUrl = String(input);
|
|
42
|
+
receivedInit = init;
|
|
43
|
+
return new Response(null, { status: 204 });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = await submitPublicFeedback({
|
|
47
|
+
pageUrl: 'https://docs.example.com/page',
|
|
48
|
+
helpful: false,
|
|
49
|
+
reasonText: 'Update this documentation',
|
|
50
|
+
details: '',
|
|
51
|
+
email: undefined,
|
|
52
|
+
siteHost: 'docs.example.com',
|
|
53
|
+
fetchImpl,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(result, { ok: true });
|
|
57
|
+
assert.equal(receivedUrl, `${PUBLIC_API_BASE_URL}${PUBLIC_FEEDBACK_ENDPOINT_PATH}`);
|
|
58
|
+
assert.equal(receivedInit?.method, 'POST');
|
|
59
|
+
assert.equal(receivedInit?.credentials, 'include');
|
|
60
|
+
assert.equal(receivedInit?.body, JSON.stringify({
|
|
61
|
+
page_url: 'https://docs.example.com/page',
|
|
62
|
+
helpful: false,
|
|
63
|
+
reason_text: 'Update this documentation',
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const headers = receivedInit?.headers as Record<string, string>;
|
|
67
|
+
assert.deepEqual(headers, {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'x-velu-site-host': 'docs.example.com',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('submitPublicFeedback reports non-2xx responses as request_failed', async () => {
|
|
74
|
+
const result = await submitPublicFeedback({
|
|
75
|
+
pageUrl: 'https://docs.example.com/page',
|
|
76
|
+
helpful: true,
|
|
77
|
+
reasonText: 'Something else',
|
|
78
|
+
siteHost: 'docs.example.com',
|
|
79
|
+
fetchImpl: async () => new Response(null, { status: 500 }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(result, { ok: false, reason: 'request_failed', status: 500 });
|
|
83
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const PUBLIC_FEEDBACK_ENDPOINT_PATH = '/api/v1/public/feedback';
|
|
2
|
+
export const PUBLIC_API_BASE_URL = 'https://api.getvelu.com';
|
|
3
|
+
|
|
4
|
+
export interface PublicFeedbackPayload {
|
|
5
|
+
page_url: string;
|
|
6
|
+
helpful: boolean;
|
|
7
|
+
reason_text: string;
|
|
8
|
+
details?: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface BuildPayloadInput {
|
|
13
|
+
pageUrl: string;
|
|
14
|
+
helpful: boolean;
|
|
15
|
+
reasonText: string;
|
|
16
|
+
details?: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SubmitPublicFeedbackInput extends BuildPayloadInput {
|
|
21
|
+
siteHost: string;
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SubmitErrorReason = 'invalid_payload' | 'request_failed' | 'network_error';
|
|
26
|
+
|
|
27
|
+
export type SubmitPublicFeedbackResult =
|
|
28
|
+
| { ok: true }
|
|
29
|
+
| { ok: false; reason: SubmitErrorReason; status?: number };
|
|
30
|
+
|
|
31
|
+
function trimOptional(value: string | undefined): string | undefined {
|
|
32
|
+
if (typeof value !== 'string') return undefined;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolvePublicFeedbackEndpoint(): string {
|
|
38
|
+
return `${PUBLIC_API_BASE_URL}${PUBLIC_FEEDBACK_ENDPOINT_PATH}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildPublicFeedbackPayload(input: BuildPayloadInput): PublicFeedbackPayload | null {
|
|
42
|
+
const pageUrl = trimOptional(input.pageUrl);
|
|
43
|
+
const reasonText = trimOptional(input.reasonText);
|
|
44
|
+
if (!pageUrl || !reasonText) return null;
|
|
45
|
+
|
|
46
|
+
const payload: PublicFeedbackPayload = {
|
|
47
|
+
page_url: pageUrl,
|
|
48
|
+
helpful: input.helpful,
|
|
49
|
+
reason_text: reasonText,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const details = trimOptional(input.details);
|
|
53
|
+
if (details) payload.details = details;
|
|
54
|
+
|
|
55
|
+
const email = trimOptional(input.email);
|
|
56
|
+
if (email) payload.email = email;
|
|
57
|
+
|
|
58
|
+
return payload;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function submitPublicFeedback(input: SubmitPublicFeedbackInput): Promise<SubmitPublicFeedbackResult> {
|
|
62
|
+
const payload = buildPublicFeedbackPayload(input);
|
|
63
|
+
if (!payload) return { ok: false, reason: 'invalid_payload' };
|
|
64
|
+
|
|
65
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetchImpl(resolvePublicFeedbackEndpoint(), {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
credentials: 'include',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'x-velu-site-host': input.siteHost,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(payload),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: 'request_failed',
|
|
81
|
+
status: response.status,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ok: true };
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: false, reason: 'network_error' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, type MouseEvent as ReactMouseEvent } from 'react';
|
|
4
4
|
import { ThumbsDown, ThumbsUp } from 'lucide-react';
|
|
5
|
+
import { submitPublicFeedback } from '@/components/page-feedback-api';
|
|
5
6
|
|
|
6
7
|
type Vote = 'yes' | 'no';
|
|
7
8
|
|
|
@@ -26,10 +27,13 @@ export function PageFeedback() {
|
|
|
26
27
|
const [selectedReason, setSelectedReason] = useState<string>('');
|
|
27
28
|
const [details, setDetails] = useState('');
|
|
28
29
|
const [email, setEmail] = useState('');
|
|
30
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
31
|
+
const [submitError, setSubmitError] = useState(false);
|
|
29
32
|
|
|
30
33
|
const showForm = vote !== null;
|
|
31
34
|
const options = vote === 'yes' ? YES_OPTIONS : NO_OPTIONS;
|
|
32
35
|
const showOptionalInputs = selectedReason === 'Something else';
|
|
36
|
+
const canSubmit = vote !== null && selectedReason.trim().length > 0 && !isSubmitting;
|
|
33
37
|
|
|
34
38
|
const onChooseVote = (value: Vote) => {
|
|
35
39
|
if (vote === value) return;
|
|
@@ -37,6 +41,7 @@ export function PageFeedback() {
|
|
|
37
41
|
setSelectedReason('');
|
|
38
42
|
setDetails('');
|
|
39
43
|
setEmail('');
|
|
44
|
+
setSubmitError(false);
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
const stopEvent = (event: ReactMouseEvent) => {
|
|
@@ -49,11 +54,32 @@ export function PageFeedback() {
|
|
|
49
54
|
setSelectedReason('');
|
|
50
55
|
setDetails('');
|
|
51
56
|
setEmail('');
|
|
57
|
+
setIsSubmitting(false);
|
|
58
|
+
setSubmitError(false);
|
|
52
59
|
};
|
|
53
60
|
|
|
54
|
-
const onSubmit = () => {
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const onSubmit = async () => {
|
|
62
|
+
if (!vote || !selectedReason.trim() || isSubmitting) return;
|
|
63
|
+
|
|
64
|
+
setIsSubmitting(true);
|
|
65
|
+
setSubmitError(false);
|
|
66
|
+
|
|
67
|
+
const result = await submitPublicFeedback({
|
|
68
|
+
helpful: vote === 'yes',
|
|
69
|
+
reasonText: selectedReason,
|
|
70
|
+
details: showOptionalInputs ? details : undefined,
|
|
71
|
+
email: showOptionalInputs ? email : undefined,
|
|
72
|
+
pageUrl: window.location.href,
|
|
73
|
+
siteHost: window.location.host,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.ok) {
|
|
77
|
+
onCancel();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setSubmitError(true);
|
|
82
|
+
setIsSubmitting(false);
|
|
57
83
|
};
|
|
58
84
|
|
|
59
85
|
return (
|
|
@@ -65,6 +91,7 @@ export function PageFeedback() {
|
|
|
65
91
|
type="button"
|
|
66
92
|
className={['velu-page-feedback-btn', vote === 'yes' ? 'is-active' : ''].filter(Boolean).join(' ')}
|
|
67
93
|
aria-label="Mark page as helpful"
|
|
94
|
+
disabled={isSubmitting}
|
|
68
95
|
onClick={(event) => {
|
|
69
96
|
stopEvent(event);
|
|
70
97
|
onChooseVote('yes');
|
|
@@ -77,6 +104,7 @@ export function PageFeedback() {
|
|
|
77
104
|
type="button"
|
|
78
105
|
className={['velu-page-feedback-btn', vote === 'no' ? 'is-active' : ''].filter(Boolean).join(' ')}
|
|
79
106
|
aria-label="Mark page as not helpful"
|
|
107
|
+
disabled={isSubmitting}
|
|
80
108
|
onClick={(event) => {
|
|
81
109
|
stopEvent(event);
|
|
82
110
|
onChooseVote('no');
|
|
@@ -103,10 +131,12 @@ export function PageFeedback() {
|
|
|
103
131
|
type="button"
|
|
104
132
|
role="radio"
|
|
105
133
|
aria-checked={checked}
|
|
134
|
+
disabled={isSubmitting}
|
|
106
135
|
className={['velu-page-feedback-option', checked ? 'is-checked' : ''].filter(Boolean).join(' ')}
|
|
107
136
|
onClick={(event) => {
|
|
108
137
|
stopEvent(event);
|
|
109
138
|
setSelectedReason(option);
|
|
139
|
+
setSubmitError(false);
|
|
110
140
|
}}
|
|
111
141
|
>
|
|
112
142
|
<span className="velu-page-feedback-radio" aria-hidden="true" />
|
|
@@ -123,6 +153,7 @@ export function PageFeedback() {
|
|
|
123
153
|
rows={3}
|
|
124
154
|
placeholder="(Optional) Could you share more about your experience?"
|
|
125
155
|
value={details}
|
|
156
|
+
disabled={isSubmitting}
|
|
126
157
|
onChange={(event) => setDetails(event.target.value)}
|
|
127
158
|
/>
|
|
128
159
|
<input
|
|
@@ -130,6 +161,7 @@ export function PageFeedback() {
|
|
|
130
161
|
type="email"
|
|
131
162
|
placeholder="(Optional) Email"
|
|
132
163
|
value={email}
|
|
164
|
+
disabled={isSubmitting}
|
|
133
165
|
onChange={(event) => setEmail(event.target.value)}
|
|
134
166
|
/>
|
|
135
167
|
</div>
|
|
@@ -139,6 +171,7 @@ export function PageFeedback() {
|
|
|
139
171
|
<button
|
|
140
172
|
type="button"
|
|
141
173
|
className="velu-page-feedback-cancel"
|
|
174
|
+
disabled={isSubmitting}
|
|
142
175
|
onClick={(event) => {
|
|
143
176
|
stopEvent(event);
|
|
144
177
|
onCancel();
|
|
@@ -149,9 +182,12 @@ export function PageFeedback() {
|
|
|
149
182
|
<button
|
|
150
183
|
type="button"
|
|
151
184
|
className="velu-page-feedback-submit"
|
|
185
|
+
disabled={!canSubmit}
|
|
186
|
+
aria-busy={isSubmitting}
|
|
187
|
+
title={submitError ? 'Unable to submit feedback right now. Please try again.' : undefined}
|
|
152
188
|
onClick={(event) => {
|
|
153
189
|
stopEvent(event);
|
|
154
|
-
onSubmit();
|
|
190
|
+
void onSubmit();
|
|
155
191
|
}}
|
|
156
192
|
>
|
|
157
193
|
Submit feedback
|