@devsantara/head 0.2.0 → 0.3.0
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 +123 -7
- package/dist/adapters/index.d.ts +1 -1
- package/dist/index.d.ts +61 -20
- package/dist/index.js +190 -91
- package/dist/{types-Cvpk_Zha.d.ts → types-D-ta-0P-.d.ts} +14 -4
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -60,7 +60,12 @@ import { HeadBuilder } from '@devsantara/head';
|
|
|
60
60
|
const head = new HeadBuilder()
|
|
61
61
|
.addTitle('My Awesome Website')
|
|
62
62
|
.addDescription('A comprehensive guide to web development')
|
|
63
|
+
.addStyle('body { margin: 0; padding: 0; }')
|
|
63
64
|
.addViewport({ width: 'device-width', initialScale: 1 })
|
|
65
|
+
.addScript({ code: 'console.log("Hello, world!");' })
|
|
66
|
+
.addScript(new URL('https://devsantara.com/assets/scripts/utils.js'), {
|
|
67
|
+
async: true,
|
|
68
|
+
})
|
|
64
69
|
.build();
|
|
65
70
|
```
|
|
66
71
|
|
|
@@ -85,6 +90,28 @@ const head = new HeadBuilder()
|
|
|
85
90
|
content: 'width=device-width, initial-scale=1',
|
|
86
91
|
},
|
|
87
92
|
},
|
|
93
|
+
{
|
|
94
|
+
type: 'style',
|
|
95
|
+
attributes: {
|
|
96
|
+
type: 'text/css',
|
|
97
|
+
children: 'body { margin: 0; padding: 0; }',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'script',
|
|
102
|
+
attributes: {
|
|
103
|
+
type: 'text/javascript',
|
|
104
|
+
children: 'console.log("Hello, world!");',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'script',
|
|
109
|
+
attributes: {
|
|
110
|
+
type: 'text/javascript',
|
|
111
|
+
src: 'https://devsantara.com/assets/scripts/utils.js',
|
|
112
|
+
async: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
88
115
|
];
|
|
89
116
|
```
|
|
90
117
|
|
|
@@ -140,6 +167,95 @@ const head = new HeadBuilder({
|
|
|
140
167
|
];
|
|
141
168
|
```
|
|
142
169
|
|
|
170
|
+
### With Templated Title
|
|
171
|
+
|
|
172
|
+
Set a title template with a default value, then pass page-specific titles as strings. The builder automatically applies the saved template to subsequent title updates:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { HeadBuilder } from '@devsantara/head';
|
|
176
|
+
|
|
177
|
+
// Create a builder and set title template with default
|
|
178
|
+
// The template stays active for all future addTitle() calls
|
|
179
|
+
const sharedHead = new HeadBuilder().addTitle({
|
|
180
|
+
template: '%s | My Awesome site', // Store template (%s is the placeholder)
|
|
181
|
+
default: 'Home', // Initial title using template
|
|
182
|
+
});
|
|
183
|
+
// Output: <title>Home | My Awesome site</title>
|
|
184
|
+
|
|
185
|
+
// Update title for Posts page
|
|
186
|
+
// Pass a string, builder applies the saved template automatically
|
|
187
|
+
const postHead = sharedHead.addTitle('Posts').build();
|
|
188
|
+
// Output: <title>Posts | My Awesome site</title>
|
|
189
|
+
|
|
190
|
+
// Update title for About page
|
|
191
|
+
// Template is still active from the first addTitle() call
|
|
192
|
+
const aboutHead = sharedHead.addTitle('About Us').build();
|
|
193
|
+
// Output: <title>About Us | My Awesome site</title>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**How it works:**
|
|
197
|
+
|
|
198
|
+
1. First `addTitle()` with template object stores the template internally
|
|
199
|
+
2. Subsequent `addTitle()` calls with strings automatically use the stored template
|
|
200
|
+
3. The `%s` placeholder gets replaced with your page title
|
|
201
|
+
4. Each title replaces the previous one (deduplication)
|
|
202
|
+
|
|
203
|
+
### With Element Deduplication
|
|
204
|
+
|
|
205
|
+
HeadBuilder automatically deduplicates elements—when you add an element matching an existing one, the new one replaces the old:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { HeadBuilder } from '@devsantara/head';
|
|
209
|
+
|
|
210
|
+
const head = new HeadBuilder()
|
|
211
|
+
.addTitle('My Site')
|
|
212
|
+
.addTitle('Updated Title') // Replaces previous title
|
|
213
|
+
|
|
214
|
+
.addDescription('First description')
|
|
215
|
+
.addDescription('Updated description') // Replaces previous
|
|
216
|
+
|
|
217
|
+
.addMeta({ name: 'keywords', content: 'web, development' })
|
|
218
|
+
.addMeta({ name: 'author', content: 'John Doe' }) // Separate meta tags coexist
|
|
219
|
+
|
|
220
|
+
.addCanonical('https://devsantara.com/page1')
|
|
221
|
+
.addCanonical('https://devsantara.com/page2') // Replaces previous canonical
|
|
222
|
+
|
|
223
|
+
.build();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Output (HeadElement[]):
|
|
228
|
+
[
|
|
229
|
+
{ type: 'title', attributes: { children: 'Updated Title' } },
|
|
230
|
+
{
|
|
231
|
+
type: 'meta',
|
|
232
|
+
attributes: { name: 'description', content: 'Updated description' },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'meta',
|
|
236
|
+
attributes: { name: 'keywords', content: 'web, development' },
|
|
237
|
+
},
|
|
238
|
+
{ type: 'meta', attributes: { name: 'author', content: 'John Doe' } },
|
|
239
|
+
{
|
|
240
|
+
type: 'link',
|
|
241
|
+
attributes: { rel: 'canonical', href: 'https://devsantara.com/page2' },
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**How it works:**
|
|
247
|
+
|
|
248
|
+
- **Title**: Only one per document
|
|
249
|
+
- **Meta by name**: One per unique `name` attribute (e.g., description, keywords)
|
|
250
|
+
- **Meta by property**: One per unique `property` attribute (e.g., `og:title`, `og:description`)
|
|
251
|
+
- **Charset**: Only one per document
|
|
252
|
+
- **Canonical**: Only one per document
|
|
253
|
+
- **Manifest**: Only one per document
|
|
254
|
+
- **Alternate locales**: One per unique language code
|
|
255
|
+
- **Other tags**: Deduplicated by exact attribute match
|
|
256
|
+
|
|
257
|
+
This ensures clean metadata without accidental duplicates.
|
|
258
|
+
|
|
143
259
|
### With React Adapter
|
|
144
260
|
|
|
145
261
|
```tsx
|
|
@@ -222,13 +338,13 @@ export const Route = createRootRoute({
|
|
|
222
338
|
|
|
223
339
|
For advanced use cases not covered by the essential methods below, use these basic methods to add any custom element directly.
|
|
224
340
|
|
|
225
|
-
| Method
|
|
226
|
-
|
|
|
227
|
-
| `addTitle(title: string)`
|
|
228
|
-
| `addMeta(attributes: HeadAttributeTypeMap['meta'])`
|
|
229
|
-
| `addLink(
|
|
230
|
-
| `addScript(
|
|
231
|
-
| `addStyle(
|
|
341
|
+
| Method | Description |
|
|
342
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
343
|
+
| `addTitle(title: string \| TitleOptions)` | Adds a `<title>` element with optional templating |
|
|
344
|
+
| `addMeta(attributes: HeadAttributeTypeMap['meta'])` | Adds a `<meta>` element with custom attributes |
|
|
345
|
+
| `addLink(href: string \| URL, attributes?)` | Adds a `<link>` element with a URL and custom attributes |
|
|
346
|
+
| `addScript(srcOrCode: string \| URL \| { code: string }, attributes?)` | Adds a `<script>` element (external file with string/URL or inline with `{ code: string }`) |
|
|
347
|
+
| `addStyle(css: string, attributes?)` | Adds a `<style>` element with inline CSS |
|
|
232
348
|
|
|
233
349
|
### Essential Methods
|
|
234
350
|
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as HeadMetaAttributes, i as HeadAdapter, l as HeadScriptAttributes, o as HeadElement, s as HeadLinkAttributes, u as HeadStyleAttributes } from "../types-
|
|
1
|
+
import { c as HeadMetaAttributes, i as HeadAdapter, l as HeadScriptAttributes, o as HeadElement, s as HeadLinkAttributes, u as HeadStyleAttributes } from "../types-D-ta-0P-.js";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/react-adapter.d.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as TitleOptions, a as HeadAttributeTypeMap, c as HeadMetaAttributes, d as HeadTitleAttributes, f as IconOptions, g as StylesheetOptions, h as RobotsOptions, i as HeadAdapter, l as HeadScriptAttributes, m as OpenGraphOptions, n as CharSet, o as HeadElement, p as IconPreset, r as ColorScheme, s as HeadLinkAttributes, t as AlternateLocaleOptions, u as HeadStyleAttributes, v as TwitterOptions, y as ViewportOptions } from "./types-D-ta-0P-.js";
|
|
2
2
|
|
|
3
3
|
//#region src/builder.d.ts
|
|
4
4
|
/**
|
|
@@ -18,9 +18,25 @@ interface BuilderHelper {
|
|
|
18
18
|
*/
|
|
19
19
|
type BuilderOption<T> = T | ((helper: BuilderHelper) => T);
|
|
20
20
|
declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
21
|
+
/**
|
|
22
|
+
* Optional base URL for resolving relative URLs in metadata (Open Graph, canonical, etc.)
|
|
23
|
+
*/
|
|
21
24
|
private metadataBase?;
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Optional adapter to transform the built head elements into a framework-specific format.
|
|
27
|
+
* If not provided, `build()` returns `HeadElement[]`
|
|
28
|
+
*/
|
|
23
29
|
private adapter?;
|
|
30
|
+
/**
|
|
31
|
+
* Internal storage for title options to support templated titles.
|
|
32
|
+
* This allows the builder to generate the title dynamically based on previously set options.
|
|
33
|
+
*/
|
|
34
|
+
private titleOptions?;
|
|
35
|
+
/**
|
|
36
|
+
* Internal collection of head elements being built,
|
|
37
|
+
* stored in a Map for deduplication based on element type and key attributes.
|
|
38
|
+
*/
|
|
39
|
+
private elementsMap;
|
|
24
40
|
/**
|
|
25
41
|
* Resolves a value that can be either static or a callback function receiving helper utilities.
|
|
26
42
|
*
|
|
@@ -55,11 +71,20 @@ declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
|
55
71
|
*/
|
|
56
72
|
private resolveUrl;
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
74
|
+
* Generates a unique key for a head element based on its type and attributes.
|
|
75
|
+
* Used for deduplication - elements with the same key replace previous ones.
|
|
76
|
+
*
|
|
77
|
+
* @param element - The head element to generate a key for
|
|
78
|
+
* @returns A unique string key for the element
|
|
79
|
+
*/
|
|
80
|
+
private getElementKey;
|
|
81
|
+
/**
|
|
82
|
+
* Adds a head element to the internal collection with deduplication.
|
|
83
|
+
* Elements with the same key will replace previous ones.
|
|
59
84
|
*
|
|
60
85
|
* @param type - The HTML element type
|
|
61
86
|
* @param attributes - The element's attributes
|
|
62
|
-
* @returns The
|
|
87
|
+
* @returns The unique key for the added element
|
|
63
88
|
*/
|
|
64
89
|
private addElement;
|
|
65
90
|
/**
|
|
@@ -75,41 +100,48 @@ declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
|
75
100
|
*/
|
|
76
101
|
addMeta(attributes: HeadAttributeTypeMap['meta']): this;
|
|
77
102
|
/**
|
|
78
|
-
* Adds a custom link element with any valid attributes.
|
|
103
|
+
* Adds a custom link element with any valid attributes.
|
|
79
104
|
*
|
|
80
|
-
* @param
|
|
105
|
+
* @param href - The URL to link to
|
|
106
|
+
* @param attributes - Additional link element attributes
|
|
81
107
|
* @returns The builder instance for method chaining
|
|
82
108
|
*
|
|
83
109
|
* @example
|
|
84
110
|
* new HeadBuilder()
|
|
85
|
-
* .addLink(
|
|
111
|
+
* .addLink('https://fonts.googleapis.com', { rel: 'preconnect' })
|
|
86
112
|
* .build();
|
|
87
113
|
*/
|
|
88
|
-
addLink(
|
|
114
|
+
addLink(href: string | URL, attributes?: Omit<HeadAttributeTypeMap['link'], 'href'>): this;
|
|
89
115
|
/**
|
|
90
|
-
* Adds a
|
|
116
|
+
* Adds a script element, either inline code or an external file.
|
|
91
117
|
*
|
|
92
|
-
* @param
|
|
118
|
+
* @param srcOrCode - Script source: a URL string/object for external files, or `{ code: string }` for inline scripts
|
|
119
|
+
* @param attributes - Additional script attributes (async, defer, integrity, etc.)
|
|
93
120
|
* @returns The builder instance for method chaining
|
|
94
121
|
*
|
|
95
122
|
* @example
|
|
96
123
|
* new HeadBuilder()
|
|
97
|
-
* .addScript(
|
|
124
|
+
* .addScript('/script.js')
|
|
125
|
+
* .addScript(new URL('https://devsantara.com/script.js'), { async: true })
|
|
126
|
+
* .addScript({ code: 'console.log("Hello, World!")' })
|
|
98
127
|
* .build();
|
|
99
128
|
*/
|
|
100
|
-
addScript(
|
|
129
|
+
addScript(srcOrCode: string | URL | {
|
|
130
|
+
code: string;
|
|
131
|
+
}, attributes?: Omit<HeadAttributeTypeMap['script'], 'children' | 'src'>): this;
|
|
101
132
|
/**
|
|
102
|
-
* Adds
|
|
133
|
+
* Adds an inline style element with CSS code.
|
|
103
134
|
*
|
|
104
|
-
* @param
|
|
135
|
+
* @param css - The inline CSS code
|
|
136
|
+
* @param attributes - Additional style attributes
|
|
105
137
|
* @returns The builder instance for method chaining
|
|
106
138
|
*
|
|
107
139
|
* @example
|
|
108
140
|
* new HeadBuilder()
|
|
109
|
-
* .addStyle(
|
|
141
|
+
* .addStyle('body { margin: 0; padding: 0; }')
|
|
110
142
|
* .build();
|
|
111
143
|
*/
|
|
112
|
-
addStyle(
|
|
144
|
+
addStyle(css: string, attributes?: Omit<HeadAttributeTypeMap['style'], 'children'>): this;
|
|
113
145
|
/**
|
|
114
146
|
* Adds a character encoding declaration to specify how the document should be interpreted.
|
|
115
147
|
*
|
|
@@ -136,16 +168,25 @@ declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
|
136
168
|
addColorScheme(colorScheme: ColorScheme): this;
|
|
137
169
|
/**
|
|
138
170
|
* Adds a title element that appears in browser tabs, search results, and bookmarks.
|
|
171
|
+
* Supports both simple string titles and templated titles with dynamic substitution.
|
|
139
172
|
*
|
|
140
|
-
* @param title - The document title
|
|
173
|
+
* @param title - The document title as a string, or TitleOptions object with template and default
|
|
141
174
|
* @returns The builder instance for method chaining
|
|
142
175
|
*
|
|
143
176
|
* @example
|
|
177
|
+
* // Simple title
|
|
144
178
|
* new HeadBuilder()
|
|
145
179
|
* .addTitle('My Awesome Website')
|
|
146
180
|
* .build();
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Templated title with page-specific suffix
|
|
184
|
+
* const baseHead = new HeadBuilder()
|
|
185
|
+
* .addTitle({ template: '%s | My Site', default: 'Home' })
|
|
186
|
+
*
|
|
187
|
+
* const head = baseHead.addTitle('About Us').build(); // Results in title "About Us | My Site"
|
|
147
188
|
*/
|
|
148
|
-
addTitle(title: string): this;
|
|
189
|
+
addTitle(title: string | TitleOptions): this;
|
|
149
190
|
/**
|
|
150
191
|
* Adds viewport configuration for responsive web design and mobile optimization.
|
|
151
192
|
*
|
|
@@ -232,7 +273,7 @@ declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
|
232
273
|
* @returns The builder instance for method chaining
|
|
233
274
|
*
|
|
234
275
|
* @example
|
|
235
|
-
* new HeadBuilder({ metadataBase: new URL('https://
|
|
276
|
+
* new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
|
|
236
277
|
* .addAlternateLocale((helper) => ({
|
|
237
278
|
* 'en-US': helper.resolveUrl('/en'),
|
|
238
279
|
* 'fr-FR': helper.resolveUrl('/fr'),
|
|
@@ -290,4 +331,4 @@ declare class HeadBuilder<TOutput = HeadElement[]> {
|
|
|
290
331
|
build(): TOutput;
|
|
291
332
|
}
|
|
292
333
|
//#endregion
|
|
293
|
-
export { AlternateLocaleOptions, CharSet, ColorScheme, HeadAdapter, HeadAttributeTypeMap, HeadBuilder, HeadElement, HeadLinkAttributes, HeadMetaAttributes, HeadScriptAttributes, HeadStyleAttributes, HeadTitleAttributes, IconOptions, IconPreset, OpenGraphOptions, RobotsOptions, StylesheetOptions, TwitterOptions, ViewportOptions };
|
|
334
|
+
export { AlternateLocaleOptions, CharSet, ColorScheme, HeadAdapter, HeadAttributeTypeMap, HeadBuilder, HeadElement, HeadLinkAttributes, HeadMetaAttributes, HeadScriptAttributes, HeadStyleAttributes, HeadTitleAttributes, IconOptions, IconPreset, OpenGraphOptions, RobotsOptions, StylesheetOptions, TitleOptions, TwitterOptions, ViewportOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
//#region src/builder.ts
|
|
2
2
|
var HeadBuilder = class {
|
|
3
|
+
/**
|
|
4
|
+
* Optional base URL for resolving relative URLs in metadata (Open Graph, canonical, etc.)
|
|
5
|
+
*/
|
|
3
6
|
metadataBase;
|
|
4
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Optional adapter to transform the built head elements into a framework-specific format.
|
|
9
|
+
* If not provided, `build()` returns `HeadElement[]`
|
|
10
|
+
*/
|
|
5
11
|
adapter;
|
|
6
12
|
/**
|
|
13
|
+
* Internal storage for title options to support templated titles.
|
|
14
|
+
* This allows the builder to generate the title dynamically based on previously set options.
|
|
15
|
+
*/
|
|
16
|
+
titleOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Internal collection of head elements being built,
|
|
19
|
+
* stored in a Map for deduplication based on element type and key attributes.
|
|
20
|
+
*/
|
|
21
|
+
elementsMap = /* @__PURE__ */ new Map();
|
|
22
|
+
/**
|
|
7
23
|
* Resolves a value that can be either static or a callback function receiving helper utilities.
|
|
8
24
|
*
|
|
9
25
|
* @param valueOrFn - Static value or function that returns the value
|
|
@@ -41,25 +57,47 @@ var HeadBuilder = class {
|
|
|
41
57
|
resolveUrl(url) {
|
|
42
58
|
if (url instanceof URL) return url.href;
|
|
43
59
|
if (!this.metadataBase) return url;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
return new URL(url, this.metadataBase).href;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generates a unique key for a head element based on its type and attributes.
|
|
64
|
+
* Used for deduplication - elements with the same key replace previous ones.
|
|
65
|
+
*
|
|
66
|
+
* @param element - The head element to generate a key for
|
|
67
|
+
* @returns A unique string key for the element
|
|
68
|
+
*/
|
|
69
|
+
getElementKey({ type, attributes }) {
|
|
70
|
+
if (type === "title") return "title";
|
|
71
|
+
if (type === "meta") {
|
|
72
|
+
if ("charSet" in attributes) return "meta:charSet";
|
|
73
|
+
if ("name" in attributes && "content" in attributes) return `meta:name:${attributes.name}`;
|
|
74
|
+
if ("property" in attributes && "content" in attributes) return `meta:property:${attributes.property}`;
|
|
75
|
+
}
|
|
76
|
+
if (type === "link") {
|
|
77
|
+
if (attributes.rel === "canonical") return "link:canonical";
|
|
78
|
+
if (attributes.rel === "manifest") return "link:manifest";
|
|
79
|
+
if (attributes.rel === "alternate" && "hrefLang" in attributes) return `link:alternate:${attributes.hrefLang}`;
|
|
48
80
|
}
|
|
81
|
+
return JSON.stringify(`${type}:${JSON.stringify(attributes)}`);
|
|
49
82
|
}
|
|
50
83
|
/**
|
|
51
|
-
* Adds a head element to the internal collection
|
|
84
|
+
* Adds a head element to the internal collection with deduplication.
|
|
85
|
+
* Elements with the same key will replace previous ones.
|
|
52
86
|
*
|
|
53
87
|
* @param type - The HTML element type
|
|
54
88
|
* @param attributes - The element's attributes
|
|
55
|
-
* @returns The
|
|
89
|
+
* @returns The unique key for the added element
|
|
56
90
|
*/
|
|
57
91
|
addElement(type, attributes) {
|
|
58
|
-
this.
|
|
92
|
+
const key = this.getElementKey({
|
|
59
93
|
type,
|
|
60
94
|
attributes
|
|
61
95
|
});
|
|
62
|
-
|
|
96
|
+
this.elementsMap.set(key, {
|
|
97
|
+
type,
|
|
98
|
+
attributes
|
|
99
|
+
});
|
|
100
|
+
return key;
|
|
63
101
|
}
|
|
64
102
|
/**
|
|
65
103
|
* Adds a custom meta element with any valid attributes. Use this for meta tags without dedicated helper methods.
|
|
@@ -73,49 +111,77 @@ var HeadBuilder = class {
|
|
|
73
111
|
* .build();
|
|
74
112
|
*/
|
|
75
113
|
addMeta(attributes) {
|
|
76
|
-
|
|
114
|
+
this.addElement("meta", attributes);
|
|
115
|
+
return this;
|
|
77
116
|
}
|
|
78
117
|
/**
|
|
79
|
-
* Adds a custom link element with any valid attributes.
|
|
118
|
+
* Adds a custom link element with any valid attributes.
|
|
80
119
|
*
|
|
81
|
-
* @param
|
|
120
|
+
* @param href - The URL to link to
|
|
121
|
+
* @param attributes - Additional link element attributes
|
|
82
122
|
* @returns The builder instance for method chaining
|
|
83
123
|
*
|
|
84
124
|
* @example
|
|
85
125
|
* new HeadBuilder()
|
|
86
|
-
* .addLink(
|
|
126
|
+
* .addLink('https://fonts.googleapis.com', { rel: 'preconnect' })
|
|
87
127
|
* .build();
|
|
88
128
|
*/
|
|
89
|
-
addLink(attributes) {
|
|
90
|
-
|
|
129
|
+
addLink(href, attributes) {
|
|
130
|
+
this.addElement("link", {
|
|
131
|
+
href: href.toString(),
|
|
132
|
+
...attributes
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
91
135
|
}
|
|
92
136
|
/**
|
|
93
|
-
* Adds a
|
|
137
|
+
* Adds a script element, either inline code or an external file.
|
|
94
138
|
*
|
|
95
|
-
* @param
|
|
139
|
+
* @param srcOrCode - Script source: a URL string/object for external files, or `{ code: string }` for inline scripts
|
|
140
|
+
* @param attributes - Additional script attributes (async, defer, integrity, etc.)
|
|
96
141
|
* @returns The builder instance for method chaining
|
|
97
142
|
*
|
|
98
143
|
* @example
|
|
99
144
|
* new HeadBuilder()
|
|
100
|
-
* .addScript(
|
|
145
|
+
* .addScript('/script.js')
|
|
146
|
+
* .addScript(new URL('https://devsantara.com/script.js'), { async: true })
|
|
147
|
+
* .addScript({ code: 'console.log("Hello, World!")' })
|
|
101
148
|
* .build();
|
|
102
149
|
*/
|
|
103
|
-
addScript(attributes) {
|
|
104
|
-
|
|
150
|
+
addScript(srcOrCode, attributes) {
|
|
151
|
+
if (typeof srcOrCode === "object" && "code" in srcOrCode) {
|
|
152
|
+
this.addElement("script", {
|
|
153
|
+
children: srcOrCode.code,
|
|
154
|
+
type: "text/javascript",
|
|
155
|
+
...attributes
|
|
156
|
+
});
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
this.addElement("script", {
|
|
160
|
+
src: srcOrCode.toString(),
|
|
161
|
+
type: "text/javascript",
|
|
162
|
+
...attributes
|
|
163
|
+
});
|
|
164
|
+
return this;
|
|
105
165
|
}
|
|
106
166
|
/**
|
|
107
|
-
* Adds
|
|
167
|
+
* Adds an inline style element with CSS code.
|
|
108
168
|
*
|
|
109
|
-
* @param
|
|
169
|
+
* @param css - The inline CSS code
|
|
170
|
+
* @param attributes - Additional style attributes
|
|
110
171
|
* @returns The builder instance for method chaining
|
|
111
172
|
*
|
|
112
173
|
* @example
|
|
113
174
|
* new HeadBuilder()
|
|
114
|
-
* .addStyle(
|
|
175
|
+
* .addStyle('body { margin: 0; padding: 0; }')
|
|
115
176
|
* .build();
|
|
116
177
|
*/
|
|
117
|
-
addStyle(attributes) {
|
|
118
|
-
|
|
178
|
+
addStyle(css, attributes) {
|
|
179
|
+
this.addElement("style", {
|
|
180
|
+
children: css,
|
|
181
|
+
type: "text/css",
|
|
182
|
+
...attributes
|
|
183
|
+
});
|
|
184
|
+
return this;
|
|
119
185
|
}
|
|
120
186
|
/**
|
|
121
187
|
* Adds a character encoding declaration to specify how the document should be interpreted.
|
|
@@ -129,7 +195,8 @@ var HeadBuilder = class {
|
|
|
129
195
|
* .build();
|
|
130
196
|
*/
|
|
131
197
|
addCharSet(charSet) {
|
|
132
|
-
|
|
198
|
+
this.addElement("meta", { charSet });
|
|
199
|
+
return this;
|
|
133
200
|
}
|
|
134
201
|
/**
|
|
135
202
|
* Adds a color scheme preference indicating which color schemes the page supports for proper rendering.
|
|
@@ -143,24 +210,50 @@ var HeadBuilder = class {
|
|
|
143
210
|
* .build();
|
|
144
211
|
*/
|
|
145
212
|
addColorScheme(colorScheme) {
|
|
146
|
-
|
|
213
|
+
this.addElement("meta", {
|
|
147
214
|
name: "color-scheme",
|
|
148
215
|
content: colorScheme
|
|
149
216
|
});
|
|
217
|
+
return this;
|
|
150
218
|
}
|
|
151
219
|
/**
|
|
152
220
|
* Adds a title element that appears in browser tabs, search results, and bookmarks.
|
|
221
|
+
* Supports both simple string titles and templated titles with dynamic substitution.
|
|
153
222
|
*
|
|
154
|
-
* @param title - The document title
|
|
223
|
+
* @param title - The document title as a string, or TitleOptions object with template and default
|
|
155
224
|
* @returns The builder instance for method chaining
|
|
156
225
|
*
|
|
157
226
|
* @example
|
|
227
|
+
* // Simple title
|
|
158
228
|
* new HeadBuilder()
|
|
159
229
|
* .addTitle('My Awesome Website')
|
|
160
230
|
* .build();
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* // Templated title with page-specific suffix
|
|
234
|
+
* const baseHead = new HeadBuilder()
|
|
235
|
+
* .addTitle({ template: '%s | My Site', default: 'Home' })
|
|
236
|
+
*
|
|
237
|
+
* const head = baseHead.addTitle('About Us').build(); // Results in title "About Us | My Site"
|
|
161
238
|
*/
|
|
162
239
|
addTitle(title) {
|
|
163
|
-
|
|
240
|
+
if (typeof title === "string") {
|
|
241
|
+
/**
|
|
242
|
+
* If title is provided as a string and titleOptions with a template exists,
|
|
243
|
+
* we generate the title using the template. This allows dynamic title generation based on previously set options.
|
|
244
|
+
* If no template is set, we use the raw title string as is.
|
|
245
|
+
*/
|
|
246
|
+
const titleText = this.titleOptions ? this.titleOptions.template.replace("%s", title) : title;
|
|
247
|
+
this.addElement("title", { children: titleText });
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* If title is provided as an object with template and default,
|
|
252
|
+
* we store the options and generate the title using the template with default.
|
|
253
|
+
*/
|
|
254
|
+
this.titleOptions = title;
|
|
255
|
+
this.addElement("title", { children: this.titleOptions.template.replace("%s", title.default) });
|
|
256
|
+
return this;
|
|
164
257
|
}
|
|
165
258
|
/**
|
|
166
259
|
* Adds viewport configuration for responsive web design and mobile optimization.
|
|
@@ -183,10 +276,11 @@ var HeadBuilder = class {
|
|
|
183
276
|
if (options.userScalable !== void 0) contentParts.push(`user-scalable=${options.userScalable ? "yes" : "no"}`);
|
|
184
277
|
if (options.viewportFit !== void 0) contentParts.push(`viewport-fit=${options.viewportFit}`);
|
|
185
278
|
if (options.interactiveWidget !== void 0) contentParts.push(`interactive-widget=${options.interactiveWidget}`);
|
|
186
|
-
|
|
279
|
+
this.addElement("meta", {
|
|
187
280
|
name: "viewport",
|
|
188
281
|
content: contentParts.join(", ")
|
|
189
282
|
});
|
|
283
|
+
return this;
|
|
190
284
|
}
|
|
191
285
|
/**
|
|
192
286
|
* Adds a description that appears in search engine results and social media previews.
|
|
@@ -200,10 +294,11 @@ var HeadBuilder = class {
|
|
|
200
294
|
* .build();
|
|
201
295
|
*/
|
|
202
296
|
addDescription(description) {
|
|
203
|
-
|
|
297
|
+
this.addElement("meta", {
|
|
204
298
|
name: "description",
|
|
205
299
|
content: description
|
|
206
300
|
});
|
|
301
|
+
return this;
|
|
207
302
|
}
|
|
208
303
|
/**
|
|
209
304
|
* Adds a canonical URL to help search engines identify the preferred version of a page and prevent duplicate content issues.
|
|
@@ -218,10 +313,11 @@ var HeadBuilder = class {
|
|
|
218
313
|
*/
|
|
219
314
|
addCanonical(valueOrFn) {
|
|
220
315
|
const value = this.parseValueOrFn(valueOrFn);
|
|
221
|
-
|
|
316
|
+
this.addElement("link", {
|
|
222
317
|
rel: "canonical",
|
|
223
318
|
href: value.toString()
|
|
224
319
|
});
|
|
320
|
+
return this;
|
|
225
321
|
}
|
|
226
322
|
/**
|
|
227
323
|
* Adds robots directives to control search engine crawling and indexing behavior.
|
|
@@ -243,10 +339,11 @@ var HeadBuilder = class {
|
|
|
243
339
|
else if (typeof value === "string" || typeof value === "number") directiveParts.push(`${key}:${value}`);
|
|
244
340
|
else if (value) directiveParts.push(key);
|
|
245
341
|
}
|
|
246
|
-
|
|
342
|
+
this.addElement("meta", {
|
|
247
343
|
name: "robots",
|
|
248
344
|
content: directiveParts.join(", ")
|
|
249
345
|
});
|
|
346
|
+
return this;
|
|
250
347
|
}
|
|
251
348
|
/**
|
|
252
349
|
* Adds Open Graph metadata for rich social media previews on platforms like Facebook, LinkedIn, and Slack.
|
|
@@ -264,51 +361,51 @@ var HeadBuilder = class {
|
|
|
264
361
|
* .build();
|
|
265
362
|
*/
|
|
266
363
|
addOpenGraph(valueOrFn) {
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
364
|
+
const value = this.parseValueOrFn(valueOrFn);
|
|
365
|
+
if (value.title) this.addElement("meta", {
|
|
269
366
|
property: "og:title",
|
|
270
|
-
content:
|
|
367
|
+
content: value.title
|
|
271
368
|
});
|
|
272
|
-
if (
|
|
369
|
+
if (value.description) this.addElement("meta", {
|
|
273
370
|
property: "og:description",
|
|
274
|
-
content:
|
|
371
|
+
content: value.description
|
|
275
372
|
});
|
|
276
|
-
if (
|
|
373
|
+
if (value.url) this.addElement("meta", {
|
|
277
374
|
property: "og:url",
|
|
278
|
-
content:
|
|
375
|
+
content: value.url.toString()
|
|
279
376
|
});
|
|
280
|
-
if (
|
|
377
|
+
if (value.locale) this.addElement("meta", {
|
|
281
378
|
property: "og:locale",
|
|
282
|
-
content:
|
|
379
|
+
content: value.locale
|
|
283
380
|
});
|
|
284
|
-
if (
|
|
381
|
+
if (value.image) {
|
|
285
382
|
this.addElement("meta", {
|
|
286
383
|
property: "og:image",
|
|
287
|
-
content:
|
|
384
|
+
content: value.image.url.toString()
|
|
288
385
|
});
|
|
289
|
-
if (
|
|
386
|
+
if (value.image.alt) this.addElement("meta", {
|
|
290
387
|
property: "og:image:alt",
|
|
291
|
-
content:
|
|
388
|
+
content: value.image.alt
|
|
292
389
|
});
|
|
293
|
-
if (
|
|
390
|
+
if (value.image.type) this.addElement("meta", {
|
|
294
391
|
property: "og:image:type",
|
|
295
|
-
content:
|
|
392
|
+
content: value.image.type
|
|
296
393
|
});
|
|
297
|
-
if (
|
|
394
|
+
if (value.image.width) this.addElement("meta", {
|
|
298
395
|
property: "og:image:width",
|
|
299
|
-
content:
|
|
396
|
+
content: value.image.width.toString()
|
|
300
397
|
});
|
|
301
|
-
if (
|
|
398
|
+
if (value.image.height) this.addElement("meta", {
|
|
302
399
|
property: "og:image:height",
|
|
303
|
-
content:
|
|
400
|
+
content: value.image.height.toString()
|
|
304
401
|
});
|
|
305
402
|
}
|
|
306
|
-
if (
|
|
403
|
+
if (value.type) {
|
|
307
404
|
this.addElement("meta", {
|
|
308
405
|
property: "og:type",
|
|
309
|
-
content:
|
|
406
|
+
content: value.type.name
|
|
310
407
|
});
|
|
311
|
-
if ("properties" in
|
|
408
|
+
if ("properties" in value.type) for (const typeProperty of value.type.properties) this.addElement("meta", {
|
|
312
409
|
property: typeProperty.name,
|
|
313
410
|
content: typeProperty.content
|
|
314
411
|
});
|
|
@@ -330,47 +427,47 @@ var HeadBuilder = class {
|
|
|
330
427
|
* .build();
|
|
331
428
|
*/
|
|
332
429
|
addTwitter(valueOrFn) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
430
|
+
const value = this.parseValueOrFn(valueOrFn);
|
|
431
|
+
if (value.title) this.addElement("meta", {
|
|
335
432
|
name: "twitter:title",
|
|
336
|
-
content:
|
|
433
|
+
content: value.title
|
|
337
434
|
});
|
|
338
|
-
if (
|
|
435
|
+
if (value.description) this.addElement("meta", {
|
|
339
436
|
name: "twitter:description",
|
|
340
|
-
content:
|
|
437
|
+
content: value.description
|
|
341
438
|
});
|
|
342
|
-
if (
|
|
439
|
+
if (value.site) this.addElement("meta", {
|
|
343
440
|
name: "twitter:site",
|
|
344
|
-
content:
|
|
441
|
+
content: value.site
|
|
345
442
|
});
|
|
346
|
-
if (
|
|
443
|
+
if (value.siteId) this.addElement("meta", {
|
|
347
444
|
name: "twitter:site:id",
|
|
348
|
-
content:
|
|
445
|
+
content: value.siteId
|
|
349
446
|
});
|
|
350
|
-
if (
|
|
447
|
+
if (value.creator) this.addElement("meta", {
|
|
351
448
|
name: "twitter:creator",
|
|
352
|
-
content:
|
|
449
|
+
content: value.creator
|
|
353
450
|
});
|
|
354
|
-
if (
|
|
451
|
+
if (value.creatorId) this.addElement("meta", {
|
|
355
452
|
name: "twitter:creator:id",
|
|
356
|
-
content:
|
|
453
|
+
content: value.creatorId
|
|
357
454
|
});
|
|
358
|
-
if (
|
|
455
|
+
if (value.image) {
|
|
359
456
|
this.addElement("meta", {
|
|
360
457
|
name: "twitter:image",
|
|
361
|
-
content:
|
|
458
|
+
content: value.image.url.toString()
|
|
362
459
|
});
|
|
363
|
-
if (
|
|
460
|
+
if (value.image.alt) this.addElement("meta", {
|
|
364
461
|
name: "twitter:image:alt",
|
|
365
|
-
content:
|
|
462
|
+
content: value.image.alt
|
|
366
463
|
});
|
|
367
464
|
}
|
|
368
|
-
if (
|
|
465
|
+
if (value.card) {
|
|
369
466
|
this.addElement("meta", {
|
|
370
467
|
name: "twitter:card",
|
|
371
|
-
content:
|
|
468
|
+
content: value.card.name
|
|
372
469
|
});
|
|
373
|
-
if ("properties" in
|
|
470
|
+
if ("properties" in value.card) for (const cardProperty of value.card.properties) this.addElement("meta", {
|
|
374
471
|
name: cardProperty.name,
|
|
375
472
|
content: cardProperty.content.toString()
|
|
376
473
|
});
|
|
@@ -384,7 +481,7 @@ var HeadBuilder = class {
|
|
|
384
481
|
* @returns The builder instance for method chaining
|
|
385
482
|
*
|
|
386
483
|
* @example
|
|
387
|
-
* new HeadBuilder({ metadataBase: new URL('https://
|
|
484
|
+
* new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
|
|
388
485
|
* .addAlternateLocale((helper) => ({
|
|
389
486
|
* 'en-US': helper.resolveUrl('/en'),
|
|
390
487
|
* 'fr-FR': helper.resolveUrl('/fr'),
|
|
@@ -393,8 +490,8 @@ var HeadBuilder = class {
|
|
|
393
490
|
* .build();
|
|
394
491
|
*/
|
|
395
492
|
addAlternateLocale(valueOrFn) {
|
|
396
|
-
const
|
|
397
|
-
for (const [lang, href] of Object.entries(
|
|
493
|
+
const value = this.parseValueOrFn(valueOrFn);
|
|
494
|
+
for (const [lang, href] of Object.entries(value)) this.addElement("link", {
|
|
398
495
|
rel: "alternate",
|
|
399
496
|
hrefLang: lang,
|
|
400
497
|
href: String(href)
|
|
@@ -413,11 +510,12 @@ var HeadBuilder = class {
|
|
|
413
510
|
* .build();
|
|
414
511
|
*/
|
|
415
512
|
addManifest(valueOrFn) {
|
|
416
|
-
const
|
|
417
|
-
|
|
513
|
+
const value = this.parseValueOrFn(valueOrFn);
|
|
514
|
+
this.addElement("link", {
|
|
418
515
|
rel: "manifest",
|
|
419
|
-
href:
|
|
516
|
+
href: value.toString()
|
|
420
517
|
});
|
|
518
|
+
return this;
|
|
421
519
|
}
|
|
422
520
|
/**
|
|
423
521
|
* Adds an external CSS stylesheet link to the page.
|
|
@@ -432,11 +530,13 @@ var HeadBuilder = class {
|
|
|
432
530
|
* .build();
|
|
433
531
|
*/
|
|
434
532
|
addStylesheet(href, options) {
|
|
435
|
-
|
|
533
|
+
this.addElement("link", {
|
|
436
534
|
rel: "stylesheet",
|
|
535
|
+
type: "text/css",
|
|
437
536
|
href: href.toString(),
|
|
438
537
|
...options
|
|
439
538
|
});
|
|
539
|
+
return this;
|
|
440
540
|
}
|
|
441
541
|
/**
|
|
442
542
|
* Adds a favicon or app icon using preset types or custom rel values.
|
|
@@ -454,20 +554,18 @@ var HeadBuilder = class {
|
|
|
454
554
|
* .build();
|
|
455
555
|
*/
|
|
456
556
|
addIcon(preset, valueOrFn) {
|
|
457
|
-
const
|
|
557
|
+
const { href, ...value } = this.parseValueOrFn(valueOrFn);
|
|
458
558
|
const rel = {
|
|
459
559
|
apple: "apple-touch-icon",
|
|
460
560
|
icon: "icon",
|
|
461
561
|
shortcut: "shortcut icon"
|
|
462
562
|
}[preset] || preset;
|
|
463
|
-
|
|
563
|
+
this.addElement("link", {
|
|
464
564
|
rel,
|
|
465
|
-
href:
|
|
466
|
-
|
|
467
|
-
sizes: options.sizes,
|
|
468
|
-
media: options.media,
|
|
469
|
-
fetchPriority: options.fetchPriority
|
|
565
|
+
href: href.toString(),
|
|
566
|
+
...value
|
|
470
567
|
});
|
|
568
|
+
return this;
|
|
471
569
|
}
|
|
472
570
|
/**
|
|
473
571
|
* Builds and returns the final head configuration. Returns adapted output if an adapter was provided, otherwise returns `HeadElement[]`.
|
|
@@ -475,8 +573,9 @@ var HeadBuilder = class {
|
|
|
475
573
|
* @returns The head configuration in the target format
|
|
476
574
|
*/
|
|
477
575
|
build() {
|
|
478
|
-
|
|
479
|
-
return this.elements;
|
|
576
|
+
const elements = Array.from(this.elementsMap.values());
|
|
577
|
+
if (this.adapter) return this.adapter.transform(elements);
|
|
578
|
+
return elements;
|
|
480
579
|
}
|
|
481
580
|
};
|
|
482
581
|
|
|
@@ -54,6 +54,13 @@ type CharSet = 'utf-8' | (string & {});
|
|
|
54
54
|
* Color scheme preference indicating which color schemes the document supports.
|
|
55
55
|
*/
|
|
56
56
|
type ColorScheme = 'light' | 'dark' | 'light dark' | 'dark light' | 'only light' | 'only dark' | 'normal' | (string & {});
|
|
57
|
+
/**
|
|
58
|
+
* Title configuration with support for templated titles using a template string and default title value.
|
|
59
|
+
*/
|
|
60
|
+
type TitleOptions = {
|
|
61
|
+
template: string;
|
|
62
|
+
default: string;
|
|
63
|
+
};
|
|
57
64
|
/**
|
|
58
65
|
* Viewport configuration for responsive web design and mobile optimization.
|
|
59
66
|
*/
|
|
@@ -430,13 +437,16 @@ interface TwitterOptions {
|
|
|
430
437
|
card?: TwitterCard;
|
|
431
438
|
}
|
|
432
439
|
/**
|
|
433
|
-
* Locale key type supporting
|
|
440
|
+
* Locale key type supporting specific locale strings or custom values.
|
|
434
441
|
*/
|
|
435
|
-
type AlternateLocaleKey<TLocale extends string> =
|
|
442
|
+
type AlternateLocaleKey<TLocale extends string> = TLocale | (string & {});
|
|
436
443
|
/**
|
|
437
444
|
* Alternate locale/language mapping for internationalization, linking language codes to their corresponding URLs.
|
|
445
|
+
* The 'x-default' key is optional.
|
|
438
446
|
*/
|
|
439
|
-
type AlternateLocaleOptions<TLocale extends string> =
|
|
447
|
+
type AlternateLocaleOptions<TLocale extends string> = {
|
|
448
|
+
'x-default'?: string | URL;
|
|
449
|
+
} & Record<AlternateLocaleKey<TLocale>, string | URL>;
|
|
440
450
|
/**
|
|
441
451
|
* Icon preset type with autocomplete for common icon types while allowing custom values.
|
|
442
452
|
*/
|
|
@@ -452,4 +462,4 @@ type IconOptions = Omit<HeadAttributeTypeMap['link'], 'rel' | 'href'> & {
|
|
|
452
462
|
*/
|
|
453
463
|
type StylesheetOptions = Omit<HeadLinkAttributes, 'rel' | 'href'>;
|
|
454
464
|
//#endregion
|
|
455
|
-
export {
|
|
465
|
+
export { TitleOptions as _, HeadAttributeTypeMap as a, HeadMetaAttributes as c, HeadTitleAttributes as d, IconOptions as f, StylesheetOptions as g, RobotsOptions as h, HeadAdapter as i, HeadScriptAttributes as l, OpenGraphOptions as m, CharSet as n, HeadElement as o, IconPreset as p, ColorScheme as r, HeadLinkAttributes as s, AlternateLocaleOptions as t, HeadStyleAttributes as u, TwitterOptions as v, ViewportOptions as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devsantara/head",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A type-safe HTML head builder",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devsantara",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@changesets/cli": "^2.29.8",
|
|
38
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
39
|
+
"@vitest/ui": "4.0.18",
|
|
38
40
|
"oxfmt": "^0.27.0",
|
|
39
41
|
"oxlint": "^1.42.0",
|
|
40
42
|
"oxlint-tsgolint": "^0.11.4",
|
|
@@ -50,6 +52,8 @@
|
|
|
50
52
|
"build": "tsdown",
|
|
51
53
|
"dev": "tsdown --watch",
|
|
52
54
|
"test": "vitest",
|
|
55
|
+
"test:ui": "vitest --ui",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
53
57
|
"lint": "oxlint --type-aware",
|
|
54
58
|
"lint:fix": "oxlint --type-aware --fix",
|
|
55
59
|
"lint:ts": "tsc --noEmit",
|