@hover-dev/cli 0.3.0 → 0.3.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.
@@ -15,7 +15,7 @@
15
15
  * `configCandidates` is the list of filenames the mutator will look for,
16
16
  * in priority order. The first one that exists in cwd wins.
17
17
  */
18
- export type FrameworkId = 'astro' | 'nuxt' | 'webpack' | 'vite';
18
+ export type FrameworkId = 'astro' | 'nuxt' | 'next' | 'webpack' | 'vite';
19
19
  export interface Framework {
20
20
  /** Short id used as the --<id> CLI flag and the `Detected: <id>` output. */
21
21
  id: FrameworkId;
@@ -1 +1 @@
1
- {"version":3,"file":"frameworks.d.ts","sourceRoot":"","sources":["../src/frameworks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,EAAE,EAAE,WAAW,CAAC;IAChB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB;yDACqD;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,EAiCjC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAExE"}
1
+ {"version":3,"file":"frameworks.d.ts","sourceRoot":"","sources":["../src/frameworks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,EAAE,EAAE,WAAW,CAAC;IAChB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB;yDACqD;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,EA4CjC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAExE"}
@@ -23,14 +23,25 @@ export const FRAMEWORKS = [
23
23
  detectDeps: ['nuxt'],
24
24
  configCandidates: ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'],
25
25
  },
26
+ {
27
+ id: 'next',
28
+ label: 'Next.js',
29
+ hoverPackage: '@hover-dev/next',
30
+ // Must check before `webpack` — Next 16+ defaults to Turbopack, and a
31
+ // Next project's `next` dep should land on `@hover-dev/next`, not the
32
+ // webpack plugin (which only covers `next dev --webpack`).
33
+ detectDeps: ['next'],
34
+ configCandidates: ['next.config.ts', 'next.config.mjs', 'next.config.js'],
35
+ },
26
36
  {
27
37
  id: 'webpack',
28
38
  label: 'Webpack',
29
39
  hoverPackage: 'webpack-plugin-hover',
30
- // `webpack-cli` is the user-facing wrapper; `next` ships its own webpack
31
- // (and our plugin works under `next dev --webpack`). Pure `webpack` as a
40
+ // `webpack-cli` is the user-facing wrapper for vanilla webpack-dev-server,
41
+ // Rspack / Rsbuild, CRA, Vue CLI. We no longer detect on `next` here
42
+ // Next projects route to `@hover-dev/next` above. Pure `webpack` as a
32
43
  // transitive dep is too noisy to detect on.
33
- detectDeps: ['webpack-cli', 'next'],
44
+ detectDeps: ['webpack-cli'],
34
45
  configCandidates: ['webpack.config.js', 'webpack.config.mjs', 'webpack.config.ts'],
35
46
  },
36
47
  {
package/dist/index.js CHANGED
@@ -50,13 +50,14 @@ Usage:
50
50
  npx @hover-dev/cli add --vite ${dim('# force a specific bundler')}
51
51
  npx @hover-dev/cli add --astro
52
52
  npx @hover-dev/cli add --nuxt
53
+ npx @hover-dev/cli add --next
53
54
  npx @hover-dev/cli add --webpack
54
55
  npx @hover-dev/cli add --dry-run ${dim('# show what would happen, change nothing')}
55
56
  npx @hover-dev/cli --help
56
57
  npx @hover-dev/cli --version
57
58
 
58
59
  What it does:
59
- 1. Detects your bundler (Vite / Astro / Nuxt / Webpack) from package.json.
60
+ 1. Detects your bundler (Vite / Astro / Nuxt / Next / Webpack) from package.json.
60
61
  2. Detects your package manager (pnpm / yarn / bun / npm) from your lockfile.
61
62
  3. Installs the matching Hover integration as a dev dependency.
62
63
  4. Adds the plugin/integration to your config file.
@@ -79,7 +80,7 @@ async function runAdd(args) {
79
80
  if (!framework) {
80
81
  err(`Couldn't detect a supported bundler in package.json.`);
81
82
  info(`Supported: ${FRAMEWORKS.map(f => f.id).join(', ')}.`);
82
- info(`Force one with --vite / --astro / --nuxt / --webpack.`);
83
+ info(`Force one with --vite / --astro / --nuxt / --next / --webpack.`);
83
84
  return 1;
84
85
  }
85
86
  if (args.framework) {
@@ -127,6 +128,27 @@ async function runAdd(args) {
127
128
  console.log(result.instructions);
128
129
  break;
129
130
  }
131
+ // Next.js needs one extra manual step the CLI cannot safely do: render
132
+ // `<HoverScript />` in `app/layout.tsx`. Modifying JSX in user code with
133
+ // ASTs invites whitespace drift and Server Component shape surprises;
134
+ // the instruction is short, so we print it and let the human paste it.
135
+ if (framework.id === 'next' && result.kind === 'ok' && !result.alreadyWired) {
136
+ info(`One last step — add ${cyan('<HoverScript />')} to your ${cyan('app/layout.tsx')}:`);
137
+ console.log(`
138
+ import { HoverScript } from '@hover-dev/next';
139
+
140
+ export default function RootLayout({ children }) {
141
+ return (
142
+ <html>
143
+ <body>
144
+ {children}
145
+ <HoverScript />
146
+ </body>
147
+ </html>
148
+ );
149
+ }
150
+ `);
151
+ }
130
152
  spark(`Done. Run your dev server and click the floating ✨.`);
131
153
  return 0;
132
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mutate.d.ts","sourceRoot":"","sources":["../src/mutate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B/F;AAoHD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,CAiC1D"}
1
+ {"version":3,"file":"mutate.d.ts","sourceRoot":"","sources":["../src/mutate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC/F;AAyOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,CAmD1D"}
package/dist/mutate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { builders, loadFile, writeFile } from 'magicast';
4
4
  /**
@@ -26,6 +26,8 @@ export async function mutateConfig(rootDir, framework) {
26
26
  return await mutateAstro(configPath);
27
27
  case 'nuxt':
28
28
  return await mutateNuxt(configPath);
29
+ case 'next':
30
+ return await mutateNext(configPath, rootDir);
29
31
  case 'webpack':
30
32
  return await mutateWebpack(configPath);
31
33
  }
@@ -86,6 +88,116 @@ async function mutateNuxt(configPath) {
86
88
  await writeFile(mod, configPath);
87
89
  return { kind: 'ok', configPath, alreadyWired: false };
88
90
  }
91
+ // ─── Next.js: withHover() wrap + instrumentation.ts merge ──────────────
92
+ /**
93
+ * Next is the only framework where wiring touches two files:
94
+ *
95
+ * 1. `next.config.{ts,mjs,js}` — wrap the user's exported config in
96
+ * `withHover(...)`. Idempotent: detect an existing import from
97
+ * `@hover-dev/next` and bail.
98
+ * 2. `instrumentation.ts` — Next's blessed hook for dev-only server-side
99
+ * init. We MUST NOT boot the Hover service in `next.config.ts` because
100
+ * that file is also loaded by `next build`, which would leak an orphan
101
+ * service into CI. The instrumentation hook only fires for
102
+ * `next dev` / `next start`.
103
+ *
104
+ * The user's `app/layout.tsx` still needs a `<HoverScript />` import after
105
+ * `{children}` — we can't safely AST-mutate JSX in user code (RSC,
106
+ * Server Component conventions, formatting), so the CLI prints a manual
107
+ * one-liner for that step instead of touching the file.
108
+ */
109
+ async function mutateNext(configPath, rootDir) {
110
+ const mod = await loadFile(configPath);
111
+ // Step 1: wrap next.config export in withHover(...) — idempotent.
112
+ let configAlreadyWired = false;
113
+ if (alreadyImported(mod, '@hover-dev/next')) {
114
+ configAlreadyWired = true;
115
+ }
116
+ else {
117
+ mod.imports.$add({ from: '@hover-dev/next', imported: 'withHover' });
118
+ // Wrap whatever the user has as `export default`. Works for plain object,
119
+ // for `defineConfig({...})` (no-op upstream — Next never had that
120
+ // helper), and for already-wrapped configs (which we skip via the
121
+ // `alreadyImported` check above).
122
+ const previous = mod.exports.default;
123
+ mod.exports.default = builders.functionCall('withHover', previous);
124
+ await writeFile(mod, configPath);
125
+ }
126
+ // Step 2: instrumentation.ts — create or merge.
127
+ const instrumentationPath = findOrPickInstrumentationPath(rootDir);
128
+ const instrumentationAlreadyWired = ensureInstrumentationRegistersHover(instrumentationPath);
129
+ const alreadyWired = configAlreadyWired && instrumentationAlreadyWired;
130
+ return { kind: 'ok', configPath, alreadyWired };
131
+ }
132
+ /**
133
+ * Locate the user's existing instrumentation file, or pick a default path
134
+ * to create one at. Next looks for `instrumentation.{ts,js}` at the project
135
+ * root or under `src/`. We prefer `src/` if it exists (consistent with the
136
+ * Next default scaffold), otherwise drop one at the project root.
137
+ */
138
+ function findOrPickInstrumentationPath(rootDir) {
139
+ const candidates = [
140
+ join(rootDir, 'instrumentation.ts'),
141
+ join(rootDir, 'instrumentation.js'),
142
+ join(rootDir, 'src', 'instrumentation.ts'),
143
+ join(rootDir, 'src', 'instrumentation.js'),
144
+ ];
145
+ for (const c of candidates) {
146
+ if (existsSync(c))
147
+ return c;
148
+ }
149
+ const useSrc = existsSync(join(rootDir, 'src'));
150
+ return useSrc ? join(rootDir, 'src', 'instrumentation.ts') : join(rootDir, 'instrumentation.ts');
151
+ }
152
+ /**
153
+ * Ensure the instrumentation file calls `register` from
154
+ * `@hover-dev/next/instrumentation`. Returns true if the file already
155
+ * had it wired (so the caller can report "already wired" honestly).
156
+ *
157
+ * We do this as a plain text edit (not magicast) because instrumentation
158
+ * files are usually tiny and the user might have written them in one of
159
+ * many idiomatic shapes (named function, arrow, async). String-level
160
+ * editing keeps formatting stable; magicast's stringifier would reformat.
161
+ */
162
+ function ensureInstrumentationRegistersHover(filePath) {
163
+ const HOVER_IMPORT = "import { register as registerHover } from '@hover-dev/next/instrumentation';";
164
+ const HOVER_CALL = 'await registerHover();';
165
+ if (!existsSync(filePath)) {
166
+ // Greenfield — write a full instrumentation file.
167
+ const fresh = [
168
+ HOVER_IMPORT,
169
+ '',
170
+ 'export async function register() {',
171
+ ` ${HOVER_CALL}`,
172
+ '}',
173
+ '',
174
+ ].join('\n');
175
+ writeFileSync(filePath, fresh, 'utf-8');
176
+ return false;
177
+ }
178
+ const existing = readFileSync(filePath, 'utf-8');
179
+ if (existing.includes('@hover-dev/next/instrumentation')) {
180
+ return true;
181
+ }
182
+ // The file exists but doesn't reference us. We want to (a) add our
183
+ // import at the top, (b) inject `await registerHover();` into the
184
+ // user's existing `register` function if we can find it, or
185
+ // (c) bail to a comment if we can't.
186
+ let next = `${HOVER_IMPORT}\n${existing}`;
187
+ const registerMatch = /(export\s+async\s+function\s+register\s*\([^)]*\)\s*\{)/.exec(next);
188
+ if (registerMatch) {
189
+ next = next.replace(registerMatch[0], `${registerMatch[0]}\n ${HOVER_CALL}`);
190
+ }
191
+ else {
192
+ // Couldn't find a function signature to splice into — append a new
193
+ // register export. If the user already has one in a non-standard
194
+ // shape Next will warn at startup, which is fine — better than us
195
+ // silently doing nothing.
196
+ next = `${next}\n\nexport async function register() {\n ${HOVER_CALL}\n}\n`;
197
+ }
198
+ writeFileSync(filePath, next, 'utf-8');
199
+ return false;
200
+ }
89
201
  // ─── Webpack: new HoverPlugin() in plugins array ────────────────────────
90
202
  async function mutateWebpack(configPath) {
91
203
  const mod = await loadFile(configPath);
@@ -172,6 +284,24 @@ export function manualInstructions(id) {
172
284
  ``,
173
285
  ` modules: ['@hover-dev/nuxt'],`,
174
286
  ].join('\n');
287
+ case 'next':
288
+ return [
289
+ `Three steps for Next.js:`,
290
+ ``,
291
+ `1. Wrap your next.config:`,
292
+ ` import { withHover } from '@hover-dev/next';`,
293
+ ` export default withHover({ /* your config */ });`,
294
+ ``,
295
+ `2. Create instrumentation.ts at your project root:`,
296
+ ` import { register as registerHover } from '@hover-dev/next/instrumentation';`,
297
+ ` export async function register() {`,
298
+ ` await registerHover();`,
299
+ ` }`,
300
+ ``,
301
+ `3. Render <HoverScript /> in your app/layout.tsx, after {children}:`,
302
+ ` import { HoverScript } from '@hover-dev/next';`,
303
+ ` // ... inside <body>: {children}<HoverScript />`,
304
+ ].join('\n');
175
305
  case 'webpack':
176
306
  return [
177
307
  `Add to your webpack config:`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for Hover. Detects your bundler, installs the right Hover integration package, and wires it into your config — one command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hyperyond",