@unhead/cli 3.0.5
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/LICENSE +21 -0
- package/README.md +123 -0
- package/bin/unhead.mjs +7 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.mjs +1356 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Harlan Wilton <harlan@harlanzw.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# `@unhead/cli`
|
|
2
|
+
|
|
3
|
+
Command-line tools to audit and migrate unhead head usage in your codebase. Source-level checks run on a native [oxc-parser](https://oxc.rs) AST so `.js` / `.ts` / `.tsx` / `.vue` / `.svelte` files lint with zero parser configuration. Rendered-HTML checks reuse the runtime `ValidatePlugin`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @unhead/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The binary is installed as `unhead`.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### `unhead audit [globs...]`
|
|
16
|
+
|
|
17
|
+
Lint your source for unhead misuse using the recommended rule set.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
unhead audit # default: **/*.{js,ts,vue,svelte,...}
|
|
21
|
+
unhead audit src/**/*.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Vue and Svelte single-file components are handled by extracting each `<script>` block and parsing it with oxc; diagnostics report the original file's line/column. `nuxt.config.ts`'s `app.head` block is treated as a `useHead` input and audited the same way. No `eslint`, `typescript-eslint`, or `vue-eslint-parser` install is needed.
|
|
25
|
+
|
|
26
|
+
Exits with code 1 when any rule fires at `error` severity. Warnings and info findings don't block CI.
|
|
27
|
+
|
|
28
|
+
The output also includes:
|
|
29
|
+
|
|
30
|
+
- A green `✓` line for every file with `useHead` / `useSeoMeta` usage and zero diagnostics, with a per-file call breakdown (e.g. `useHead ×2, useSeoMeta`). Confirms which files were actually scanned vs silently skipped.
|
|
31
|
+
- A `parse-error` warning for any script block oxc-parser refused (typically a real TS/JS syntax error in the file), so you don't mistake a parse failure for a clean run.
|
|
32
|
+
- A **Title consistency** section (see below) that surfaces project-wide title-format observations.
|
|
33
|
+
|
|
34
|
+
### Project insights
|
|
35
|
+
|
|
36
|
+
In addition to per-file lints, `audit` runs a few cross-file checks:
|
|
37
|
+
|
|
38
|
+
- **`page-missing-head`** (`info`) — flags any file under `**/pages/**/*.vue` that doesn't call `useHead` / `useSeoMeta` directly *or* via a project composable that transitively does. The transitive part comes from a fixpoint over the project's call graph, so wrappers like `useDefaultMeta()` → `useHead()` count as coverage.
|
|
39
|
+
- **`prefer-use-seo-meta`** (`warning`, autofixable) — fires on `useHead` calls that only set `title` / `description` / `meta:[…]`. The flat `useSeoMeta` shape is fully typed against `MetaFlat`, so a typo like `name: 'descriptipon'` becomes a TypeScript error instead of silently shipping a broken meta tag. `migrate` rewrites the call in place.
|
|
40
|
+
- **Title consistency** — collects every static `title:` / `titleTemplate:` literal from `useHead`, `useSeoMeta`, `defineNuxtConfig`, **and** any project composable that the call-graph fixpoint identifies as head-providing (e.g. `useToolSeo({ title: '…' })`). Reports:
|
|
41
|
+
- mixed separators across pages (`" | "` vs `" - "` vs `" · "`),
|
|
42
|
+
- a common trailing suffix shared by ≥50% of titles (suggests extracting via `titleTemplate` + `templateParams.siteName`),
|
|
43
|
+
- the suffix being duplicated when `titleTemplate` is *already* set (titles will render the suffix twice),
|
|
44
|
+
- mixing literal titles with titles that already use `%templateParams`.
|
|
45
|
+
|
|
46
|
+
Each finding pairs the observation with a concrete `templateParams` / `titleTemplate` migration hint.
|
|
47
|
+
|
|
48
|
+
### `unhead migrate [globs...]`
|
|
49
|
+
|
|
50
|
+
Apply autofixes for v2-to-v3 migration plus the type-safety upgrades: rewrites deprecated props (`children` → `innerHTML`, `hid`/`vmid` → `key`, `body: true` → `tagPosition: 'bodyClose'`), prepends missing `@` on Twitter handles, removes redundant `defer` on module scripts, adds `crossorigin` to font preloads, wraps tag object literals in their `defineX` helper for type narrowing, and converts meta-only `useHead` calls to `useSeoMeta` for typed key autocompletion.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
unhead migrate
|
|
54
|
+
unhead migrate --dry-run # report fixable count without writing
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `unhead validate-html [globs...]`
|
|
58
|
+
|
|
59
|
+
Run the runtime `ValidatePlugin` over prerendered HTML files. Catches the cross-tag and rendered-output rules that lint can't see.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
unhead validate-html '.output/public/**/*.html'
|
|
63
|
+
unhead validate-html dist/index.html --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Exits with code 1 when any rule fires at `warn` severity. Runtime `ValidatePlugin` rules don't have an `error` tier (`audit` does), so this is the strictest CI gate; downgrade specific rules to `info` or `off` via the plugin options if you want them non-blocking.
|
|
67
|
+
|
|
68
|
+
### `unhead validate-url <url>`
|
|
69
|
+
|
|
70
|
+
Fetch a live URL and run the runtime `ValidatePlugin` over its `<head>`.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
unhead validate-url https://example.com
|
|
74
|
+
unhead validate-url https://example.com --json
|
|
75
|
+
unhead validate-url https://example.com --user-agent 'Twitterbot/1.0'
|
|
76
|
+
unhead validate-url https://example.com --timeout 10000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The default user agent is `facebookexternalhit/1.1` so social-crawler-aware rules (e.g. `meta-beyond-1mb`) engage on the response. The fetch is aborted after `--timeout` milliseconds (default 30000) and non-HTML responses fail fast.
|
|
80
|
+
|
|
81
|
+
Exits with code 1 when any rule fires at `warn` severity (runtime rules don't expose an `error` tier).
|
|
82
|
+
|
|
83
|
+
## What runs where
|
|
84
|
+
|
|
85
|
+
unhead validation lives in three layers; each catches a different class of issue. The CLI gives you one entry point to all three.
|
|
86
|
+
|
|
87
|
+
| Rule class | Source-level lint (`audit` / `migrate`) | HTML lint (`validate-html` / `validate-url`) | Runtime `ValidatePlugin` in your app |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| Typos in meta `name` / `property` | ✓ | ✓ | ✓ |
|
|
90
|
+
| Deprecated v2 props (`children`, `hid`, `body`) | ✓ | — | ✓ |
|
|
91
|
+
| Numeric `tagPriority` | ✓ | ✓ | ✓ |
|
|
92
|
+
| `<script type="module" defer>` | ✓ | ✓ | ✓ |
|
|
93
|
+
| Preload missing `as` / font `crossorigin` | ✓ | ✓ | ✓ |
|
|
94
|
+
| Twitter handle missing `@` | ✓ | ✓ | ✓ |
|
|
95
|
+
| Robots `index/noindex` conflict | ✓ | ✓ | ✓ |
|
|
96
|
+
| Non-absolute canonical / OG URLs | ✓ | ✓ | ✓ |
|
|
97
|
+
| Empty meta content / HTML-in-title | ✓ | ✓ | ✓ |
|
|
98
|
+
| Viewport blocks user zoom | ✓ | ✓ | ✓ |
|
|
99
|
+
| Canonical vs `og:url` mismatch | — | ✓ | ✓ |
|
|
100
|
+
| `og:image` missing dimensions | — | ✓ | ✓ |
|
|
101
|
+
| Missing description / title | — | ✓ | ✓ |
|
|
102
|
+
| Missing OG title / description | — | ✓ | ✓ |
|
|
103
|
+
| Charset not within first N tags (SSR) | — | ✓ | ✓ |
|
|
104
|
+
| Meta tag past 1MB crawler limit | — | ✓ | ✓ |
|
|
105
|
+
| Too many preloads / preconnects / `fetchpriority="high"` | — | ✓ | ✓ |
|
|
106
|
+
| Duplicate / redundant resource hints | — | ✓ | ✓ |
|
|
107
|
+
| Preload + async/defer / preload + prefetch conflicts | — | ✓ | ✓ |
|
|
108
|
+
| Inline script / style size budget | — | ✓ | ✓ |
|
|
109
|
+
| Missing `TemplateParamsPlugin` / `AliasSortingPlugin` | — | — | ✓ (plugin presence is runtime-only) |
|
|
110
|
+
| `prefer-use-seo-meta` (meta-only `useHead` → `useSeoMeta`) | ✓ (autofix) | — | — |
|
|
111
|
+
| `page-missing-head` (no `useHead`/composable in `pages/**`) | ✓ (info, cross-file fixpoint) | — | — |
|
|
112
|
+
| Title separator / suffix consistency across pages | ✓ (cross-file) | — | — |
|
|
113
|
+
| `parse-error` (script block oxc couldn't parse) | ✓ | — | — |
|
|
114
|
+
|
|
115
|
+
Source-level lint is the cheapest feedback loop and runs in your editor. The HTML pass catches everything that depends on the resolved tag list. The runtime plugin gives you live warnings during dev. Use them together.
|
|
116
|
+
|
|
117
|
+
## Rule references
|
|
118
|
+
|
|
119
|
+
Rule IDs are shared across all three layers. See [`@unhead/eslint-plugin`'s rules table](../eslint-plugin/README.md#rules) for source-level rules; runtime-only rule IDs are documented in `unhead/plugins`'s `ValidatePlugin` JSDoc.
|
|
120
|
+
|
|
121
|
+
## Sharing logic with the editor
|
|
122
|
+
|
|
123
|
+
`audit` and `migrate` invoke the same predicate functions exported from `unhead/validate` that `@unhead/eslint-plugin` registers as ESLint rules. Source-level diagnostics are byte-for-byte identical between `unhead audit` (CLI) and `pnpm lint` (your editor + CI ESLint pipeline). Use the CLI for one-shot project-wide audits and CI; use the ESLint plugin for inline editor feedback.
|
package/bin/unhead.mjs
ADDED
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as citty from 'citty';
|
|
2
|
+
|
|
3
|
+
declare const audit: citty.CommandDef<{
|
|
4
|
+
readonly cwd: {
|
|
5
|
+
readonly type: "string";
|
|
6
|
+
readonly description: "Project root.";
|
|
7
|
+
readonly default: ".";
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
declare const migrate: citty.CommandDef<{
|
|
12
|
+
readonly cwd: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Project root.";
|
|
15
|
+
readonly default: ".";
|
|
16
|
+
};
|
|
17
|
+
readonly 'dry-run': {
|
|
18
|
+
readonly type: "boolean";
|
|
19
|
+
readonly description: "Report what would change without writing files.";
|
|
20
|
+
readonly default: false;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
declare const validateHtmlCommand: citty.CommandDef<{
|
|
25
|
+
readonly cwd: {
|
|
26
|
+
readonly type: "string";
|
|
27
|
+
readonly description: "Project root.";
|
|
28
|
+
readonly default: ".";
|
|
29
|
+
};
|
|
30
|
+
readonly json: {
|
|
31
|
+
readonly type: "boolean";
|
|
32
|
+
readonly description: "Emit JSON instead of human-readable output.";
|
|
33
|
+
readonly default: false;
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
declare const validateUrlCommand: citty.CommandDef<{
|
|
38
|
+
readonly url: {
|
|
39
|
+
readonly type: "positional";
|
|
40
|
+
readonly description: "URL to fetch and validate.";
|
|
41
|
+
readonly required: true;
|
|
42
|
+
};
|
|
43
|
+
readonly 'user-agent': {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
readonly description: "User-Agent header to send (default: facebookexternalhit so social-crawler-aware rules engage).";
|
|
46
|
+
readonly default: "facebookexternalhit/1.1 (+https://unhead.unjs.io)";
|
|
47
|
+
};
|
|
48
|
+
readonly timeout: {
|
|
49
|
+
readonly type: "string";
|
|
50
|
+
readonly description: "Fetch timeout in milliseconds.";
|
|
51
|
+
readonly default: "30000";
|
|
52
|
+
};
|
|
53
|
+
readonly json: {
|
|
54
|
+
readonly type: "boolean";
|
|
55
|
+
readonly description: "Emit JSON instead of human-readable output.";
|
|
56
|
+
readonly default: false;
|
|
57
|
+
};
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
declare function run(): Promise<void>;
|
|
61
|
+
|
|
62
|
+
export { audit, migrate, run, validateHtmlCommand, validateUrlCommand };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as citty from 'citty';
|
|
2
|
+
|
|
3
|
+
declare const audit: citty.CommandDef<{
|
|
4
|
+
readonly cwd: {
|
|
5
|
+
readonly type: "string";
|
|
6
|
+
readonly description: "Project root.";
|
|
7
|
+
readonly default: ".";
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
declare const migrate: citty.CommandDef<{
|
|
12
|
+
readonly cwd: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Project root.";
|
|
15
|
+
readonly default: ".";
|
|
16
|
+
};
|
|
17
|
+
readonly 'dry-run': {
|
|
18
|
+
readonly type: "boolean";
|
|
19
|
+
readonly description: "Report what would change without writing files.";
|
|
20
|
+
readonly default: false;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
declare const validateHtmlCommand: citty.CommandDef<{
|
|
25
|
+
readonly cwd: {
|
|
26
|
+
readonly type: "string";
|
|
27
|
+
readonly description: "Project root.";
|
|
28
|
+
readonly default: ".";
|
|
29
|
+
};
|
|
30
|
+
readonly json: {
|
|
31
|
+
readonly type: "boolean";
|
|
32
|
+
readonly description: "Emit JSON instead of human-readable output.";
|
|
33
|
+
readonly default: false;
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
declare const validateUrlCommand: citty.CommandDef<{
|
|
38
|
+
readonly url: {
|
|
39
|
+
readonly type: "positional";
|
|
40
|
+
readonly description: "URL to fetch and validate.";
|
|
41
|
+
readonly required: true;
|
|
42
|
+
};
|
|
43
|
+
readonly 'user-agent': {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
readonly description: "User-Agent header to send (default: facebookexternalhit so social-crawler-aware rules engage).";
|
|
46
|
+
readonly default: "facebookexternalhit/1.1 (+https://unhead.unjs.io)";
|
|
47
|
+
};
|
|
48
|
+
readonly timeout: {
|
|
49
|
+
readonly type: "string";
|
|
50
|
+
readonly description: "Fetch timeout in milliseconds.";
|
|
51
|
+
readonly default: "30000";
|
|
52
|
+
};
|
|
53
|
+
readonly json: {
|
|
54
|
+
readonly type: "boolean";
|
|
55
|
+
readonly description: "Emit JSON instead of human-readable output.";
|
|
56
|
+
readonly default: false;
|
|
57
|
+
};
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
declare function run(): Promise<void>;
|
|
61
|
+
|
|
62
|
+
export { audit, migrate, run, validateHtmlCommand, validateUrlCommand };
|