@kimesh/head 0.0.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/README.md +3 -0
- package/augment.d.ts +42 -0
- package/dist/index.d.mts +294 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +430 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.d.mts +19 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +71 -0
- package/dist/plugin.mjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
package/augment.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kimesh/head - Type Augmentation
|
|
3
|
+
*
|
|
4
|
+
* Augments Kimesh types to include head management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Head, MergeHead, VueHeadClient } from '@unhead/vue'
|
|
8
|
+
|
|
9
|
+
import type { KmHead, KmTitle, KmMeta, KmLink, KmScript, KmStyle, KmHtml, KmBody } from '@kimesh/head'
|
|
10
|
+
|
|
11
|
+
declare module '@kimesh/router-runtime' {
|
|
12
|
+
interface KimeshAppContext {
|
|
13
|
+
/** Unhead client instance */
|
|
14
|
+
head: VueHeadClient
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module 'vue' {
|
|
19
|
+
export interface GlobalComponents {
|
|
20
|
+
KmHead: typeof KmHead
|
|
21
|
+
KmTitle: typeof KmTitle
|
|
22
|
+
KmMeta: typeof KmMeta
|
|
23
|
+
KmLink: typeof KmLink
|
|
24
|
+
KmScript: typeof KmScript
|
|
25
|
+
KmStyle: typeof KmStyle
|
|
26
|
+
KmHtml: typeof KmHtml
|
|
27
|
+
KmBody: typeof KmBody
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface KimeshHeadConfig {
|
|
32
|
+
/** Enable head management (default: true) */
|
|
33
|
+
enabled?: boolean
|
|
34
|
+
|
|
35
|
+
/** Global title template */
|
|
36
|
+
titleTemplate?: string | ((title: string) => string)
|
|
37
|
+
|
|
38
|
+
/** Template parameters */
|
|
39
|
+
templateParams?: Record<string, string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type KimeshHeadOptions = MergeHead & Head
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { HeadClient, KIMESH_HEAD_KEY, KimeshHeadPlugin } from "./plugin.mjs";
|
|
2
|
+
import { RouteHeadConfig } from "@kimesh/router-runtime";
|
|
3
|
+
import "@unhead/vue/client";
|
|
4
|
+
import * as vue6 from "vue";
|
|
5
|
+
import { PropType, Ref } from "vue";
|
|
6
|
+
import { ActiveHeadEntry, Head, Head as Head$1, HeadEntryOptions, HeadSafe, MergeHead, MergeHead as MergeHead$1, UseHeadInput, UseHeadInput as UseHeadInput$1, UseHeadSafeInput, UseHeadSafeInput as UseHeadSafeInput$1, UseSeoMetaInput, UseSeoMetaInput as UseSeoMetaInput$1 } from "@unhead/vue";
|
|
7
|
+
|
|
8
|
+
//#region src/composables/use-head.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Set head meta tags with full reactivity support
|
|
11
|
+
*
|
|
12
|
+
* @example Basic usage
|
|
13
|
+
* ```ts
|
|
14
|
+
* useHead({
|
|
15
|
+
* title: 'My Page',
|
|
16
|
+
* meta: [
|
|
17
|
+
* { name: 'description', content: 'Page description' }
|
|
18
|
+
* ]
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Reactive values
|
|
23
|
+
* ```ts
|
|
24
|
+
* const title = ref('Dynamic Title')
|
|
25
|
+
* useHead({
|
|
26
|
+
* title: () => title.value
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function useHead<T extends UseHeadInput$1>(input: T): void;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/composables/use-seo-meta.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Flat API for SEO meta tags
|
|
35
|
+
*
|
|
36
|
+
* Provides a cleaner, flat interface for setting SEO-related meta tags
|
|
37
|
+
* without needing to manually structure the meta array.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* useSeoMeta({
|
|
42
|
+
* title: 'My Site',
|
|
43
|
+
* description: 'Site description',
|
|
44
|
+
* ogTitle: 'Open Graph Title',
|
|
45
|
+
* ogDescription: 'OG Description',
|
|
46
|
+
* ogImage: 'https://example.com/image.png',
|
|
47
|
+
* twitterCard: 'summary_large_image',
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function useSeoMeta(input: UseSeoMetaInput$1): void;
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/composables/use-head-safe.d.ts
|
|
54
|
+
/**
|
|
55
|
+
* XSS-safe version of useHead - sanitizes all input
|
|
56
|
+
*
|
|
57
|
+
* Use this when head values come from user input or external sources.
|
|
58
|
+
* This prevents XSS attacks by sanitizing potentially malicious content.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const userTitle = ref(userInput) // potentially unsafe
|
|
63
|
+
*
|
|
64
|
+
* useHeadSafe({
|
|
65
|
+
* title: userTitle,
|
|
66
|
+
* meta: [
|
|
67
|
+
* { name: 'description', content: userDescription }
|
|
68
|
+
* ]
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function useHeadSafe(input: UseHeadSafeInput$1): void;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/composables/use-route-head.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* Apply head configuration from current route
|
|
77
|
+
*
|
|
78
|
+
* Call this in your root layout/app component to enable route-based head.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```vue
|
|
82
|
+
* <script setup>
|
|
83
|
+
* import { useRouteHead } from '@kimesh/head'
|
|
84
|
+
*
|
|
85
|
+
* useRouteHead()
|
|
86
|
+
* </script>
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function useRouteHead(): Ref<RouteHeadConfig | undefined>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/components/KmHead.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* KmHead - Container component for head tags
|
|
94
|
+
*
|
|
95
|
+
* This is a wrapper component that renders slot content.
|
|
96
|
+
* Use the specific components (KmTitle, KmMeta, etc.) inside.
|
|
97
|
+
*/
|
|
98
|
+
declare const KmHead: vue6.DefineComponent<{}, () => vue6.VNode<vue6.RendererNode, vue6.RendererElement, {
|
|
99
|
+
[key: string]: any;
|
|
100
|
+
}>[] | null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/components/KmTitle.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* KmTitle - Set page title
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```vue
|
|
108
|
+
* <KmTitle>My Page Title</KmTitle>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare const KmTitle: vue6.DefineComponent<{}, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/components/KmMeta.d.ts
|
|
114
|
+
/**
|
|
115
|
+
* KmMeta - Set meta tags
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```vue
|
|
119
|
+
* <KmMeta name="description" content="Page description" />
|
|
120
|
+
* <KmMeta property="og:title" content="OG Title" />
|
|
121
|
+
* <KmMeta charset="utf-8" />
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare const KmMeta: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
125
|
+
name: StringConstructor;
|
|
126
|
+
property: StringConstructor;
|
|
127
|
+
httpEquiv: StringConstructor;
|
|
128
|
+
content: StringConstructor;
|
|
129
|
+
charset: StringConstructor;
|
|
130
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
131
|
+
name: StringConstructor;
|
|
132
|
+
property: StringConstructor;
|
|
133
|
+
httpEquiv: StringConstructor;
|
|
134
|
+
content: StringConstructor;
|
|
135
|
+
charset: StringConstructor;
|
|
136
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/components/KmLink.d.ts
|
|
139
|
+
type AsValue = 'object' | 'audio' | 'document' | 'embed' | 'fetch' | 'font' | 'image' | 'script' | 'style' | 'track' | 'video' | 'worker';
|
|
140
|
+
/**
|
|
141
|
+
* KmLink - Set link tags (stylesheets, preload, favicon, etc.)
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```vue
|
|
145
|
+
* <KmLink rel="stylesheet" href="/styles.css" />
|
|
146
|
+
* <KmLink rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
147
|
+
* <KmLink rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
declare const KmLink: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
151
|
+
rel: StringConstructor;
|
|
152
|
+
href: StringConstructor;
|
|
153
|
+
type: StringConstructor;
|
|
154
|
+
as: PropType<AsValue>;
|
|
155
|
+
crossorigin: PropType<"" | "anonymous" | "use-credentials">;
|
|
156
|
+
media: StringConstructor;
|
|
157
|
+
sizes: StringConstructor;
|
|
158
|
+
hreflang: StringConstructor;
|
|
159
|
+
title: StringConstructor;
|
|
160
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
161
|
+
rel: StringConstructor;
|
|
162
|
+
href: StringConstructor;
|
|
163
|
+
type: StringConstructor;
|
|
164
|
+
as: PropType<AsValue>;
|
|
165
|
+
crossorigin: PropType<"" | "anonymous" | "use-credentials">;
|
|
166
|
+
media: StringConstructor;
|
|
167
|
+
sizes: StringConstructor;
|
|
168
|
+
hreflang: StringConstructor;
|
|
169
|
+
title: StringConstructor;
|
|
170
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/components/KmScript.d.ts
|
|
173
|
+
type ReferrerPolicy = '' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
|
|
174
|
+
/**
|
|
175
|
+
* KmScript - Set script tags
|
|
176
|
+
*
|
|
177
|
+
* @example External script
|
|
178
|
+
* ```vue
|
|
179
|
+
* <KmScript src="https://example.com/script.js" async />
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @example Inline script
|
|
183
|
+
* ```vue
|
|
184
|
+
* <KmScript>console.log('Hello')</KmScript>
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare const KmScript: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
188
|
+
src: StringConstructor;
|
|
189
|
+
type: StringConstructor;
|
|
190
|
+
async: BooleanConstructor;
|
|
191
|
+
defer: BooleanConstructor;
|
|
192
|
+
crossorigin: PropType<"" | "anonymous" | "use-credentials">;
|
|
193
|
+
integrity: StringConstructor;
|
|
194
|
+
nomodule: BooleanConstructor;
|
|
195
|
+
nonce: StringConstructor;
|
|
196
|
+
referrerpolicy: PropType<ReferrerPolicy>;
|
|
197
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
198
|
+
src: StringConstructor;
|
|
199
|
+
type: StringConstructor;
|
|
200
|
+
async: BooleanConstructor;
|
|
201
|
+
defer: BooleanConstructor;
|
|
202
|
+
crossorigin: PropType<"" | "anonymous" | "use-credentials">;
|
|
203
|
+
integrity: StringConstructor;
|
|
204
|
+
nomodule: BooleanConstructor;
|
|
205
|
+
nonce: StringConstructor;
|
|
206
|
+
referrerpolicy: PropType<ReferrerPolicy>;
|
|
207
|
+
}>> & Readonly<{}>, {
|
|
208
|
+
async: boolean;
|
|
209
|
+
defer: boolean;
|
|
210
|
+
nomodule: boolean;
|
|
211
|
+
}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/components/KmStyle.d.ts
|
|
214
|
+
/**
|
|
215
|
+
* KmStyle - Set inline styles
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```vue
|
|
219
|
+
* <KmStyle>
|
|
220
|
+
* body { background: #fff; }
|
|
221
|
+
* </KmStyle>
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example With media query
|
|
225
|
+
* ```vue
|
|
226
|
+
* <KmStyle media="print">
|
|
227
|
+
* body { font-size: 12pt; }
|
|
228
|
+
* </KmStyle>
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
declare const KmStyle: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
232
|
+
type: StringConstructor;
|
|
233
|
+
media: StringConstructor;
|
|
234
|
+
nonce: StringConstructor;
|
|
235
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
236
|
+
type: StringConstructor;
|
|
237
|
+
media: StringConstructor;
|
|
238
|
+
nonce: StringConstructor;
|
|
239
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/components/KmHtml.d.ts
|
|
242
|
+
/**
|
|
243
|
+
* KmHtml - Set HTML element attributes
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```vue
|
|
247
|
+
* <KmHtml lang="en" dir="ltr" />
|
|
248
|
+
* <KmHtml lang="vi" class="dark-mode" />
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare const KmHtml: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
252
|
+
lang: StringConstructor;
|
|
253
|
+
dir: PropType<"ltr" | "rtl" | "auto">;
|
|
254
|
+
class: StringConstructor;
|
|
255
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
256
|
+
lang: StringConstructor;
|
|
257
|
+
dir: PropType<"ltr" | "rtl" | "auto">;
|
|
258
|
+
class: StringConstructor;
|
|
259
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/components/KmBody.d.ts
|
|
262
|
+
/**
|
|
263
|
+
* KmBody - Set body element attributes
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```vue
|
|
267
|
+
* <KmBody class="dark-mode" />
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
declare const KmBody: vue6.DefineComponent<vue6.ExtractPropTypes<{
|
|
271
|
+
class: StringConstructor;
|
|
272
|
+
}>, () => null, {}, {}, {}, vue6.ComponentOptionsMixin, vue6.ComponentOptionsMixin, {}, string, vue6.PublicProps, Readonly<vue6.ExtractPropTypes<{
|
|
273
|
+
class: StringConstructor;
|
|
274
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, vue6.ComponentProvideOptions, true, {}, any>;
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/types/index.d.ts
|
|
277
|
+
/**
|
|
278
|
+
* Kimesh head module configuration
|
|
279
|
+
*/
|
|
280
|
+
interface KimeshHeadConfig {
|
|
281
|
+
/** Enable head management (default: true) */
|
|
282
|
+
enabled?: boolean;
|
|
283
|
+
/** Global title template */
|
|
284
|
+
titleTemplate?: string | ((title: string) => string);
|
|
285
|
+
/** Template parameters */
|
|
286
|
+
templateParams?: Record<string, string>;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Head options that can be set in kimesh.config.ts app.head
|
|
290
|
+
*/
|
|
291
|
+
type KimeshHeadOptions = MergeHead$1 & Head$1;
|
|
292
|
+
//#endregion
|
|
293
|
+
export { type ActiveHeadEntry, type Head, type HeadClient, type HeadEntryOptions, type HeadSafe, KIMESH_HEAD_KEY, type KimeshHeadConfig, type KimeshHeadOptions, KimeshHeadPlugin, KmBody, KmHead, KmHtml, KmLink, KmMeta, KmScript, KmStyle, KmTitle, type MergeHead, type UseHeadInput, type UseHeadSafeInput, type UseSeoMetaInput, useHead, useHeadSafe, useRouteHead, useSeoMeta };
|
|
294
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/composables/use-head.ts","../src/composables/use-seo-meta.ts","../src/composables/use-head-safe.ts","../src/composables/use-route-head.ts","../src/components/KmHead.ts","../src/components/KmTitle.ts","../src/components/KmMeta.ts","../src/components/KmLink.ts","../src/components/KmScript.ts","../src/components/KmStyle.ts","../src/components/KmHtml.ts","../src/components/KmBody.ts","../src/types/index.ts"],"mappings":";;;;;;;;;AA+BA;;;;ACLA;;;;ACAA;;;;ACwCA;;;;ACzDA;;;;iBJsBgB,OAAA,WAAkB,cAAA,CAAA,CAAA,KAAA,EAAqB,CAAA;;;;ACLvD;;;;ACAA;;;;ACwCA;;;;ACzDA;;;;;iBHiBgB,UAAA,CAAA,KAAA,EAAkB,iBAAA;;;;ACAlC;;;;ACwCA;;;;ACzDA;;;;;;;;;iBFiBgB,WAAA,CAAA,KAAA,EAAmB,kBAAA;;;;ACwCnC;;;;ACzDA;;;;;;;;;iBDyDgB,YAAA,CAAA,GAAgB,GAAA,CAAI,eAAA;;;;ACzDpC;;;;;cAAa,MAAA,OAAM,eAAA,WAAA,IAAA,CAAA,KAAA,CAMjB,IAAA,CANiB,YAAA,EAAA,IAAA,CAAA,eAAA;EAAA,CAAA,GAAA;AAAA,yBAAA,IAAA,CAAA,qBAAA;;;;ACcnB;;;;;;;cAAa,OAAA,EAAO,IAAA,CAAA,eAAA,6BAclB,IAAA,CAdkB,qBAAA,EAAA,IAAA,CAAA,qBAAA,cAAA,IAAA,CAAA,WAAA,EAAA,QAAA,OAAA,QAAA,8BAAA,IAAA,CAAA,uBAAA;;;;ACVpB;;;;;;;;;cAAa,MAAA,OAAM,eAAA,MAAA,gBAAA;EAAA,IAAA;;;;;4BAAA,IAAA,CAAA,qBAAA;;;;;;;;;KCVd,OAAA;AAAA;AAYL;;;;;;;;;AAZK,cAYQ,MAAA,OAAM,eAAA,MAAA,gBAAA;EAAA,GAAA;;;MAOD,QAAA,CAAS,OAAA;EAAA,WAAA,EACA,QAAA;EAAA,KAAA;;;;4BARR,IAAA,CAAA,qBAAA;;;;MAOD,QAAA,CAAS,OAAA;EAAA,WAAA,EACA,QAAA;EAAA,KAAA;;;;;;;KCpBtB,cAAA;AAAA;AA2BL;;;;;;;;;;;;AA3BK,cA2BQ,QAAA,OAAQ,eAAA,CAYiB,IAAA,CAZjB,gBAAA;EAAA,GAAA;;;;eAQM,QAAA;EAAA,SAAA;;;kBAIG,QAAA,CAAS,cAAA;AAAA,4BAZlB,IAAA,CAAA,qBAAA,qEAYiB,IAAA,CAAA,gBAAA;EAAA,GAAA;;;;eAJX,QAAA;EAAA,SAAA;;;kBAIG,QAAA,CAAS,cAAA;AAAA;;;;;;;;ACVvC;;;;;;;;;;;;;;;;cAAa,OAAA,OAAO,eAAA,MAAA,gBAAA;EAAA,IAAA;;;4BAAA,IAAA,CAAA,qBAAA;;;;;;;;ACpBpB;;;;;;;;cAAa,MAAA,OAAM,eAAA,MAAA,gBAAA;EAAA,IAAA;OAKA,QAAA;EAAA,KAAA;4BALA,IAAA,CAAA,qBAAA;;OAKA,QAAA;EAAA,KAAA;;;;;ACNnB;;;;;;;cAAa,MAAA,OAAM,eAAA,MAAA,gBAAA;EAAA,KAAA;4BAAA,IAAA,CAAA,qBAAA;;;;;;ACoBnB;AAcA;UAdiB,gBAAA;EAAA;EAAA,OAAA;EAAA;EAAA,aAAA,cAAA,KAAA;EAAA;EAAA,cAAA,GAQE,MAAA;AAAA;AAAA;AAMnB;;AANmB,KAMP,iBAAA,GAAoB,WAAA,GAAY,MAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { KIMESH_HEAD_KEY, KimeshHeadPlugin } from "./plugin.mjs";
|
|
2
|
+
import { tryUseKimeshApp } from "@kimesh/router-runtime";
|
|
3
|
+
import { computed, defineComponent, getCurrentInstance } from "vue";
|
|
4
|
+
import { useHead as useHead$1, useHeadSafe as useHeadSafe$1, useSeoMeta as useSeoMeta$1 } from "@unhead/vue";
|
|
5
|
+
import { useRoute } from "vue-router";
|
|
6
|
+
|
|
7
|
+
//#region src/composables/use-head.ts
|
|
8
|
+
/**
|
|
9
|
+
* @kimesh/head - useHead composable
|
|
10
|
+
*
|
|
11
|
+
* Set head meta tags with full reactivity support.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Set head meta tags with full reactivity support
|
|
15
|
+
*
|
|
16
|
+
* @example Basic usage
|
|
17
|
+
* ```ts
|
|
18
|
+
* useHead({
|
|
19
|
+
* title: 'My Page',
|
|
20
|
+
* meta: [
|
|
21
|
+
* { name: 'description', content: 'Page description' }
|
|
22
|
+
* ]
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Reactive values
|
|
27
|
+
* ```ts
|
|
28
|
+
* const title = ref('Dynamic Title')
|
|
29
|
+
* useHead({
|
|
30
|
+
* title: () => title.value
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
function useHead(input) {
|
|
35
|
+
if (!getCurrentInstance()) {
|
|
36
|
+
const app = tryUseKimeshApp();
|
|
37
|
+
if (app?.head) {
|
|
38
|
+
app.head.push(input);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.warn("[Kimesh Head] useHead called outside component context");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
useHead$1(input);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/composables/use-seo-meta.ts
|
|
49
|
+
/**
|
|
50
|
+
* @kimesh/head - useSeoMeta composable
|
|
51
|
+
*
|
|
52
|
+
* Flat API for SEO meta tags.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Flat API for SEO meta tags
|
|
56
|
+
*
|
|
57
|
+
* Provides a cleaner, flat interface for setting SEO-related meta tags
|
|
58
|
+
* without needing to manually structure the meta array.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* useSeoMeta({
|
|
63
|
+
* title: 'My Site',
|
|
64
|
+
* description: 'Site description',
|
|
65
|
+
* ogTitle: 'Open Graph Title',
|
|
66
|
+
* ogDescription: 'OG Description',
|
|
67
|
+
* ogImage: 'https://example.com/image.png',
|
|
68
|
+
* twitterCard: 'summary_large_image',
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function useSeoMeta(input) {
|
|
73
|
+
useSeoMeta$1(input);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/composables/use-head-safe.ts
|
|
78
|
+
/**
|
|
79
|
+
* @kimesh/head - useHeadSafe composable
|
|
80
|
+
*
|
|
81
|
+
* XSS-safe version of useHead that sanitizes all input.
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* XSS-safe version of useHead - sanitizes all input
|
|
85
|
+
*
|
|
86
|
+
* Use this when head values come from user input or external sources.
|
|
87
|
+
* This prevents XSS attacks by sanitizing potentially malicious content.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const userTitle = ref(userInput) // potentially unsafe
|
|
92
|
+
*
|
|
93
|
+
* useHeadSafe({
|
|
94
|
+
* title: userTitle,
|
|
95
|
+
* meta: [
|
|
96
|
+
* { name: 'description', content: userDescription }
|
|
97
|
+
* ]
|
|
98
|
+
* })
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
function useHeadSafe(input) {
|
|
102
|
+
try {
|
|
103
|
+
useHeadSafe$1(input);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("[Kimesh Head] Failed to set head:", error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/composables/use-route-head.ts
|
|
111
|
+
/**
|
|
112
|
+
* @kimesh/head - useRouteHead composable
|
|
113
|
+
*
|
|
114
|
+
* Applies head configuration from matched routes.
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Get head configuration from route
|
|
118
|
+
*/
|
|
119
|
+
function getRouteHead(route) {
|
|
120
|
+
const heads = [];
|
|
121
|
+
for (const record of route.matched) {
|
|
122
|
+
const routeDef = record.meta?.__kimesh;
|
|
123
|
+
if (!routeDef?.head) continue;
|
|
124
|
+
const headConfig = typeof routeDef.head === "function" ? routeDef.head({
|
|
125
|
+
params: route.params,
|
|
126
|
+
loaderData: void 0,
|
|
127
|
+
route
|
|
128
|
+
}) : routeDef.head;
|
|
129
|
+
heads.push(headConfig);
|
|
130
|
+
}
|
|
131
|
+
if (heads.length === 0) return void 0;
|
|
132
|
+
return heads.reduce((merged, head) => ({
|
|
133
|
+
...merged,
|
|
134
|
+
...head,
|
|
135
|
+
meta: [...merged.meta || [], ...head.meta || []],
|
|
136
|
+
link: [...merged.link || [], ...head.link || []],
|
|
137
|
+
script: [...merged.script || [], ...head.script || []],
|
|
138
|
+
style: [...merged.style || [], ...head.style || []]
|
|
139
|
+
}), {});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Apply head configuration from current route
|
|
143
|
+
*
|
|
144
|
+
* Call this in your root layout/app component to enable route-based head.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```vue
|
|
148
|
+
* <script setup>
|
|
149
|
+
* import { useRouteHead } from '@kimesh/head'
|
|
150
|
+
*
|
|
151
|
+
* useRouteHead()
|
|
152
|
+
* <\/script>
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
function useRouteHead() {
|
|
156
|
+
const route = useRoute();
|
|
157
|
+
const routeHead = computed(() => getRouteHead(route));
|
|
158
|
+
useHead((() => routeHead.value || {}));
|
|
159
|
+
return routeHead;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/components/KmHead.ts
|
|
164
|
+
/**
|
|
165
|
+
* KmHead - Container component for head tags
|
|
166
|
+
*
|
|
167
|
+
* This is a wrapper component that renders slot content.
|
|
168
|
+
* Use the specific components (KmTitle, KmMeta, etc.) inside.
|
|
169
|
+
*/
|
|
170
|
+
const KmHead = defineComponent({
|
|
171
|
+
name: "KmHead",
|
|
172
|
+
setup(_, { slots }) {
|
|
173
|
+
return () => slots.default ? slots.default() : null;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/components/KmTitle.ts
|
|
179
|
+
function extractTextContent$2(vnodes) {
|
|
180
|
+
let text = "";
|
|
181
|
+
for (const vnode of vnodes) if (typeof vnode.children === "string") text += vnode.children;
|
|
182
|
+
else if (Array.isArray(vnode.children)) text += extractTextContent$2(vnode.children);
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* KmTitle - Set page title
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```vue
|
|
190
|
+
* <KmTitle>My Page Title</KmTitle>
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
const KmTitle = defineComponent({
|
|
194
|
+
name: "KmTitle",
|
|
195
|
+
setup(_, { slots }) {
|
|
196
|
+
useHead({ title: () => {
|
|
197
|
+
const content = slots.default?.();
|
|
198
|
+
if (!content) return "";
|
|
199
|
+
return extractTextContent$2(content);
|
|
200
|
+
} });
|
|
201
|
+
return () => null;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/components/KmMeta.ts
|
|
207
|
+
/**
|
|
208
|
+
* KmMeta - Set meta tags
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```vue
|
|
212
|
+
* <KmMeta name="description" content="Page description" />
|
|
213
|
+
* <KmMeta property="og:title" content="OG Title" />
|
|
214
|
+
* <KmMeta charset="utf-8" />
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
const KmMeta = defineComponent({
|
|
218
|
+
name: "KmMeta",
|
|
219
|
+
props: {
|
|
220
|
+
name: String,
|
|
221
|
+
property: String,
|
|
222
|
+
httpEquiv: String,
|
|
223
|
+
content: String,
|
|
224
|
+
charset: String
|
|
225
|
+
},
|
|
226
|
+
setup(props) {
|
|
227
|
+
useHead({ meta: [{
|
|
228
|
+
...props.name && { name: props.name },
|
|
229
|
+
...props.property && { property: props.property },
|
|
230
|
+
...props.httpEquiv && { "http-equiv": props.httpEquiv },
|
|
231
|
+
...props.content && { content: props.content },
|
|
232
|
+
...props.charset && { charset: props.charset }
|
|
233
|
+
}] });
|
|
234
|
+
return () => null;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/components/KmLink.ts
|
|
240
|
+
/**
|
|
241
|
+
* KmLink - Set link tags (stylesheets, preload, favicon, etc.)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```vue
|
|
245
|
+
* <KmLink rel="stylesheet" href="/styles.css" />
|
|
246
|
+
* <KmLink rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
247
|
+
* <KmLink rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
const KmLink = defineComponent({
|
|
251
|
+
name: "KmLink",
|
|
252
|
+
props: {
|
|
253
|
+
rel: String,
|
|
254
|
+
href: String,
|
|
255
|
+
type: String,
|
|
256
|
+
as: String,
|
|
257
|
+
crossorigin: String,
|
|
258
|
+
media: String,
|
|
259
|
+
sizes: String,
|
|
260
|
+
hreflang: String,
|
|
261
|
+
title: String
|
|
262
|
+
},
|
|
263
|
+
setup(props) {
|
|
264
|
+
useHead({ link: [{
|
|
265
|
+
...props.rel && { rel: props.rel },
|
|
266
|
+
...props.href && { href: props.href },
|
|
267
|
+
...props.type && { type: props.type },
|
|
268
|
+
...props.as && { as: props.as },
|
|
269
|
+
...props.crossorigin && { crossorigin: props.crossorigin },
|
|
270
|
+
...props.media && { media: props.media },
|
|
271
|
+
...props.sizes && { sizes: props.sizes },
|
|
272
|
+
...props.hreflang && { hreflang: props.hreflang },
|
|
273
|
+
...props.title && { title: props.title }
|
|
274
|
+
}] });
|
|
275
|
+
return () => null;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/components/KmScript.ts
|
|
281
|
+
function extractTextContent$1(vnodes) {
|
|
282
|
+
let text = "";
|
|
283
|
+
for (const vnode of vnodes) if (typeof vnode.children === "string") text += vnode.children;
|
|
284
|
+
else if (Array.isArray(vnode.children)) text += extractTextContent$1(vnode.children);
|
|
285
|
+
return text;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* KmScript - Set script tags
|
|
289
|
+
*
|
|
290
|
+
* @example External script
|
|
291
|
+
* ```vue
|
|
292
|
+
* <KmScript src="https://example.com/script.js" async />
|
|
293
|
+
* ```
|
|
294
|
+
*
|
|
295
|
+
* @example Inline script
|
|
296
|
+
* ```vue
|
|
297
|
+
* <KmScript>console.log('Hello')</KmScript>
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
const KmScript = defineComponent({
|
|
301
|
+
name: "KmScript",
|
|
302
|
+
props: {
|
|
303
|
+
src: String,
|
|
304
|
+
type: String,
|
|
305
|
+
async: Boolean,
|
|
306
|
+
defer: Boolean,
|
|
307
|
+
crossorigin: String,
|
|
308
|
+
integrity: String,
|
|
309
|
+
nomodule: Boolean,
|
|
310
|
+
nonce: String,
|
|
311
|
+
referrerpolicy: String
|
|
312
|
+
},
|
|
313
|
+
setup(props, { slots }) {
|
|
314
|
+
useHead({ script: [{
|
|
315
|
+
...props.src && { src: props.src },
|
|
316
|
+
...props.type && { type: props.type },
|
|
317
|
+
...props.async && { async: props.async },
|
|
318
|
+
...props.defer && { defer: props.defer },
|
|
319
|
+
...props.crossorigin && { crossorigin: props.crossorigin },
|
|
320
|
+
...props.integrity && { integrity: props.integrity },
|
|
321
|
+
...props.nomodule && { nomodule: props.nomodule },
|
|
322
|
+
...props.nonce && { nonce: props.nonce },
|
|
323
|
+
...props.referrerpolicy && { referrerpolicy: props.referrerpolicy },
|
|
324
|
+
innerHTML: () => {
|
|
325
|
+
const content = slots.default?.();
|
|
326
|
+
if (!content) return void 0;
|
|
327
|
+
return extractTextContent$1(content);
|
|
328
|
+
}
|
|
329
|
+
}] });
|
|
330
|
+
return () => null;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/components/KmStyle.ts
|
|
336
|
+
function extractTextContent(vnodes) {
|
|
337
|
+
let text = "";
|
|
338
|
+
for (const vnode of vnodes) if (typeof vnode.children === "string") text += vnode.children;
|
|
339
|
+
else if (Array.isArray(vnode.children)) text += extractTextContent(vnode.children);
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* KmStyle - Set inline styles
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```vue
|
|
347
|
+
* <KmStyle>
|
|
348
|
+
* body { background: #fff; }
|
|
349
|
+
* </KmStyle>
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* @example With media query
|
|
353
|
+
* ```vue
|
|
354
|
+
* <KmStyle media="print">
|
|
355
|
+
* body { font-size: 12pt; }
|
|
356
|
+
* </KmStyle>
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
const KmStyle = defineComponent({
|
|
360
|
+
name: "KmStyle",
|
|
361
|
+
props: {
|
|
362
|
+
type: String,
|
|
363
|
+
media: String,
|
|
364
|
+
nonce: String
|
|
365
|
+
},
|
|
366
|
+
setup(props, { slots }) {
|
|
367
|
+
useHead({ style: [{
|
|
368
|
+
innerHTML: () => {
|
|
369
|
+
const content = slots.default?.();
|
|
370
|
+
if (!content) return "";
|
|
371
|
+
return extractTextContent(content);
|
|
372
|
+
},
|
|
373
|
+
...props.type && { type: props.type },
|
|
374
|
+
...props.media && { media: props.media },
|
|
375
|
+
...props.nonce && { nonce: props.nonce }
|
|
376
|
+
}] });
|
|
377
|
+
return () => null;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/components/KmHtml.ts
|
|
383
|
+
/**
|
|
384
|
+
* KmHtml - Set HTML element attributes
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```vue
|
|
388
|
+
* <KmHtml lang="en" dir="ltr" />
|
|
389
|
+
* <KmHtml lang="vi" class="dark-mode" />
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
const KmHtml = defineComponent({
|
|
393
|
+
name: "KmHtml",
|
|
394
|
+
props: {
|
|
395
|
+
lang: String,
|
|
396
|
+
dir: String,
|
|
397
|
+
class: String
|
|
398
|
+
},
|
|
399
|
+
setup(props) {
|
|
400
|
+
useHead({ htmlAttrs: {
|
|
401
|
+
...props.lang && { lang: props.lang },
|
|
402
|
+
...props.dir && { dir: props.dir },
|
|
403
|
+
...props.class && { class: props.class }
|
|
404
|
+
} });
|
|
405
|
+
return () => null;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/components/KmBody.ts
|
|
411
|
+
/**
|
|
412
|
+
* KmBody - Set body element attributes
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```vue
|
|
416
|
+
* <KmBody class="dark-mode" />
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
const KmBody = defineComponent({
|
|
420
|
+
name: "KmBody",
|
|
421
|
+
props: { class: String },
|
|
422
|
+
setup(props) {
|
|
423
|
+
useHead({ bodyAttrs: { ...props.class && { class: props.class } } });
|
|
424
|
+
return () => null;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
//#endregion
|
|
429
|
+
export { KIMESH_HEAD_KEY, KimeshHeadPlugin, KmBody, KmHead, KmHtml, KmLink, KmMeta, KmScript, KmStyle, KmTitle, useHead, useHeadSafe, useRouteHead, useSeoMeta };
|
|
430
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["extractTextContent","extractTextContent"],"sources":["../src/composables/use-head.ts","../src/composables/use-seo-meta.ts","../src/composables/use-head-safe.ts","../src/composables/use-route-head.ts","../src/components/KmHead.ts","../src/components/KmTitle.ts","../src/components/KmMeta.ts","../src/components/KmLink.ts","../src/components/KmScript.ts","../src/components/KmStyle.ts","../src/components/KmHtml.ts","../src/components/KmBody.ts"],"sourcesContent":["/**\n * @kimesh/head - useHead composable\n *\n * Set head meta tags with full reactivity support.\n */\n\nimport { useHead as unheadUseHead, type UseHeadInput } from '@unhead/vue'\nimport { getCurrentInstance } from 'vue'\nimport { tryUseKimeshApp } from '@kimesh/router-runtime'\n\n/**\n * Set head meta tags with full reactivity support\n *\n * @example Basic usage\n * ```ts\n * useHead({\n * title: 'My Page',\n * meta: [\n * { name: 'description', content: 'Page description' }\n * ]\n * })\n * ```\n *\n * @example Reactive values\n * ```ts\n * const title = ref('Dynamic Title')\n * useHead({\n * title: () => title.value\n * })\n * ```\n */\nexport function useHead<T extends UseHeadInput>(input: T): void {\n const instance = getCurrentInstance()\n\n if (!instance) {\n const app = tryUseKimeshApp()\n if (app?.head) {\n app.head.push(input as Parameters<typeof app.head.push>[0])\n return\n }\n console.warn('[Kimesh Head] useHead called outside component context')\n return\n }\n\n unheadUseHead(input)\n}\n","/**\n * @kimesh/head - useSeoMeta composable\n *\n * Flat API for SEO meta tags.\n */\n\nimport { useSeoMeta as unheadUseSeoMeta, type UseSeoMetaInput } from '@unhead/vue'\n\n/**\n * Flat API for SEO meta tags\n *\n * Provides a cleaner, flat interface for setting SEO-related meta tags\n * without needing to manually structure the meta array.\n *\n * @example\n * ```ts\n * useSeoMeta({\n * title: 'My Site',\n * description: 'Site description',\n * ogTitle: 'Open Graph Title',\n * ogDescription: 'OG Description',\n * ogImage: 'https://example.com/image.png',\n * twitterCard: 'summary_large_image',\n * })\n * ```\n */\nexport function useSeoMeta(input: UseSeoMetaInput): void {\n unheadUseSeoMeta(input)\n}\n","/**\n * @kimesh/head - useHeadSafe composable\n *\n * XSS-safe version of useHead that sanitizes all input.\n */\n\nimport { useHeadSafe as unheadUseHeadSafe, type UseHeadSafeInput } from '@unhead/vue'\n\n/**\n * XSS-safe version of useHead - sanitizes all input\n *\n * Use this when head values come from user input or external sources.\n * This prevents XSS attacks by sanitizing potentially malicious content.\n *\n * @example\n * ```ts\n * const userTitle = ref(userInput) // potentially unsafe\n *\n * useHeadSafe({\n * title: userTitle,\n * meta: [\n * { name: 'description', content: userDescription }\n * ]\n * })\n * ```\n */\nexport function useHeadSafe(input: UseHeadSafeInput): void {\n try {\n unheadUseHeadSafe(input)\n } catch (error) {\n console.error('[Kimesh Head] Failed to set head:', error)\n }\n}\n","/**\n * @kimesh/head - useRouteHead composable\n *\n * Applies head configuration from matched routes.\n */\n\nimport { computed, watch, type Ref } from 'vue'\nimport { useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'\nimport { useHead } from './use-head'\nimport type { RouteHeadConfig, RouteHeadFn, RouteHeadContext } from '@kimesh/router-runtime'\n\ninterface RouteDefinitionWithHead {\n head?: RouteHeadFn | RouteHeadConfig\n}\n\n/**\n * Get head configuration from route\n */\nfunction getRouteHead(\n route: RouteLocationNormalizedLoaded\n): RouteHeadConfig | undefined {\n // Collect head from all matched routes (parent -> child)\n const heads: RouteHeadConfig[] = []\n\n for (const record of route.matched) {\n const routeDef = record.meta?.__kimesh as RouteDefinitionWithHead | undefined\n if (!routeDef?.head) continue\n\n const headConfig = typeof routeDef.head === 'function'\n ? routeDef.head({\n params: route.params as Record<string, string>,\n loaderData: undefined, // TODO: integrate with loader data\n route,\n })\n : routeDef.head\n\n heads.push(headConfig)\n }\n\n if (heads.length === 0) return undefined\n\n // Merge all heads (later ones override earlier)\n return heads.reduce((merged, head) => ({\n ...merged,\n ...head,\n meta: [...(merged.meta || []), ...(head.meta || [])],\n link: [...(merged.link || []), ...(head.link || [])],\n script: [...(merged.script || []), ...(head.script || [])],\n style: [...(merged.style || []), ...(head.style || [])],\n }), {} as RouteHeadConfig)\n}\n\n/**\n * Apply head configuration from current route\n *\n * Call this in your root layout/app component to enable route-based head.\n *\n * @example\n * ```vue\n * <script setup>\n * import { useRouteHead } from '@kimesh/head'\n *\n * useRouteHead()\n * </script>\n * ```\n */\nexport function useRouteHead(): Ref<RouteHeadConfig | undefined> {\n const route = useRoute()\n\n const routeHead = computed(() => getRouteHead(route))\n\n // Apply head reactively when route changes\n useHead((() => routeHead.value || {}) as Parameters<typeof useHead>[0])\n\n return routeHead\n}\n","import { defineComponent, h, type Slots } from 'vue'\nimport { useHead } from '../composables/use-head'\n\n/**\n * KmHead - Container component for head tags\n *\n * This is a wrapper component that renders slot content.\n * Use the specific components (KmTitle, KmMeta, etc.) inside.\n */\nexport const KmHead = defineComponent({\n name: 'KmHead',\n\n setup(_, { slots }: { slots: Slots }) {\n return () => (slots.default ? slots.default() : null)\n },\n})\n","import { defineComponent, h, watchEffect, type Slots, type VNode } from 'vue'\nimport { useHead } from '../composables/use-head'\n\nfunction extractTextContent(vnodes: VNode[]): string {\n let text = ''\n for (const vnode of vnodes) {\n if (typeof vnode.children === 'string') {\n text += vnode.children\n } else if (Array.isArray(vnode.children)) {\n text += extractTextContent(vnode.children as VNode[])\n }\n }\n return text\n}\n\n/**\n * KmTitle - Set page title\n *\n * @example\n * ```vue\n * <KmTitle>My Page Title</KmTitle>\n * ```\n */\nexport const KmTitle = defineComponent({\n name: 'KmTitle',\n\n setup(_, { slots }: { slots: Slots }) {\n useHead({\n title: () => {\n const content = slots.default?.()\n if (!content) return ''\n return extractTextContent(content)\n },\n })\n\n return () => null\n },\n})\n","import { defineComponent, type PropType } from 'vue'\nimport { useHead } from '../composables/use-head'\n\n/**\n * KmMeta - Set meta tags\n *\n * @example\n * ```vue\n * <KmMeta name=\"description\" content=\"Page description\" />\n * <KmMeta property=\"og:title\" content=\"OG Title\" />\n * <KmMeta charset=\"utf-8\" />\n * ```\n */\nexport const KmMeta = defineComponent({\n name: 'KmMeta',\n\n props: {\n name: String,\n property: String,\n httpEquiv: String,\n content: String,\n charset: String,\n },\n\n setup(props) {\n useHead({\n meta: [\n {\n ...(props.name && { name: props.name }),\n ...(props.property && { property: props.property }),\n ...(props.httpEquiv && { 'http-equiv': props.httpEquiv }),\n ...(props.content && { content: props.content }),\n ...(props.charset && { charset: props.charset }),\n },\n ],\n })\n\n return () => null\n },\n})\n","import { defineComponent, type PropType } from 'vue'\nimport { useHead } from '../composables/use-head'\n\ntype AsValue = 'object' | 'audio' | 'document' | 'embed' | 'fetch' | 'font' | 'image' | 'script' | 'style' | 'track' | 'video' | 'worker'\n\n/**\n * KmLink - Set link tags (stylesheets, preload, favicon, etc.)\n *\n * @example\n * ```vue\n * <KmLink rel=\"stylesheet\" href=\"/styles.css\" />\n * <KmLink rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n * <KmLink rel=\"preload\" href=\"/font.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\" />\n * ```\n */\nexport const KmLink = defineComponent({\n name: 'KmLink',\n\n props: {\n rel: String,\n href: String,\n type: String,\n as: String as PropType<AsValue>,\n crossorigin: String as PropType<'' | 'anonymous' | 'use-credentials'>,\n media: String,\n sizes: String,\n hreflang: String,\n title: String,\n },\n\n setup(props) {\n useHead({\n link: [\n {\n ...(props.rel && { rel: props.rel }),\n ...(props.href && { href: props.href }),\n ...(props.type && { type: props.type }),\n ...(props.as && { as: props.as }),\n ...(props.crossorigin && { crossorigin: props.crossorigin }),\n ...(props.media && { media: props.media }),\n ...(props.sizes && { sizes: props.sizes }),\n ...(props.hreflang && { hreflang: props.hreflang }),\n ...(props.title && { title: props.title }),\n },\n ],\n })\n\n return () => null\n },\n})\n","import { defineComponent, type PropType, type Slots, type VNode } from 'vue'\nimport { useHead } from '../composables/use-head'\n\ntype ReferrerPolicy = '' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'\n\nfunction extractTextContent(vnodes: VNode[]): string {\n let text = ''\n for (const vnode of vnodes) {\n if (typeof vnode.children === 'string') {\n text += vnode.children\n } else if (Array.isArray(vnode.children)) {\n text += extractTextContent(vnode.children as VNode[])\n }\n }\n return text\n}\n\n/**\n * KmScript - Set script tags\n *\n * @example External script\n * ```vue\n * <KmScript src=\"https://example.com/script.js\" async />\n * ```\n *\n * @example Inline script\n * ```vue\n * <KmScript>console.log('Hello')</KmScript>\n * ```\n */\nexport const KmScript = defineComponent({\n name: 'KmScript',\n\n props: {\n src: String,\n type: String,\n async: Boolean,\n defer: Boolean,\n crossorigin: String as PropType<'' | 'anonymous' | 'use-credentials'>,\n integrity: String,\n nomodule: Boolean,\n nonce: String,\n referrerpolicy: String as PropType<ReferrerPolicy>,\n },\n\n setup(props, { slots }: { slots: Slots }) {\n useHead({\n script: [\n {\n ...(props.src && { src: props.src }),\n ...(props.type && { type: props.type }),\n ...(props.async && { async: props.async }),\n ...(props.defer && { defer: props.defer }),\n ...(props.crossorigin && { crossorigin: props.crossorigin }),\n ...(props.integrity && { integrity: props.integrity }),\n ...(props.nomodule && { nomodule: props.nomodule }),\n ...(props.nonce && { nonce: props.nonce }),\n ...(props.referrerpolicy && { referrerpolicy: props.referrerpolicy }),\n innerHTML: () => {\n const content = slots.default?.()\n if (!content) return undefined\n return extractTextContent(content)\n },\n },\n ],\n })\n\n return () => null\n },\n})\n","import { defineComponent, type Slots, type VNode } from 'vue'\nimport { useHead } from '../composables/use-head'\n\nfunction extractTextContent(vnodes: VNode[]): string {\n let text = ''\n for (const vnode of vnodes) {\n if (typeof vnode.children === 'string') {\n text += vnode.children\n } else if (Array.isArray(vnode.children)) {\n text += extractTextContent(vnode.children as VNode[])\n }\n }\n return text\n}\n\n/**\n * KmStyle - Set inline styles\n *\n * @example\n * ```vue\n * <KmStyle>\n * body { background: #fff; }\n * </KmStyle>\n * ```\n *\n * @example With media query\n * ```vue\n * <KmStyle media=\"print\">\n * body { font-size: 12pt; }\n * </KmStyle>\n * ```\n */\nexport const KmStyle = defineComponent({\n name: 'KmStyle',\n\n props: {\n type: String,\n media: String,\n nonce: String,\n },\n\n setup(props, { slots }: { slots: Slots }) {\n useHead({\n style: [\n {\n innerHTML: () => {\n const content = slots.default?.()\n if (!content) return ''\n return extractTextContent(content)\n },\n ...(props.type && { type: props.type }),\n ...(props.media && { media: props.media }),\n ...(props.nonce && { nonce: props.nonce }),\n },\n ],\n })\n\n return () => null\n },\n})\n","import { defineComponent, type PropType } from 'vue'\nimport { useHead } from '../composables/use-head'\n\n/**\n * KmHtml - Set HTML element attributes\n *\n * @example\n * ```vue\n * <KmHtml lang=\"en\" dir=\"ltr\" />\n * <KmHtml lang=\"vi\" class=\"dark-mode\" />\n * ```\n */\nexport const KmHtml = defineComponent({\n name: 'KmHtml',\n\n props: {\n lang: String,\n dir: String as PropType<'ltr' | 'rtl' | 'auto'>,\n class: String,\n },\n\n setup(props) {\n useHead({\n htmlAttrs: {\n ...(props.lang && { lang: props.lang }),\n ...(props.dir && { dir: props.dir }),\n ...(props.class && { class: props.class }),\n },\n })\n\n return () => null\n },\n})\n","import { defineComponent } from 'vue'\nimport { useHead } from '../composables/use-head'\n\n/**\n * KmBody - Set body element attributes\n *\n * @example\n * ```vue\n * <KmBody class=\"dark-mode\" />\n * ```\n */\nexport const KmBody = defineComponent({\n name: 'KmBody',\n\n props: {\n class: String,\n },\n\n setup(props) {\n useHead({\n bodyAttrs: {\n ...(props.class && { class: props.class }),\n },\n })\n\n return () => null\n },\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { VueHeadClient as HeadClient } from "@unhead/vue/client";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* @kimesh/head - Runtime Plugin
|
|
6
|
+
*
|
|
7
|
+
* CSR-only head management plugin using @unhead/vue.
|
|
8
|
+
*/
|
|
9
|
+
declare const KIMESH_HEAD_KEY: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* Kimesh Head Plugin
|
|
12
|
+
*
|
|
13
|
+
* Provides head meta management for Kimesh apps.
|
|
14
|
+
* Uses @unhead/vue for CSR-only head management.
|
|
15
|
+
*/
|
|
16
|
+
declare const KimeshHeadPlugin: any;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { type HeadClient, KIMESH_HEAD_KEY, KimeshHeadPlugin, KimeshHeadPlugin as default };
|
|
19
|
+
//# sourceMappingURL=plugin.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;AAaA;AA6CA;;;cA7Ca,eAAA;AAAA;AA6Cb;;;;;AA7Ca,cA6CA,gBAAA"}
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineKimeshRuntimePlugin } from "@kimesh/router-runtime";
|
|
2
|
+
import { createHead, renderDOMHead } from "@unhead/vue/client";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
|
|
5
|
+
//#region src/plugin.ts
|
|
6
|
+
/**
|
|
7
|
+
* @kimesh/head - Runtime Plugin
|
|
8
|
+
*
|
|
9
|
+
* CSR-only head management plugin using @unhead/vue.
|
|
10
|
+
*/
|
|
11
|
+
const KIMESH_HEAD_KEY = Symbol("kimesh:head");
|
|
12
|
+
/**
|
|
13
|
+
* Get merged head configuration from matched routes
|
|
14
|
+
*/
|
|
15
|
+
function getRouteHead(route) {
|
|
16
|
+
const heads = [];
|
|
17
|
+
for (const record of route.matched) {
|
|
18
|
+
const routeDef = record.meta?.__kimesh;
|
|
19
|
+
if (!routeDef?.head) continue;
|
|
20
|
+
const headConfig = typeof routeDef.head === "function" ? routeDef.head({
|
|
21
|
+
params: route.params,
|
|
22
|
+
loaderData: void 0,
|
|
23
|
+
route
|
|
24
|
+
}) : routeDef.head;
|
|
25
|
+
heads.push(headConfig);
|
|
26
|
+
}
|
|
27
|
+
if (heads.length === 0) return void 0;
|
|
28
|
+
return heads.reduce((merged, head) => ({
|
|
29
|
+
...merged,
|
|
30
|
+
...head,
|
|
31
|
+
meta: [...merged.meta || [], ...head.meta || []],
|
|
32
|
+
link: [...merged.link || [], ...head.link || []],
|
|
33
|
+
script: [...merged.script || [], ...head.script || []],
|
|
34
|
+
style: [...merged.style || [], ...head.style || []]
|
|
35
|
+
}), {});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Kimesh Head Plugin
|
|
39
|
+
*
|
|
40
|
+
* Provides head meta management for Kimesh apps.
|
|
41
|
+
* Uses @unhead/vue for CSR-only head management.
|
|
42
|
+
*/
|
|
43
|
+
const KimeshHeadPlugin = defineKimeshRuntimePlugin({
|
|
44
|
+
name: "kimesh:head",
|
|
45
|
+
enforce: "pre",
|
|
46
|
+
setup(app) {
|
|
47
|
+
const headOptions = app.$config.head;
|
|
48
|
+
const head = createHead();
|
|
49
|
+
app.vueApp.use(head);
|
|
50
|
+
if (headOptions) head.push(headOptions);
|
|
51
|
+
let routeHeadEntry = null;
|
|
52
|
+
app.router.afterEach((to) => {
|
|
53
|
+
if (routeHeadEntry) {
|
|
54
|
+
routeHeadEntry.dispose();
|
|
55
|
+
routeHeadEntry = null;
|
|
56
|
+
}
|
|
57
|
+
const routeHead = getRouteHead(to);
|
|
58
|
+
if (routeHead) routeHeadEntry = head.push(routeHead);
|
|
59
|
+
nextTick(() => {
|
|
60
|
+
renderDOMHead(head);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return { provide: { head } };
|
|
64
|
+
},
|
|
65
|
+
hooks: { "app:mounted": () => {} }
|
|
66
|
+
});
|
|
67
|
+
var plugin_default = KimeshHeadPlugin;
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { KIMESH_HEAD_KEY, KimeshHeadPlugin, plugin_default as default };
|
|
71
|
+
//# sourceMappingURL=plugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * @kimesh/head - Runtime Plugin\n *\n * CSR-only head management plugin using @unhead/vue.\n */\n\nimport { defineKimeshRuntimePlugin } from '@kimesh/router-runtime'\nimport { createHead, renderDOMHead, type VueHeadClient } from '@unhead/vue/client'\nimport { nextTick } from 'vue'\nimport type { RouteLocationNormalizedLoaded } from 'vue-router'\nimport type { KimeshHeadOptions } from './types'\nimport type { RouteHeadConfig } from '@kimesh/router-runtime'\n\nexport const KIMESH_HEAD_KEY = Symbol('kimesh:head')\n\ninterface RouteDefinitionWithHead {\n head?: ((ctx: { params: Record<string, string>; loaderData: unknown; route: RouteLocationNormalizedLoaded }) => RouteHeadConfig) | RouteHeadConfig\n}\n\n/**\n * Get merged head configuration from matched routes\n */\nfunction getRouteHead(route: RouteLocationNormalizedLoaded): RouteHeadConfig | undefined {\n const heads: RouteHeadConfig[] = []\n\n for (const record of route.matched) {\n const routeDef = record.meta?.__kimesh as RouteDefinitionWithHead | undefined\n if (!routeDef?.head) continue\n\n const headConfig = typeof routeDef.head === 'function'\n ? routeDef.head({\n params: route.params as Record<string, string>,\n loaderData: undefined,\n route,\n })\n : routeDef.head\n\n heads.push(headConfig)\n }\n\n if (heads.length === 0) return undefined\n\n return heads.reduce((merged, head) => ({\n ...merged,\n ...head,\n meta: [...(merged.meta || []), ...(head.meta || [])],\n link: [...(merged.link || []), ...(head.link || [])],\n script: [...(merged.script || []), ...(head.script || [])],\n style: [...(merged.style || []), ...(head.style || [])],\n }), {} as RouteHeadConfig)\n}\n\n/**\n * Kimesh Head Plugin\n *\n * Provides head meta management for Kimesh apps.\n * Uses @unhead/vue for CSR-only head management.\n */\nexport const KimeshHeadPlugin = defineKimeshRuntimePlugin<{\n head: VueHeadClient\n}>({\n name: 'kimesh:head',\n enforce: 'pre',\n\n setup(app) {\n const headOptions = app.$config.head as KimeshHeadOptions | undefined\n\n const head = createHead()\n\n app.vueApp.use(head)\n\n if (headOptions) {\n head.push(headOptions)\n }\n\n let routeHeadEntry: ReturnType<typeof head.push> | null = null\n\n app.router.afterEach((to) => {\n if (routeHeadEntry) {\n routeHeadEntry.dispose()\n routeHeadEntry = null\n }\n\n const routeHead = getRouteHead(to)\n if (routeHead) {\n routeHeadEntry = head.push(routeHead as Parameters<typeof head.push>[0])\n }\n\n nextTick(() => {\n renderDOMHead(head)\n })\n })\n\n return {\n provide: { head },\n }\n },\n\n hooks: {\n 'app:mounted': () => {\n },\n },\n})\n\nexport default KimeshHeadPlugin\n\nexport type { VueHeadClient as HeadClient } from '@unhead/vue/client'\n"],"mappings":";;;;;;;;;;AAaA,MAAa,kBAAkB,OAAO,cAAc;;;;AASpD,SAAS,aAAa,OAAmE;CACvF,MAAM,QAA2B,EAAE;AAEnC,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,MAAM,WAAW,OAAO,MAAM;AAC9B,MAAI,CAAC,UAAU,KAAM;EAErB,MAAM,aAAa,OAAO,SAAS,SAAS,aACxC,SAAS,KAAK;GACZ,QAAQ,MAAM;GACd,YAAY;GACZ;GACD,CAAC,GACF,SAAS;AAEb,QAAM,KAAK,WAAW;;AAGxB,KAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAO,MAAM,QAAQ,QAAQ,UAAU;EACrC,GAAG;EACH,GAAG;EACH,MAAM,CAAC,GAAI,OAAO,QAAQ,EAAE,EAAG,GAAI,KAAK,QAAQ,EAAE,CAAE;EACpD,MAAM,CAAC,GAAI,OAAO,QAAQ,EAAE,EAAG,GAAI,KAAK,QAAQ,EAAE,CAAE;EACpD,QAAQ,CAAC,GAAI,OAAO,UAAU,EAAE,EAAG,GAAI,KAAK,UAAU,EAAE,CAAE;EAC1D,OAAO,CAAC,GAAI,OAAO,SAAS,EAAE,EAAG,GAAI,KAAK,SAAS,EAAE,CAAE;EACxD,GAAG,EAAE,CAAoB;;;;;;;;AAS5B,MAAa,mBAAmB,0BAE7B;CACD,MAAM;CACN,SAAS;CAET,MAAM,KAAK;EACT,MAAM,cAAc,IAAI,QAAQ;EAEhC,MAAM,OAAO,YAAY;AAEzB,MAAI,OAAO,IAAI,KAAK;AAEpB,MAAI,YACF,MAAK,KAAK,YAAY;EAGxB,IAAI,iBAAsD;AAE1D,MAAI,OAAO,WAAW,OAAO;AAC3B,OAAI,gBAAgB;AAClB,mBAAe,SAAS;AACxB,qBAAiB;;GAGnB,MAAM,YAAY,aAAa,GAAG;AAClC,OAAI,UACF,kBAAiB,KAAK,KAAK,UAA6C;AAG1E,kBAAe;AACb,kBAAc,KAAK;KACnB;IACF;AAEF,SAAO,EACL,SAAS,EAAE,MAAM,EAClB;;CAGH,OAAO,EACL,qBAAqB,IAEtB;CACF,CAAC;AAEF,qBAAe"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kimesh/head",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Head meta management for Kimesh framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"import": "./dist/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./plugin": {
|
|
12
|
+
"types": "./dist/plugin.d.mts",
|
|
13
|
+
"import": "./dist/plugin.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./augment": {
|
|
16
|
+
"types": "./augment.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"augment.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"dev": "tsdown --watch",
|
|
28
|
+
"typecheck": "vue-tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@unhead/vue": "^2.0.8"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"vue": "^3.5.0",
|
|
37
|
+
"@kimesh/router-runtime": "0.0.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@kimesh/router-runtime": "0.0.1",
|
|
41
|
+
"@types/node": "^25.0.8",
|
|
42
|
+
"@vue/test-utils": "^2.4.6",
|
|
43
|
+
"jsdom": "^27.4.0",
|
|
44
|
+
"tsdown": "^0.20.0-beta.3",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.0.17",
|
|
47
|
+
"vue": "^3.5.26",
|
|
48
|
+
"vue-router": "^4.6.4",
|
|
49
|
+
"vue-tsc": "^3.2.2"
|
|
50
|
+
}
|
|
51
|
+
}
|