ayezee-astro-cms 1.1.0 → 1.2.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.
@@ -0,0 +1,175 @@
1
+ ---
2
+ /**
3
+ * AyezeeForm Component
4
+ *
5
+ * A flexible form component for AyeZee CMS that handles form submission,
6
+ * Turnstile integration, and validation. Fully customizable via slots and props.
7
+ *
8
+ * @example
9
+ * ```astro
10
+ * <AyezeeForm label="Contact Form" formClass="my-form" submitButtonClass="btn-primary">
11
+ * <input type="text" name="name" required />
12
+ * <input type="email" name="email" required />
13
+ * <textarea name="message" required></textarea>
14
+ * </AyezeeForm>
15
+ * ```
16
+ */
17
+
18
+ import { getFormConfig } from '../cms-helper.js';
19
+
20
+ export interface Props {
21
+ /** Module label, slug, or instance key to identify the form */
22
+ label?: string;
23
+ /** Instance key (alternative to label) */
24
+ instanceKey?: string;
25
+ /** Text for the submit button */
26
+ submitButtonText?: string;
27
+ /** CSS classes for the submit button */
28
+ submitButtonClass?: string;
29
+ /** CSS classes for the form element */
30
+ formClass?: string;
31
+ /** Success message to display */
32
+ successMessage?: string;
33
+ /** Error message to display */
34
+ errorMessage?: string;
35
+ /** CSS classes for the message container */
36
+ messageClass?: string;
37
+ /** CSS classes for the Turnstile container */
38
+ turnstileClass?: string;
39
+ /** Whether to show the default submit button (set to false if using custom button in slot) */
40
+ showSubmitButton?: boolean;
41
+ /** Whether to show the default message div (set to false if handling messages externally) */
42
+ showMessageDiv?: boolean;
43
+ /** Additional data attributes to add to the form */
44
+ dataAttributes?: Record<string, string>;
45
+ }
46
+
47
+ const {
48
+ label,
49
+ instanceKey,
50
+ submitButtonText = 'Submit',
51
+ submitButtonClass = '',
52
+ formClass = '',
53
+ successMessage = 'Form submitted successfully!',
54
+ errorMessage = 'Failed to submit form. Please try again.',
55
+ messageClass = '',
56
+ turnstileClass = '',
57
+ showSubmitButton = true,
58
+ showMessageDiv = true,
59
+ dataAttributes = {},
60
+ } = Astro.props;
61
+
62
+ // Get form configuration (includes Turnstile settings)
63
+ const moduleIdentifier = instanceKey || label;
64
+ if (!moduleIdentifier) {
65
+ throw new Error('AyezeeForm requires either "label" or "instanceKey" prop');
66
+ }
67
+
68
+ const formConfig = getFormConfig(moduleIdentifier);
69
+
70
+ if (!formConfig.module) {
71
+ throw new Error(`Form module "${moduleIdentifier}" not found`);
72
+ }
73
+
74
+ // Generate unique form ID
75
+ const formId = `ayezee-form-${formConfig.module.instanceKey}`;
76
+
77
+ // Pass config to client
78
+ const clientConfig = {
79
+ formId,
80
+ apiUrl: formConfig.apiUrl,
81
+ apiKey: formConfig.apiKey,
82
+ turnstile: formConfig.turnstile,
83
+ successMessage,
84
+ errorMessage,
85
+ };
86
+
87
+ // Build data attributes
88
+ const dataAttrs = {
89
+ 'data-ayezee-form': '',
90
+ ...dataAttributes,
91
+ };
92
+ ---
93
+
94
+ <form id={formId} class={formClass} {...dataAttrs}>
95
+ <!-- Honeypot field (hidden from users, catches bots) -->
96
+ <input
97
+ type="text"
98
+ name="website"
99
+ class="absolute -left-[9999px] opacity-0 pointer-events-none"
100
+ tabindex="-1"
101
+ autocomplete="off"
102
+ aria-hidden="true"
103
+ />
104
+
105
+ <!-- Form fields from slot -->
106
+ <slot />
107
+
108
+ <!-- Turnstile container (only rendered if enabled) -->
109
+ {
110
+ formConfig.turnstile.enabled && (
111
+ <div id={`${formId}-turnstile`} class={turnstileClass || 'turnstile-container my-4'} />
112
+ )
113
+ }
114
+
115
+ <!-- Message container -->
116
+ {
117
+ showMessageDiv && (
118
+ <div
119
+ id={`${formId}-message`}
120
+ class={messageClass || 'hidden mt-2 p-3 border rounded text-sm'}
121
+ role="alert"
122
+ aria-live="polite"
123
+ />
124
+ )
125
+ }
126
+
127
+ <!-- Submit button -->
128
+ {
129
+ showSubmitButton && (
130
+ <button
131
+ type="submit"
132
+ id={`${formId}-submit`}
133
+ class={
134
+ submitButtonClass ||
135
+ 'bg-blue-600 text-white px-6 py-3 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed'
136
+ }
137
+ >
138
+ <span class="button-text">{submitButtonText}</span>
139
+ <span class="button-loading hidden">Submitting...</span>
140
+ </button>
141
+ )
142
+ }
143
+
144
+ <!-- Slot for custom submit button or additional form elements -->
145
+ <slot name="after-form" />
146
+ </form>
147
+
148
+ <!-- Load Turnstile script if enabled -->
149
+ {
150
+ formConfig.turnstile.enabled && (
151
+ <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
152
+ )
153
+ }
154
+
155
+ <!-- Pass config to client-side script -->
156
+ <script is:inline define:vars={{ clientConfig }}>
157
+ window[`FORM_CONFIG_${clientConfig.formId}`] = clientConfig;
158
+ </script>
159
+
160
+ <!-- Form handler script -->
161
+ <script>
162
+ import { AyezeeFormHandler, initAyezeeForms } from '../form-handler.js';
163
+
164
+ // Initialize all forms on the page
165
+ function initForms() {
166
+ initAyezeeForms();
167
+ }
168
+
169
+ // Init when DOM is ready
170
+ if (document.readyState === 'loading') {
171
+ document.addEventListener('DOMContentLoaded', initForms);
172
+ } else {
173
+ initForms();
174
+ }
175
+ </script>
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AyeZee CMS Components
3
+ *
4
+ * Export Astro components for use in projects
5
+ */
6
+ export { AyezeeFormHandler, initAyezeeForms, submitToAyezeeCms } from '../form-handler.js';
7
+ export type { FormHandlerConfig, FormSubmissionResult } from '../form-handler.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * AyeZee CMS Components
3
+ *
4
+ * Export Astro components for use in projects
5
+ */
6
+ // Note: Astro components are exported individually, not through this index
7
+ // Import them directly like: import AyezeeForm from 'ayezee-astro-cms/components/AyezeeForm.astro'
8
+ export { AyezeeFormHandler, initAyezeeForms, submitToAyezeeCms } from '../form-handler.js';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Form Handler Utilities for AyeZee CMS Forms
3
+ *
4
+ * Provides core form submission logic, Turnstile integration, and error handling
5
+ * that can be used in custom form implementations or with the default AyezeeForm component
6
+ */
7
+ export interface FormHandlerConfig {
8
+ formId: string;
9
+ apiUrl: string;
10
+ apiKey: string;
11
+ turnstile: {
12
+ enabled: boolean;
13
+ siteKey: string | null;
14
+ };
15
+ successMessage?: string;
16
+ errorMessage?: string;
17
+ onSuccess?: (data: any) => void;
18
+ onError?: (error: string) => void;
19
+ onSubmitStart?: () => void;
20
+ onSubmitEnd?: () => void;
21
+ }
22
+ export interface FormSubmissionResult {
23
+ success: boolean;
24
+ message: string;
25
+ data?: any;
26
+ errors?: string[];
27
+ }
28
+ /**
29
+ * Core form handler class that manages form submission, validation, and Turnstile integration
30
+ */
31
+ export declare class AyezeeFormHandler {
32
+ private form;
33
+ private submitButton;
34
+ private messageDiv;
35
+ private turnstileContainer;
36
+ private turnstileWidgetId;
37
+ private config;
38
+ constructor(config: FormHandlerConfig);
39
+ private init;
40
+ private initTurnstile;
41
+ private handleSubmit;
42
+ /**
43
+ * Show a message in the message div
44
+ */
45
+ showMessage(message: string, isSuccess: boolean): void;
46
+ /**
47
+ * Hide the message div
48
+ */
49
+ hideMessage(): void;
50
+ /**
51
+ * Set the loading state of the form
52
+ */
53
+ setLoading(isLoading: boolean): void;
54
+ /**
55
+ * Manually submit the form (useful for custom implementations)
56
+ */
57
+ submit(customData?: Record<string, any>): Promise<FormSubmissionResult>;
58
+ /**
59
+ * Reset the form
60
+ */
61
+ reset(): void;
62
+ /**
63
+ * Get form data as an object
64
+ */
65
+ getFormData(): Record<string, any>;
66
+ }
67
+ /**
68
+ * Initialize all AyeZee forms on the page
69
+ * This automatically finds and initializes all forms with [data-ayezee-form] attribute
70
+ */
71
+ export declare function initAyezeeForms(): void;
72
+ /**
73
+ * Standalone function to submit form data to AyeZee CMS API
74
+ * Useful for custom form implementations that don't use the default form handler
75
+ */
76
+ export declare function submitToAyezeeCms(data: Record<string, any>, config: {
77
+ apiUrl: string;
78
+ apiKey: string;
79
+ turnstileToken?: string;
80
+ }): Promise<FormSubmissionResult>;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Form Handler Utilities for AyeZee CMS Forms
3
+ *
4
+ * Provides core form submission logic, Turnstile integration, and error handling
5
+ * that can be used in custom form implementations or with the default AyezeeForm component
6
+ */
7
+ /**
8
+ * Core form handler class that manages form submission, validation, and Turnstile integration
9
+ */
10
+ export class AyezeeFormHandler {
11
+ form;
12
+ submitButton;
13
+ messageDiv;
14
+ turnstileContainer;
15
+ turnstileWidgetId = null;
16
+ config;
17
+ constructor(config) {
18
+ this.config = config;
19
+ this.form = document.getElementById(config.formId);
20
+ this.submitButton = document.getElementById(`${config.formId}-submit`);
21
+ this.messageDiv = document.getElementById(`${config.formId}-message`);
22
+ this.turnstileContainer = document.getElementById(`${config.formId}-turnstile`);
23
+ if (!this.form) {
24
+ console.error('Form not found:', config.formId);
25
+ return;
26
+ }
27
+ this.init();
28
+ }
29
+ init() {
30
+ // Initialize Turnstile if enabled
31
+ if (this.config.turnstile.enabled && this.turnstileContainer) {
32
+ this.initTurnstile();
33
+ }
34
+ // Attach submit handler
35
+ this.form.addEventListener('submit', (e) => this.handleSubmit(e));
36
+ }
37
+ initTurnstile() {
38
+ if (!window.turnstile) {
39
+ console.warn('Turnstile script not loaded yet, waiting...');
40
+ setTimeout(() => this.initTurnstile(), 100);
41
+ return;
42
+ }
43
+ this.turnstileWidgetId = window.turnstile.render(this.turnstileContainer, {
44
+ sitekey: this.config.turnstile.siteKey,
45
+ theme: 'light',
46
+ size: 'normal',
47
+ });
48
+ }
49
+ async handleSubmit(e) {
50
+ e.preventDefault();
51
+ this.hideMessage();
52
+ this.setLoading(true);
53
+ this.config.onSubmitStart?.();
54
+ try {
55
+ const formData = new FormData(this.form);
56
+ const data = {};
57
+ formData.forEach((value, key) => {
58
+ data[key] = value;
59
+ });
60
+ // Honeypot check
61
+ if (data.website) {
62
+ await new Promise((resolve) => setTimeout(resolve, 1000));
63
+ this.showMessage(this.config.successMessage || 'Form submitted successfully!', true);
64
+ this.form.reset();
65
+ this.config.onSuccess?.(data);
66
+ return;
67
+ }
68
+ // Remove honeypot field before sending to API
69
+ delete data.website;
70
+ // Get Turnstile token if enabled
71
+ if (this.config.turnstile.enabled) {
72
+ const token = window.turnstile?.getResponse(this.turnstileWidgetId);
73
+ if (!token) {
74
+ this.showMessage('Please complete the security challenge.', false);
75
+ this.config.onError?.('Turnstile validation failed');
76
+ return;
77
+ }
78
+ data['cf-turnstile-response'] = token;
79
+ }
80
+ // Submit to API
81
+ console.log('Submitting to:', this.config.apiUrl);
82
+ console.log('Data being sent:', data);
83
+ const response = await fetch(this.config.apiUrl, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ Authorization: `Bearer ${this.config.apiKey}`,
88
+ },
89
+ body: JSON.stringify(data),
90
+ });
91
+ const result = await response.json();
92
+ console.log('API Response:', result);
93
+ if (response.ok && result.success) {
94
+ this.showMessage(this.config.successMessage || 'Form submitted successfully!', true);
95
+ this.form.reset();
96
+ // Reset Turnstile
97
+ if (this.turnstileWidgetId !== null) {
98
+ window.turnstile?.reset(this.turnstileWidgetId);
99
+ }
100
+ this.config.onSuccess?.(result);
101
+ }
102
+ else {
103
+ console.error('Submission failed:', result);
104
+ // Parse validation errors
105
+ let errorMessage = this.config.errorMessage || 'Failed to submit form. Please try again.';
106
+ if (result.details) {
107
+ try {
108
+ const details = typeof result.details === 'string' ? JSON.parse(result.details) : result.details;
109
+ if (details.validationErrors && Array.isArray(details.validationErrors)) {
110
+ errorMessage = details.validationErrors.join('. ');
111
+ }
112
+ }
113
+ catch (e) {
114
+ console.error('Error parsing validation details:', e);
115
+ }
116
+ }
117
+ if (!errorMessage || errorMessage === this.config.errorMessage) {
118
+ errorMessage = result.error || result.message || this.config.errorMessage || 'An error occurred';
119
+ }
120
+ this.showMessage(errorMessage, false);
121
+ this.config.onError?.(errorMessage);
122
+ }
123
+ }
124
+ catch (error) {
125
+ console.error('Form submission error:', error);
126
+ const errorMsg = 'Network error. Please try again.';
127
+ this.showMessage(errorMsg, false);
128
+ this.config.onError?.(errorMsg);
129
+ }
130
+ finally {
131
+ this.setLoading(false);
132
+ this.config.onSubmitEnd?.();
133
+ }
134
+ }
135
+ /**
136
+ * Show a message in the message div
137
+ */
138
+ showMessage(message, isSuccess) {
139
+ if (!this.messageDiv)
140
+ return;
141
+ this.messageDiv.textContent = message;
142
+ this.messageDiv.classList.remove('hidden');
143
+ if (isSuccess) {
144
+ this.messageDiv.classList.remove('border-red-300', 'bg-red-50', 'text-red-700');
145
+ this.messageDiv.classList.add('border-green-300', 'bg-green-50', 'text-green-700');
146
+ }
147
+ else {
148
+ this.messageDiv.classList.remove('border-green-300', 'bg-green-50', 'text-green-700');
149
+ this.messageDiv.classList.add('border-red-300', 'bg-red-50', 'text-red-700');
150
+ }
151
+ }
152
+ /**
153
+ * Hide the message div
154
+ */
155
+ hideMessage() {
156
+ if (!this.messageDiv)
157
+ return;
158
+ this.messageDiv.classList.add('hidden');
159
+ }
160
+ /**
161
+ * Set the loading state of the form
162
+ */
163
+ setLoading(isLoading) {
164
+ if (!this.submitButton)
165
+ return;
166
+ this.submitButton.disabled = isLoading;
167
+ const textSpan = this.submitButton.querySelector('.button-text');
168
+ const loadingSpan = this.submitButton.querySelector('.button-loading');
169
+ if (isLoading) {
170
+ textSpan?.classList.add('hidden');
171
+ loadingSpan?.classList.remove('hidden');
172
+ }
173
+ else {
174
+ textSpan?.classList.remove('hidden');
175
+ loadingSpan?.classList.add('hidden');
176
+ }
177
+ }
178
+ /**
179
+ * Manually submit the form (useful for custom implementations)
180
+ */
181
+ async submit(customData) {
182
+ // If custom data provided, temporarily populate form
183
+ if (customData) {
184
+ Object.entries(customData).forEach(([key, value]) => {
185
+ const input = this.form.querySelector(`[name="${key}"]`);
186
+ if (input) {
187
+ input.value = String(value);
188
+ }
189
+ });
190
+ }
191
+ // Trigger form submission
192
+ const event = new Event('submit', { cancelable: true });
193
+ this.form.dispatchEvent(event);
194
+ return {
195
+ success: true,
196
+ message: 'Form submitted',
197
+ };
198
+ }
199
+ /**
200
+ * Reset the form
201
+ */
202
+ reset() {
203
+ this.form.reset();
204
+ this.hideMessage();
205
+ if (this.turnstileWidgetId !== null) {
206
+ window.turnstile?.reset(this.turnstileWidgetId);
207
+ }
208
+ }
209
+ /**
210
+ * Get form data as an object
211
+ */
212
+ getFormData() {
213
+ const formData = new FormData(this.form);
214
+ const data = {};
215
+ formData.forEach((value, key) => {
216
+ data[key] = value;
217
+ });
218
+ return data;
219
+ }
220
+ }
221
+ /**
222
+ * Initialize all AyeZee forms on the page
223
+ * This automatically finds and initializes all forms with [data-ayezee-form] attribute
224
+ */
225
+ export function initAyezeeForms() {
226
+ const forms = document.querySelectorAll('[data-ayezee-form]');
227
+ forms.forEach((form) => {
228
+ const formId = form.id;
229
+ const config = window[`FORM_CONFIG_${formId}`];
230
+ if (config) {
231
+ new AyezeeFormHandler(config);
232
+ }
233
+ });
234
+ }
235
+ /**
236
+ * Standalone function to submit form data to AyeZee CMS API
237
+ * Useful for custom form implementations that don't use the default form handler
238
+ */
239
+ export async function submitToAyezeeCms(data, config) {
240
+ try {
241
+ const payload = { ...data };
242
+ if (config.turnstileToken) {
243
+ payload['cf-turnstile-response'] = config.turnstileToken;
244
+ }
245
+ const response = await fetch(config.apiUrl, {
246
+ method: 'POST',
247
+ headers: {
248
+ 'Content-Type': 'application/json',
249
+ Authorization: `Bearer ${config.apiKey}`,
250
+ },
251
+ body: JSON.stringify(payload),
252
+ });
253
+ const result = await response.json();
254
+ if (response.ok && result.success) {
255
+ return {
256
+ success: true,
257
+ message: result.message || 'Form submitted successfully!',
258
+ data: result.data,
259
+ };
260
+ }
261
+ else {
262
+ // Parse validation errors
263
+ let errors = [];
264
+ if (result.details) {
265
+ try {
266
+ const details = typeof result.details === 'string' ? JSON.parse(result.details) : result.details;
267
+ if (details.validationErrors && Array.isArray(details.validationErrors)) {
268
+ errors = details.validationErrors;
269
+ }
270
+ }
271
+ catch (e) {
272
+ console.error('Error parsing validation details:', e);
273
+ }
274
+ }
275
+ return {
276
+ success: false,
277
+ message: result.error || result.message || 'Failed to submit form',
278
+ errors: errors.length > 0 ? errors : undefined,
279
+ };
280
+ }
281
+ }
282
+ catch (error) {
283
+ console.error('Form submission error:', error);
284
+ return {
285
+ success: false,
286
+ message: 'Network error. Please try again.',
287
+ };
288
+ }
289
+ }
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@
7
7
  */
