comark-ui 0.9.1 → 0.10.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.
Files changed (36) hide show
  1. package/README.md +9 -1
  2. package/dist/{client-kUEiRbtp.mjs → client-zWY8yhUx.mjs} +1 -14
  3. package/dist/client.mjs +2 -2
  4. package/dist/{editor-9IcfELjG.mjs → editor-CW05gHp7.mjs} +46 -6
  5. package/dist/{editor-yoVL7Ajq.d.mts → editor-Dk8TRweM.d.mts} +13 -2
  6. package/dist/editor-vue.d.mts +1 -1
  7. package/dist/editor-vue.mjs +1 -1
  8. package/dist/editor.d.mts +2 -2
  9. package/dist/editor.mjs +1 -1
  10. package/dist/html.d.mts +1 -1
  11. package/dist/html.mjs +6 -4
  12. package/dist/{index-Cq3zytGw.d.mts → index-7SBL13EQ.d.mts} +5 -1
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +2 -2
  15. package/dist/react.d.mts +1 -1
  16. package/dist/react.mjs +6 -3
  17. package/dist/skills/before-after-evidence/SKILL.md +1 -1
  18. package/dist/skills/comark-ui/SKILL.md +4 -1
  19. package/dist/skills/comark-ui/references/annotate.md +1 -1
  20. package/dist/skills/comark-ui/references/time.md +28 -0
  21. package/dist/skills/report/SKILL.md +81 -0
  22. package/dist/skills/report/agents/openai.yaml +3 -0
  23. package/dist/skills/report/references/blocks.md +199 -0
  24. package/dist/skills/report/references/example.md +305 -0
  25. package/dist/skills/report/scripts/render.mjs +74 -0
  26. package/dist/skills/review-document/SKILL.md +1 -1
  27. package/dist/skills.mjs +42 -7
  28. package/dist/{src-MvpWiv9v.mjs → src-C3nMECXA.mjs} +2 -0
  29. package/dist/styles-vZMqDkIY.mjs +15 -0
  30. package/dist/svelte/Mermaid.svelte +1 -1
  31. package/dist/svelte/index.js +2 -0
  32. package/dist/{typeset-css-BOtdXbMg.mjs → typeset-css-BqBoCTMG.mjs} +5 -16
  33. package/dist/typeset.css +1 -1
  34. package/dist/vue.d.mts +610 -1
  35. package/dist/vue.mjs +44 -4
  36. package/package.json +52 -51
