@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravindc26/velu",
3
- "version": "0.11.14",
3
+ "version": "0.11.16",
4
4
  "description": "A modern documentation site generator powered by Markdown and JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 PRIMARY_CONFIG_NAME = "docs.json";
15
- const LEGACY_CONFIG_NAME = "velu.json";
16
- const SOURCE_MIRROR_DIR = "velu-imports";
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
- // Placeholder for analytics/reporting integration.
56
- onCancel();
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