8
8
  export * from './cms-helper.js';
9
9
  export * from './cloudinary-utils.js';
10
+ export * from './form-handler.js';
10
11
  export { ayezeeCms, type AyezeeCmsOptions } from './integration.js';
11
12
  export { ayezeeCms as default } from './integration.js';
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@
9
9
  export * from './cms-helper.js';
10
10
  // Export Cloudinary utilities
11
11
  export * from './cloudinary-utils.js';
12
+ // Export form handler utilities
13
+ export * from './form-handler.js';
12
14
  // Export integration
13
15
  export { ayezeeCms } from './integration.js';
14
16
  // Default export
@@ -25,6 +25,11 @@ export interface AyezeeCmsOptions {
25
25
  * @default process.env.PUBLIC_PROJECT_SLUG
26
26
  */
27
27
  projectSlug?: string;
28
+ /**
29
+ * API key or project token for authentication
30
+ * @default process.env.PUBLIC_AYEZEE_API_KEY
31
+ */
32
+ apiKey?: string;
28
33
  /**
29
34
  * Output directory for cached data
30
35
  * @default 'src/data'
@@ -62,6 +62,7 @@ export function ayezeeCms(options = {}) {
62
62
  // Get configuration
63
63
  const cmsDomain = options.cmsDomain || process.env.PUBLIC_CMS_DOMAIN;
64
64
  const projectSlug = options.projectSlug || process.env.PUBLIC_PROJECT_SLUG;
65
+ const apiKey = options.apiKey || process.env.PUBLIC_AYEZEE_API_KEY;
65
66
  const outputDir = options.outputDir || 'src/data';
66
67
  const cacheFileName = options.cacheFileName || 'cms-cache.json';
67
68
  const skipOnError = options.skipOnError || false;
@@ -90,7 +91,11 @@ export function ayezeeCms(options = {}) {
90
91
  const baseUrl = `${domain}/api/v1/projects/${projectSlug}`;
91
92
  // Fetch modules
92
93
  logger.info('📡 Fetching modules...');
93
- const modulesResponse = await fetch(`${baseUrl}/modules`);
94
+ const modulesResponse = await fetch(`${baseUrl}/modules`, {
95
+ headers: {
96
+ ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
97
+ },
98
+ });
94
99
  if (!modulesResponse.ok) {
95
100
  throw new Error(`API error: ${modulesResponse.status} ${modulesResponse.statusText}`);
96
101
  }
@@ -104,7 +109,14 @@ export function ayezeeCms(options = {}) {
104
109
  const modulesWithData = [];
105
110
  for (const module of modules) {
106
111
  try {
107
- const dataResponse = await fetch(`${baseUrl}/modules/${module.instanceKey}/data`);
112
+ const dataResponse = await fetch(`${baseUrl}/modules/${module.instanceKey}/data`, {
113
+ headers: {
114
+ ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
115
+ },
116
+ });
117
+ if (!dataResponse.ok) {
118
+ throw new Error(`HTTP ${dataResponse.status}: ${dataResponse.statusText}`);
119
+ }
108
120
  const dataResult = await dataResponse.json();
109
121
  const itemCount = dataResult.data?.pagination?.total || 0;
110
122
  logger.info(` 📦 ${module.label}: ${itemCount} items`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ayezee-astro-cms",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "AyeZee CMS integration for Astro with automatic data fetching, form handling, and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,6 +17,11 @@
17
17
  "./components": {
18
18
  "types": "./dist/components/index.d.ts",
19
19
  "import": "./dist/components/index.js"
20
+ },
21
+ "./components/AyezeeForm.astro": "./dist/components/AyezeeForm.astro",
22
+ "./form-handler": {
23
+ "types": "./dist/form-handler.d.ts",
24
+ "import": "./dist/form-handler.js"
20
25
  }
21
26
  },
22
27
  "files": [
@@ -24,7 +29,8 @@
24
29
  "README.md"
25
30
  ],
26
31
  "scripts": {
27
- "build": "tsc",
32
+ "build": "tsc && npm run copy-astro",
33
+ "copy-astro": "cp src/components/*.astro dist/components/ 2>/dev/null || true",
28
34
  "dev": "tsc --watch",
29
35
  "prepublishOnly": "npm run build"
30
36
  },