@@ -0,0 +1,199 @@
1
+ # Blocks for a report
2
+
3
+ Each block below renders with the full `comark-ui` preset. The `comark-ui` skill documents every block in full; read its reference for a block before you change more than the values here.
4
+
5
+ ## The answer: callouts
6
+
7
+ ```md
8
+ > [!IMPORTANT]
9
+ > **Your calls:**
10
+ >
11
+ > 1. Hold `@nuxtjs/i18n` until #3564 has URL routing?
12
+ > 2. Ship the Env and Resend codemods before Auth's SSR gap closes?
13
+
14
+ > [!WARNING]
15
+ > Every row needs Nuxt 5, and the codemods don't upgrade Nuxt.
16
+ ```
17
+
18
+ Frontmatter `status`, `date`, and `tags` render as a properties box at the top with no extra markup.
19
+
20
+ ## Bubble chart: two scores and a size
21
+
22
+ ````md
23
+ ::chart{title="Shortlist: value and difficulty (1 to 5), bubble size = npm downloads a week"}
24
+ | module | difficulty | value | downloads |
25
+ | --- | --: | --: | --: |
26
+ | nuxt-auth-utils | 4 | 5 | 177926 |
27
+ | Resend | 2 | 4 | 1251 |
28
+ | t3-env | 2 | 3 | 6072 |
29
+ | nuxt-cron | 2 | 3 | 3123 |
30
+
31
+ ```vega-lite
32
+ {
33
+ "width": 620,
34
+ "height": 320,
35
+ "encoding": {
36
+ "x": { "field": "difficulty", "type": "quantitative", "scale": { "domain": [1.5, 5] }, "title": "Difficulty (5 = hardest)" },
37
+ "y": { "field": "value", "type": "quantitative", "scale": { "domain": [2.5, 5.5] }, "title": "Value (5 = most)" }
38
+ },
39
+ "layer": [
40
+ {
41
+ "mark": { "type": "circle", "opacity": 0.55 },
42
+ "encoding": { "size": { "field": "downloads", "type": "quantitative", "scale": { "range": [80, 3000] }, "legend": null } }
43
+ },
44
+ {
45
+ "transform": [{ "filter": "indexof(['Resend', 't3-env', 'nuxt-cron'], datum.module) < 0" }],
46
+ "mark": { "type": "text", "dy": -26, "fontSize": 12 },
47
+ "encoding": { "text": { "field": "module" } }
48
+ },
49
+ {
50
+ "transform": [{ "filter": "indexof(['Resend', 't3-env'], datum.module) >= 0" }],
51
+ "mark": { "type": "text", "align": "right", "dx": -12, "fontSize": 12 },
52
+ "encoding": { "text": { "field": "module" } }
53
+ },
54
+ {
55
+ "transform": [{ "filter": "datum.module === 'nuxt-cron'" }],
56
+ "mark": { "type": "text", "dy": 16, "fontSize": 12 },
57
+ "encoding": { "text": { "field": "module" } }
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+ ::
63
+ ````
64
+
65
+ - With a `vega-lite` spec, `::chart` takes only `title`; `type`, `x`, and `y` are lint errors. Name the columns in `encoding`.
66
+ - Set `width` in pixels. `"container"` renders about 230px wide.
67
+ - Labels overlap when two points share a spot or sit close. Give each label layer a `filter` and move those points' labels left (`align: right`, `dx: -12`) or below (`dy: 16`). Look at the render; the lint can't see overlap.
68
+ - Write a range such as 4–5 as its middle, 4.5, and say so in the item's detail.
69
+
70
+ ## Bar chart: sizes, with one mark
71
+
72
+ ```md
73
+ ::chart{type="bar" horizontal labels title="Left out: npm downloads a week (10k and up)"}
74
+ | module | downloads |
75
+ | --- | --: |
76
+ | @nuxtjs/i18n | 977490 |
77
+ | nuxt-csurf | 223523 |
78
+ :::annotation{at="@nuxtjs/i18n" mark="box" color="amber" side="right"}
79
+ Your call: hold until #3564 has URL routing.
80
+ :::
81
+ ::
82
+ ```
83
+
84
+ Rows draw in table order, so sort them largest first. Mark the bar a decision is about, at most one; a chart no decision points at gets no mark.
85
+
86
+ For sizes that split into parts, use `type="stacked"`, one column per part:
87
+
88
+ ```md
89
+ ::chart{type="stacked" horizontal title="Description characters per skill"}
90
+ | skill | sentence | trigger list |
91
+ | --- | --: | --: |
92
+ | emil-mobile-native | 760 | 551 |
93
+ | emil-prep-for-prod | 865 | 386 |
94
+ ::
95
+ ```
96
+
97
+ Each chart scales its own axis. When two charts compare the same unit, put the rows in one chart, or write both in `vega-lite` with the same `"scale": { "domain": [0, 1400] }` on the value axis, so equal values draw equal bars.
98
+
99
+ ## Order and blockers: a flowchart
100
+
101
+ ````md
102
+ ```mermaid
103
+ flowchart LR
104
+ shell["1 · Wizard shell + Env"]
105
+ shell --> resend["2 · Resend"]
106
+ shell --> auth["3 · Auth"]
107
+ ssr{{"SSR session + page middleware"}} --> auth
108
+ upstash{{"Upstash driver"}} -.-> rl["5 · Rate limit"]
109
+ ```
110
+ ````
111
+
112
+ Use `LR` for up to about five steps side by side and `TB` for a longer chain or long labels; the diagram scales down to fit the page width, so a wide one renders small. Boxes are steps, numbered in the order the reader acts. Hexagons are work that has to land first; a dashed arrow means it should land first but doesn't have to. Say both in the sentence above the diagram.
113
+
114
+ ## The decision table: past three decisions
115
+
116
+ ```md
117
+ | # | Decision | Opus | Sol | Recommend |
118
+ | --- | --- | --- | --- | --- |
119
+ | D1 | Import family for apps | `vite-hub/*` | `@vite-hub/*` | :badge[vite-hub/*]{color="success"} |
120
+ | D2 | Sandbox Task option key | `sandboxTask` | `sandbox.tasks` | :badge[sandboxTask]{color="success"} |
121
+ ```
122
+
123
+ Each cell is the position, not the argument. The arguments go in the decision's accordion item, labeled with its number.
124
+
125
+ ## The table: at most five columns
126
+
127
+ ```md
128
+ | Step | Module → primitive | npm/week | Fit |
129
+ | --- | --- | --: | --- |
130
+ | 1 | `@t3-oss/env-nuxt` → Env | 6,072 | :badge[High]{color="success"} |
131
+ | 3 | `nuxt-auth-utils` → Auth | 177,926 | :badge[Partial]{color="warning"} |
132
+ ```
133
+
134
+ Badge colors: `success` good, `warning` partial or at risk, `error` blocked, `neutral` unknown. Scores that are already in a chart stay out of the table. A cell holds a value, a badge, or a few words; a column of sentences turns the table into a wall of text, so move it into the accordion.
135
+
136
+ Write file names and domains in backticks, such as `AGENTS.md` and `devtools.fm`. Bare, they render as link chips with an icon.
137
+
138
+ ## Each item: an accordion
139
+
140
+ ````md
141
+ ::accordion
142
+ :::accordion-item{label="2 · Resend → Email"}
143
+ | Resend | ViteHub Email |
144
+ | --- | --- |
145
+ | `new Resend(key).emails.send(msg)` | `email.send(msg)` |
146
+
147
+ - **Manual:** batch, contacts, audiences, broadcasts.
148
+ - **First:** nothing.
149
+ - **Gain:** a dev outbox and Console preview.
150
+ :::
151
+ ::
152
+ ````
153
+
154
+ Every item has the same parts in the same order, with bold labels, a line or two each. A part that needs more becomes a table inside the item. The label starts with the item's step number from the table. Icons come from `lucide:`; pick one per kind of item.
155
+
156
+ ## Before and after: stats and delta
157
+
158
+ ```md
159
+ ::stats{better="lower"}
160
+ | Measure | Before | After |
161
+ | --- | --: | --: |
162
+ | Description characters loaded | 23,515 | 10,981 |
163
+ | Model-invoked skills | 49 | 41 |
164
+ ::
165
+
166
+ Trimming the lists saves :delta{from=23515 to=10981 unit="chars" better="lower"} every session.
167
+ ```
168
+
169
+ `::stats` holds one table of exactly three columns: a name, before, after. Each cell is a number with an optional unit, the same unit in both cells, and the block adds the percent itself. A value with a qualifier, such as "about 65" or "6 of 11", belongs in a plain table, or keeps its number in the cell and moves the qualifier into the row's name. `better` applies to every row, so rows that are better higher go in a second block.
170
+
171
+ ## A share: progress
172
+
173
+ ```md
174
+ :progress{value=293 max=785 label="nuxt-security configs that mention rateLimiter"}
175
+ ```
176
+
177
+ ## How I checked
178
+
179
+ ````md
180
+ > [!CAUTION]
181
+ > **Not verified:** any source module on Nuxt 5; the Docus count (one npm call).
182
+
183
+ ::details{summary="Sources and method"}
184
+ - **Downloads:** `api.npmjs.org/downloads/point/last-week`, 2026-10-30 to 2026-11-05.
185
+ ::
186
+ ````
187
+
188
+ ## Render it locally
189
+
190
+ A host renders the report only after you publish it, and a private page may need the user's sign-in, so check the render before you hand it over. Copy this skill's [scripts/render.mjs](../scripts/render.mjs) next to the report, then:
191
+
192
+ ```sh
193
+ mkdir -p tmp/report-render && cd tmp/report-render
194
+ echo '{ "private": true, "type": "module" }' > package.json
195
+ pnpm add --ignore-workspace comark @comark/html comark-ui playwright
196
+ node ../../render.mjs ../../report.md out
197
+ ```
198
+
199
+ Without its own `package.json`, pnpm walks up to the repository's and can fail on its catalog. The script writes `out/report.html` and screenshots at 900px wide, 1600px tall each: `report-closed-*.png` as the reader first sees it, and `report-open-*.png` with every fold open. When Playwright has no browser downloaded, it uses the newest Chromium in `~/.cache/ms-playwright`.
@@ -0,0 +1,305 @@
1
+ ---
2
+ title: "Migrate wizard: start with Env and Resend, then Auth and Content"
3
+ status: Proposal
4
+ date: 2026-11-07
5
+ tags: [migrate wizard, codemods]
6
+ ---
7
+
8
+ ## The answer
9
+
10
+ Build the wizard shell with the Env codemod, then add Resend, Auth and Content. Auth and Content hold most of the users, but each needs ViteHub work first. Env and Resend are cheap and prove the wizard end to end.
11
+
12
+ > [!IMPORTANT]
13
+ > **Your call:** hold `@nuxtjs/i18n`, the largest audience at 977k a week, until #3564 merges and i18n has URL routing?
14
+
15
+ > [!WARNING]
16
+ > Every row needs Nuxt 5. Most source apps run Nuxt 4 and the codemods don't upgrade Nuxt, so the wizard checks the version first.
17
+
18
+ ## Value against cost
19
+
20
+ Up and to the right is worth more and costs more. Bubble size is weekly npm downloads. Auth and Content are the two big bubbles. The cheap wins sit at difficulty 2.
21
+
22
+ ::chart{title="Shortlist: value and difficulty (1 to 5), bubble size = npm downloads a week"}
23
+ | module | difficulty | value | downloads |
24
+ | --- | --: | --: | --: |
25
+ | nuxt-auth-utils | 4 | 5 | 177926 |
26
+ | @nuxt/content | 4 | 4 | 240394 |
27
+ | Docus | 3 | 4 | 15800 |
28
+ | Resend | 2 | 4 | 1251 |
29
+ | nuxt-security limiter | 3 | 3 | 220740 |
30
+ | t3-env | 2 | 3 | 6072 |
31
+ | sidebase | 4.5 | 3 | 46448 |
32
+ | nuxt-cron | 2 | 3 | 3123 |
33
+
34
+ ```vega-lite
35
+ {
36
+ "width": 620,
37
+ "height": 320,
38
+ "encoding": {
39
+ "x": { "field": "difficulty", "type": "quantitative", "scale": { "domain": [1.5, 5] }, "title": "Difficulty (5 = hardest)" },
40
+ "y": { "field": "value", "type": "quantitative", "scale": { "domain": [2.5, 5.5] }, "title": "Value (5 = most)" }
41
+ },
42
+ "layer": [
43
+ {
44
+ "mark": { "type": "circle", "opacity": 0.55 },
45
+ "encoding": { "size": { "field": "downloads", "type": "quantitative", "scale": { "range": [80, 3000] }, "legend": null } }
46
+ },
47
+ {
48
+ "transform": [{ "filter": "indexof(['t3-env', 'Docus', 'Resend'], datum.module) < 0 && datum.module !== 'nuxt-cron'" }],
49
+ "mark": { "type": "text", "dy": -26, "fontSize": 12 },
50
+ "encoding": { "text": { "field": "module" } }
51
+ },
52
+ {
53
+ "transform": [{ "filter": "indexof(['t3-env', 'Docus', 'Resend'], datum.module) >= 0" }],
54
+ "mark": { "type": "text", "align": "right", "dx": -12, "fontSize": 12 },
55
+ "encoding": { "text": { "field": "module" } }
56
+ },
57
+ {
58
+ "transform": [{ "filter": "datum.module === 'nuxt-cron'" }],
59
+ "mark": { "type": "text", "dy": 16, "fontSize": 12 },
60
+ "encoding": { "text": { "field": "module" } }
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+ ::
66
+
67
+ ## The order
68
+
69
+ Boxes are codemods, numbered in build order. Hexagons are ViteHub work that has to land first; a dashed arrow means it should land first but doesn't have to.
70
+
71
+ ```mermaid
72
+ flowchart LR
73
+ shell["1 · Wizard shell + Env"]
74
+ shell --> resend["2 · Resend"]
75
+ shell --> auth["3 · Auth"]
76
+ shell --> content["4 · Content"] --> docus["Docus, then nuxt-studio"]
77
+ shell --> rl["5 · Rate limit"]
78
+ shell --> cron["6 · nuxt-cron"]
79
+ shell --> i18n["7 · i18n"]
80
+ ssr{{"SSR session + page middleware"}} --> auth
81
+ upstash{{"Upstash driver"}} -.-> rl
82
+ pr{{"#3564 + URL routing"}} --> i18n
83
+ ```
84
+
85
+ | Step | Module → primitive | npm/week | Fit |
86
+ | --- | --- | --: | --- |
87
+ | 1 | `@t3-oss/env-nuxt` → Env | 6,072 | :badge[High]{color="success"} |
88
+ | 2 | `resend`, `nuxt-resend` → Email | 1,251 | :badge[High]{color="success"} |
89
+ | 3 | `nuxt-auth-utils` → Auth | 177,926 | :badge[Partial]{color="warning"} |
90
+ | 3 | `@sidebase/nuxt-auth` → Auth | 46,448 | :badge[Partial]{color="warning"} |
91
+ | 4 | `@nuxt/content` v3 → Content | 240,394 | :badge[Partial]{color="warning"} |
92
+ | 4 | Docus → `@vite-hub/docs` | ~15,800 | :badge[High]{color="success"} |
93
+ | 5 | `nuxt-security` limiter → Rate Limit | 220,740 | :badge[Partial]{color="warning"} |
94
+ | 6 | `nuxt-cron` → Schedule | 3,123 | :badge[High]{color="success"} |
95
+
96
+ Resend's count is `nuxt-resend` alone; 274 more apps call `resend` directly from server routes. Inside step 3, `@nuxtjs/better-auth` (2,101 a week) goes first: it runs Better Auth on both sides, so users, hashes and sessions keep their format.
97
+
98
+ ## What ViteHub has to add
99
+
100
+ Each gap below blocks a codemod or a group of users. i18n routing unblocks the most by far.
101
+
102
+ ::chart{type="bar" horizontal labels title="npm downloads a week each missing feature would unblock"}
103
+ | Missing in ViteHub | downloads |
104
+ | --- | --: |
105
+ | i18n: URL routing | 985785 |
106
+ | Auth: SSR session + page middleware | 226475 |
107
+ | MCP: app-level server | 42096 |
108
+ | Email: SMTP driver | 4620 |
109
+ | Rate Limit: Upstash driver | 2044 |
110
+ ::
111
+
112
+ ::details{summary="Which modules each gap unblocks"}
113
+ | Gap | Unblocks |
114
+ | --- | --- |
115
+ | i18n: URL routing | `@nuxtjs/i18n` 977,490, `nuxt-i18n-micro` 8,295 |
116
+ | Auth: SSR session + page middleware | `nuxt-auth-utils` 177,926, sidebase 46,448, `@nuxtjs/better-auth` 2,101 |
117
+ | MCP: app-level server | `@nuxtjs/mcp-toolkit` 42,096 |
118
+ | Email: SMTP driver | `nuxt-nodemailer` 3,376, `nuxt-mail` 1,244, plus 609 apps on raw `nodemailer` |
119
+ | Rate Limit: Upstash driver, windows past 10s and 1m on Cloudflare | `nuxt-api-shield` 2,044, `@upstash/ratelimit`, and the nuxt-security default of 5 minutes on Cloudflare |
120
+ ::
121
+
122
+ ## Each codemod
123
+
124
+ Every item has the same three parts: what the codemod rewrites, what stays manual, and what has to land first.
125
+
126
+ ::accordion
127
+ :::accordion-item{label="1 · @t3-oss/env-nuxt → Env"}
128
+ | t3-env | ViteHub Env |
129
+ | --- | --- |
130
+ | `createEnv({ server, client })` | the existing converter in `nuxt-safe-runtime-config/schema.ts`, as it is |
131
+
132
+ - **Manual:** nothing found.
133
+ - **Bonus:** `@arkenv/nuxt` (202 a week) comes almost free, since the converter already reads ArkType.
134
+ :::
135
+
136
+ :::accordion-item{label="2 · Resend → Email"}
137
+ | Resend | ViteHub Email |
138
+ | --- | --- |
139
+ | `new Resend(key).emails.send(msg)`, `useResend()` | `email.send(msg)`, same message fields |
140
+ | `const { data, error }` | `const [error, data]` |
141
+ | (nothing) | `vitehub.email = { driver: 'resend', options: { apiKey: env(...) } }` |
142
+
143
+ - **Manual:** batch, contacts, audiences, broadcasts, `react` payloads, Resend templates.
144
+ - **Gain:** a dev outbox, Console preview, a path to Cloudflare Email.
145
+ - **The wizard's report must say:** in dev, sends go to the outbox unless `remote: true`.
146
+ :::
147
+
148
+ :::accordion-item{label="3 · nuxt-auth-utils → Auth"}
149
+ | nuxt-auth-utils | ViteHub Auth |
150
+ | --- | --- |
151
+ | `'nuxt-auth-utils'` in `modules` | `'vite-hub/nuxt'` |
152
+ | `runtimeConfig.oauth.<p>`, `NUXT_OAUTH_<P>_*` | `socialProviders.<p>` in `server/auth.ts`, with Env |
153
+ | `useUserSession()` `fetch`, `clear` | `refresh`, `signOut` (`loggedIn`, `user`, `ready` match) |
154
+ | `/auth/github` links | `signIn.social({ provider: 'github' })` |
155
+ | `getUserSession`, `requireUserSession` | `auth.api.getSession`, `withAuth` |
156
+
157
+ - **Providers:** about 25 of 52 are in Better Auth core; auth0, keycloak, okta and others go through `genericOAuth`; six, such as steam, mastodon and atproto, have no equivalent.
158
+ - **Manual:** `onSuccess` upserts move to `databaseHooks`; custom `setUserSession` data; user shape and IDs change (`user.login` breaks); everyone signs in again and each provider needs the `/api/auth/callback/<p>` URL; Adonis scrypt hashes need a custom `password.verify`; WebAuthn moves to the passkey table.
159
+ - **First:** a server-rendered session (`useUserSession()` stays pending during SSR, `packages/auth/README.md:317`) and page middleware. Without them the migrated app flashes signed-out on every load.
160
+ - **Gain:** a real users store, the Console, Auth events, admin, 2FA and organizations.
161
+ :::
162
+
163
+ :::accordion-item{label="3 · @sidebase/nuxt-auth → Auth"}
164
+ | sidebase (authjs) | ViteHub Auth |
165
+ | --- | --- |
166
+ | `NuxtAuthHandler({ providers })` | `socialProviders` |
167
+ | `useAuth()`, `status === 'authenticated'` | `useUserSession()`, `loggedIn` |
168
+ | `getServerSession` | `auth.api.getSession` |
169
+
170
+ - **Refuse:** the `local` provider, which sends bearer tokens to an external backend.
171
+ - **Manual:** the `jwt` and `session` callbacks.
172
+ :::
173
+
174
+ :::accordion-item{label="4 · @nuxt/content v3 → Content"}
175
+ | @nuxt/content | ViteHub Content |
176
+ | --- | --- |
177
+ | `content.config.ts` `defineCollection({ source })` | `server/content.ts` `defineContent({ sources })` with `glob()` |
178
+ | `queryCollection(c).path(p).first()` | `content.get(p)` |
179
+ | `queryCollectionNavigation` | `content.navigation` |
180
+ | `<ContentRenderer>`, `<MDC>` | `<Markdown>` |
181
+
182
+ - **Manual, reported with `file:line`:** every `.where/.order/.limit/.select/.count` chain (no query builder), the `database` adapters, remote `repository` sources, `queryCollectionItemSurroundings`.
183
+ - **Gain:** drafts and publishing, Console editing, Agent access, no SQL dump at build.
184
+ - **Won't move:** apps that query content heavily.
185
+ :::
186
+
187
+ :::accordion-item{label="4 · Docus → @vite-hub/docs"}
188
+ | Docus | @vite-hub/docs |
189
+ | --- | --- |
190
+ | `extends: ['docus']` + `app.config.ts` `seo`, `header`, `github`, `socials` | `modules: ['@vite-hub/docs/nuxt']` with `docs: { site, github, socials }` |
191
+ | `content/**` | `content/docs/<section>/` |
192
+ | `AppHeader`, `DocsAsideLeftTop` overrides | same names |
193
+
194
+ - **Manual:** Docus i18n, which has no target.
195
+ - **Not verified:** a Comark UI equivalent for every Nuxt UI prose component.
196
+ - **Why it ranks high:** the whole site moves, vitehub.dev is the oracle, and it reuses the Content codemod.
197
+ :::
198
+
199
+ :::accordion-item{label="5 · nuxt-security limiter → Rate Limit"}
200
+ :progress{value=293 max=785 label="nuxt-security configs that mention rateLimiter"}
201
+
202
+ The limiter is on by default: 150 requests per 300,000 ms per IP and route.
203
+
204
+ | nuxt-security | ViteHub Rate Limit |
205
+ | --- | --- |
206
+ | `security.rateLimiter` | `false`; the module stays for headers and CSP |
207
+ | (nothing) | `server/middleware/rate-limit.ts` with `requireRateLimit()`, `vitehub.rateLimit: true` |
208
+
209
+ - **Manual:** per-route `routeRules` overrides, `whiteList`, `ipHeader`, Redis storage.
210
+ - **Wall:** only memory and Cloudflare drivers, and Cloudflare allows only 10s or 1m windows, so the module's 5m default fails the build there.
211
+ :::
212
+
213
+ :::accordion-item{label="6 · nuxt-cron → Schedule"}
214
+ | nuxt-cron | ViteHub Schedule |
215
+ | --- | --- |
216
+ | `server/cron/<n>.ts` `defineCronHandler(time, cb)` | `server/schedules/<n>.ts` `defineSchedule({ cron, handler })` |
217
+ | presets | 5-field cron |
218
+
219
+ - **Manual:** `everySecond`, `timeZone`.
220
+ - **Gain:** a node-only cron runs on Cloudflare, Vercel and Netlify, with run history.
221
+ :::
222
+ ::
223
+
224
+ ## Left out
225
+
226
+ The chart shows what we leave on the table, 10k a week and up. i18n is the one big loss. The rest have no ViteHub target or wait on a codemod above.
227
+
228
+ ::chart{type="bar" horizontal labels title="Left out: npm downloads a week (10k and up)"}
229
+ | module | downloads |
230
+ | --- | --: |
231
+ | @nuxtjs/i18n | 977490 |
232
+ | nuxt-csurf | 223523 |
233
+ | @nuxtjs/supabase | 94526 |
234
+ | @nuxtjs/turnstile | 46735 |
235
+ | @nuxtjs/mcp-toolkit | 42096 |
236
+ | @clerk/nuxt | 40184 |
237
+ | nitro-cloudflare-dev | 29560 |
238
+ | nuxt-vuefire | 15428 |
239
+ | nuxt-studio | 14956 |
240
+ | nuxt-multi-cache | 14125 |
241
+ | nuxt-oidc-auth | 10859 |
242
+ :::annotation{at="@nuxtjs/i18n" mark="box" color="amber" side="right"}
243
+ Your call: hold until #3564 has URL routing.
244
+ :::
245
+ ::
246
+
247
+ ::accordion
248
+ :::accordion-item{label="Waits on ViteHub work (8)"}
249
+ | Module | npm/week | Waits on |
250
+ | --- | --: | --- |
251
+ | `@nuxtjs/i18n` | 977,490 | #3564 and URL routing (`useLocalePath`, `NuxtLinkLocale`, hreflang) |
252
+ | `nuxt-i18n-micro` | 8,295 | the same |
253
+ | `nuxt-studio` | 14,956 | the Content codemod |
254
+ | `nuxt-oidc-auth` | 10,859 | the Auth codemod; small audience, `h3` 1 |
255
+ | `nuxt-nodemailer`, `nuxt-mail` | 3,376 + 1,244 | an SMTP driver; the largest email segment |
256
+ | `nuxt-api-shield`, `@upstash/ratelimit` | 2,044 | an Upstash or Redis driver |
257
+ | `nitro-cloudflare-dev` | 29,560 | proof that ViteHub provides every binding in dev (only KV checked, #3480) |
258
+ | `nuxt-file-storage` | 845 | nothing; a cheap shim over `blob` after step 6 |
259
+ :::
260
+
261
+ :::accordion-item{label="No ViteHub primitive (5)"}
262
+ | Module | npm/week | Missing |
263
+ | --- | --: | --- |
264
+ | `nuxt-csurf` | 223,523 | CSRF |
265
+ | `@nuxtjs/turnstile` | 46,735 | captcha |
266
+ | `@nuxtjs/mcp-toolkit` | 42,096 | an app-level MCP server |
267
+ | `nuxt-multi-cache` | 14,125 | component and route caching, CDN tags |
268
+ | `nuxt-processor` (BullMQ) | 1,183 | priorities, repeats and flows in Queue |
269
+ :::
270
+
271
+ :::accordion-item{label="Data lives in a hosted service (4)"}
272
+ | Module | npm/week |
273
+ | --- | --: |
274
+ | `@nuxtjs/supabase` | 94,526 |
275
+ | `@clerk/nuxt` | 40,184 |
276
+ | `nuxt-vuefire` | 15,428 |
277
+ | Strapi, Sanity, Storyblok, Directus, Prismic | various |
278
+
279
+ Users, hashes and content stay in the service, so a codemod has nothing to move. At most a skeleton and a link to Better Auth's guide.
280
+ :::
281
+
282
+ :::accordion-item{label="Nothing worth a codemod (6)"}
283
+ | Module | Why |
284
+ | --- | --- |
285
+ | `nuxt-authorization` (7,188) | keep it; rewrite its two resolvers inside the Auth codemod |
286
+ | `nuxt-email-renderer` (5,291) | templates keep working through `email.send` |
287
+ | `@workflow/nuxt` (2.8M, transitive) | Workflow DevKit is already portable |
288
+ | `@prisma/nuxt` (896) | deprecated, and no Prisma support |
289
+ | Nitro `scheduledTasks` (294 configs) | Nitro 3 already runs them; a Vite Doctor hint fits better |
290
+ | Nitro `useStorage` with Redis (270 configs) | every call site changes and there's no ioredis driver; only `nitro.storage.cache` → `vitehub.cache` is cheap |
291
+ :::
292
+ ::
293
+
294
+ ## How I checked
295
+
296
+ > [!CAUTION]
297
+ > **Not verified:** any source module on Nuxt 5; sessions surviving the `@nuxtjs/better-auth` move; how many nuxt-security users want only the limiter; the Docus count (one npm call); Comark covering every MDC feature, such as named slots and `{{ $doc }}`.
298
+
299
+ ::details{summary="Sources and method"}
300
+ - **The field:** all 334 modules in the `api.nuxt.com/modules` registry (2026-11-07), 375 npm search hits across 45 primitive terms, and about 120 named packages the registry misses, such as nuxt-auth-utils.
301
+ - **Downloads:** `api.npmjs.org/downloads/point/last-week`, 2026-10-30 to 2026-11-05.
302
+ - **Stars and last push:** the GitHub API.
303
+ - **Module APIs:** the npm tarballs. **ViteHub APIs:** `docs/content/docs/*` and the package READMEs on main (`c845af3ae`).
304
+ - **Code counts:** GitHub's legacy code search; rough and not limited to Nuxt.
305
+ ::
@@ -0,0 +1,74 @@
1
+ // Renders a comark Markdown report to HTML and screenshots it at reading width.
2
+ // Usage: node render.mjs report.md [outDir]
3
+ // Run it from a folder where comark, @comark/html, comark-ui and playwright are installed.
4
+ import { createRequire } from "node:module";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { basename, join, resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ const [input, outDir = "render"] = process.argv.slice(2);
11
+ if (!input) throw new Error("Usage: node render.mjs report.md [outDir]");
12
+ const require = createRequire(join(process.cwd(), "noop.js"));
13
+ const load = (name) => import(pathToFileURL(require.resolve(name)).href);
14
+
15
+ const { createHtmlRenderer } = await load("@comark/html");
16
+ const { default: comarkUi } = await load("comark-ui/html");
17
+ const playwright = await load("playwright");
18
+ const { chromium } = playwright.chromium ? playwright : playwright.default;
19
+
20
+ mkdirSync(outDir, { recursive: true });
21
+ const name = basename(input).replace(/\.md$/, "");
22
+ const source = readFileSync(input, "utf8");
23
+ const rendered = await createHtmlRenderer(comarkUi())(source);
24
+ // A host such as Drop shows the frontmatter title above the page; the renderer doesn't.
25
+ const title = source.match(/^---\n[\s\S]*?^title:\s*"?(.*?)"?\s*$/m)?.[1] ?? "";
26
+ const escape = (text) =>
27
+ text.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
28
+ const htmlPath = resolve(outDir, `${name}.html`);
29
+ writeFileSync(
30
+ htmlPath,
31
+ `<!doctype html><meta charset="utf-8"><style>body{max-width:760px;margin:2rem auto;padding:0 1rem;font-family:system-ui}</style>${title ? `<h1>${escape(title)}</h1>` : ""}${typeof rendered === "string" ? rendered : rendered.html}`,
32
+ );
33
+
34
+ // Playwright's own browser may be missing; fall back to the newest one in its cache.
35
+ const cache = join(homedir(), ".cache/ms-playwright");
36
+ const fallback = existsSync(cache)
37
+ ? readdirSync(cache)
38
+ .filter((d) => /^chromium-\d+$/.test(d))
39
+ .sort((a, b) => Number(b.slice(9)) - Number(a.slice(9)))
40
+ .map((d) => join(cache, d, "chrome-linux64/chrome"))
41
+ .find((path) => existsSync(path))
42
+ : undefined;
43
+ let browser;
44
+ try {
45
+ browser = await chromium.launch();
46
+ } catch {
47
+ browser = await chromium.launch({ executablePath: fallback });
48
+ }
49
+
50
+ const page = await browser.newPage({ viewport: { width: 900, height: 1600 } });
51
+ await page.goto(pathToFileURL(htmlPath).href);
52
+ await page.waitForTimeout(1000);
53
+ const shots = [];
54
+ for (const state of ["closed", "open"]) {
55
+ if (state === "open")
56
+ await page.evaluate(() => document.querySelectorAll("details").forEach((d) => (d.open = true)));
57
+ // Folds animate open, so wait until the page stops growing before measuring it.
58
+ let height = 0;
59
+ for (let last = -1; height !== last; await page.waitForTimeout(400)) {
60
+ last = height;
61
+ height = await page.evaluate(() => document.documentElement.scrollHeight);
62
+ }
63
+ for (let y = 0, i = 0; y < height; y += 1600, i++) {
64
+ const path = resolve(outDir, `${name}-${state}-${i}.png`);
65
+ await page.screenshot({
66
+ path,
67
+ fullPage: true,
68
+ clip: { x: 0, y, width: 900, height: Math.min(1600, height - y) },
69
+ });
70
+ shots.push(path);
71
+ }
72
+ }
73
+ await browser.close();
74
+ console.log([htmlPath, ...shots].join("\n"));
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review-document
3
- description: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.
3
+ description: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, or an audit, where the reader must decide what to do about what you found.
4
4
  license: MIT
5
5
  ---
6
6