@hover-dev/cli 0.3.0 → 0.3.2
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/dist/frameworks.d.ts +1 -1
- package/dist/frameworks.d.ts.map +1 -1
- package/dist/frameworks.js +19 -3
- package/dist/index.js +24 -2
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +175 -1
- package/package.json +1 -1
package/dist/frameworks.d.ts
CHANGED
|
@@ -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;
|
package/dist/frameworks.d.ts.map
CHANGED
|
@@ -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;
|
|
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,EAiDjC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAExE"}
|
package/dist/frameworks.js
CHANGED
|
@@ -23,14 +23,30 @@ 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
|
+
// `.mjs` and `.js` first because Next loads them via native `import()`
|
|
35
|
+
// and our `mutateNext` can magicast them in place. `.ts` is checked
|
|
36
|
+
// last and triggers a `manual` result that tells the user to rename to
|
|
37
|
+
// `.mjs` first — Next loads `.ts` configs through a CJS require step
|
|
38
|
+
// that can't resolve `@hover-dev/next`'s ESM-only `exports` map.
|
|
39
|
+
configCandidates: ['next.config.mjs', 'next.config.js', 'next.config.ts'],
|
|
40
|
+
},
|
|
26
41
|
{
|
|
27
42
|
id: 'webpack',
|
|
28
43
|
label: 'Webpack',
|
|
29
44
|
hoverPackage: 'webpack-plugin-hover',
|
|
30
|
-
// `webpack-cli` is the user-facing wrapper
|
|
31
|
-
//
|
|
45
|
+
// `webpack-cli` is the user-facing wrapper for vanilla webpack-dev-server,
|
|
46
|
+
// Rspack / Rsbuild, CRA, Vue CLI. We no longer detect on `next` here —
|
|
47
|
+
// Next projects route to `@hover-dev/next` above. Pure `webpack` as a
|
|
32
48
|
// transitive dep is too noisy to detect on.
|
|
33
|
-
detectDeps: ['webpack-cli'
|
|
49
|
+
detectDeps: ['webpack-cli'],
|
|
34
50
|
configCandidates: ['webpack.config.js', 'webpack.config.mjs', 'webpack.config.ts'],
|
|
35
51
|
},
|
|
36
52
|
{
|
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
|
}
|
package/dist/mutate.d.ts.map
CHANGED
|
@@ -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,
|
|
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;AAuRD;;;;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,160 @@ 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
|
+
// Next 16 loads `.ts` configs through a CJS `transpile-config` step that
|
|
111
|
+
// does `require()` on the compiled output. That path does NOT honour
|
|
112
|
+
// the `"import"` condition in ESM-only `exports`, so it cannot load
|
|
113
|
+
// `@hover-dev/next` (which is ESM). The user has to rename to `.mjs`
|
|
114
|
+
// (or `.js` with `"type": "module"` in package.json) — `.mjs` goes
|
|
115
|
+
// through Node's native `import()` which resolves ESM correctly.
|
|
116
|
+
// Spike + verification in CLAUDE.md "Edge runtime isolation" section.
|
|
117
|
+
if (configPath.endsWith('.ts')) {
|
|
118
|
+
return {
|
|
119
|
+
kind: 'manual',
|
|
120
|
+
reason: 'next.config.ts cannot load @hover-dev/next (Next loads .ts configs via CJS require, which does not honour ESM exports)',
|
|
121
|
+
instructions: tsConfigManualInstructions(configPath),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const mod = await loadFile(configPath);
|
|
125
|
+
// Step 1: wrap next.config export in withHover(...) — idempotent.
|
|
126
|
+
let configAlreadyWired = false;
|
|
127
|
+
if (alreadyImported(mod, '@hover-dev/next')) {
|
|
128
|
+
configAlreadyWired = true;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
mod.imports.$add({ from: '@hover-dev/next', imported: 'withHover' });
|
|
132
|
+
// Wrap whatever the user has as `export default`. Works for plain object,
|
|
133
|
+
// for `defineConfig({...})` (no-op upstream — Next never had that
|
|
134
|
+
// helper), and for already-wrapped configs (which we skip via the
|
|
135
|
+
// `alreadyImported` check above).
|
|
136
|
+
const previous = mod.exports.default;
|
|
137
|
+
mod.exports.default = builders.functionCall('withHover', previous);
|
|
138
|
+
await writeFile(mod, configPath);
|
|
139
|
+
}
|
|
140
|
+
// Step 2: instrumentation.ts — create or merge.
|
|
141
|
+
const instrumentationPath = findOrPickInstrumentationPath(rootDir);
|
|
142
|
+
const instrumentationAlreadyWired = ensureInstrumentationRegistersHover(instrumentationPath);
|
|
143
|
+
const alreadyWired = configAlreadyWired && instrumentationAlreadyWired;
|
|
144
|
+
return { kind: 'ok', configPath, alreadyWired };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Tailored manual-instructions message for the "you have a next.config.ts"
|
|
148
|
+
* case. Tells the user exactly what to rename and what to paste, with the
|
|
149
|
+
* same shape as if the CLI had mutated successfully — so a `mv && paste`
|
|
150
|
+
* leaves them in the same end state as the auto-magicast path.
|
|
151
|
+
*/
|
|
152
|
+
function tsConfigManualInstructions(configPath) {
|
|
153
|
+
const tsName = configPath.split('/').pop() ?? 'next.config.ts';
|
|
154
|
+
const mjsName = tsName.replace(/\.ts$/, '.mjs');
|
|
155
|
+
return [
|
|
156
|
+
`Next 16 can't load @hover-dev/next from a ${tsName}.`,
|
|
157
|
+
`(Next loads .ts configs via a CJS \`require()\` step that can't resolve`,
|
|
158
|
+
`the ESM-only \`exports\` map @hover-dev/next ships.) Rename to ${mjsName}`,
|
|
159
|
+
`and paste this:`,
|
|
160
|
+
``,
|
|
161
|
+
` // ${mjsName}`,
|
|
162
|
+
` import { withHover } from '@hover-dev/next';`,
|
|
163
|
+
``,
|
|
164
|
+
` /** @type {import('next').NextConfig} */`,
|
|
165
|
+
` const nextConfig = {`,
|
|
166
|
+
` // your existing config`,
|
|
167
|
+
` };`,
|
|
168
|
+
``,
|
|
169
|
+
` export default withHover(nextConfig);`,
|
|
170
|
+
``,
|
|
171
|
+
`Then continue with steps 2 and 3 below.`,
|
|
172
|
+
``,
|
|
173
|
+
manualInstructions('next'),
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Locate the user's existing instrumentation file, or pick a default path
|
|
178
|
+
* to create one at. Next looks for `instrumentation.{ts,js}` at the project
|
|
179
|
+
* root or under `src/`. We prefer `src/` if it exists (consistent with the
|
|
180
|
+
* Next default scaffold), otherwise drop one at the project root.
|
|
181
|
+
*/
|
|
182
|
+
function findOrPickInstrumentationPath(rootDir) {
|
|
183
|
+
const candidates = [
|
|
184
|
+
join(rootDir, 'instrumentation.ts'),
|
|
185
|
+
join(rootDir, 'instrumentation.js'),
|
|
186
|
+
join(rootDir, 'src', 'instrumentation.ts'),
|
|
187
|
+
join(rootDir, 'src', 'instrumentation.js'),
|
|
188
|
+
];
|
|
189
|
+
for (const c of candidates) {
|
|
190
|
+
if (existsSync(c))
|
|
191
|
+
return c;
|
|
192
|
+
}
|
|
193
|
+
const useSrc = existsSync(join(rootDir, 'src'));
|
|
194
|
+
return useSrc ? join(rootDir, 'src', 'instrumentation.ts') : join(rootDir, 'instrumentation.ts');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Ensure the instrumentation file calls `register` from
|
|
198
|
+
* `@hover-dev/next/instrumentation`. Returns true if the file already
|
|
199
|
+
* had it wired (so the caller can report "already wired" honestly).
|
|
200
|
+
*
|
|
201
|
+
* We do this as a plain text edit (not magicast) because instrumentation
|
|
202
|
+
* files are usually tiny and the user might have written them in one of
|
|
203
|
+
* many idiomatic shapes (named function, arrow, async). String-level
|
|
204
|
+
* editing keeps formatting stable; magicast's stringifier would reformat.
|
|
205
|
+
*/
|
|
206
|
+
function ensureInstrumentationRegistersHover(filePath) {
|
|
207
|
+
const HOVER_IMPORT = "import { register as registerHover } from '@hover-dev/next/instrumentation';";
|
|
208
|
+
const HOVER_CALL = 'await registerHover();';
|
|
209
|
+
if (!existsSync(filePath)) {
|
|
210
|
+
// Greenfield — write a full instrumentation file.
|
|
211
|
+
const fresh = [
|
|
212
|
+
HOVER_IMPORT,
|
|
213
|
+
'',
|
|
214
|
+
'export async function register() {',
|
|
215
|
+
` ${HOVER_CALL}`,
|
|
216
|
+
'}',
|
|
217
|
+
'',
|
|
218
|
+
].join('\n');
|
|
219
|
+
writeFileSync(filePath, fresh, 'utf-8');
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
223
|
+
if (existing.includes('@hover-dev/next/instrumentation')) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// The file exists but doesn't reference us. We want to (a) add our
|
|
227
|
+
// import at the top, (b) inject `await registerHover();` into the
|
|
228
|
+
// user's existing `register` function if we can find it, or
|
|
229
|
+
// (c) bail to a comment if we can't.
|
|
230
|
+
let next = `${HOVER_IMPORT}\n${existing}`;
|
|
231
|
+
const registerMatch = /(export\s+async\s+function\s+register\s*\([^)]*\)\s*\{)/.exec(next);
|
|
232
|
+
if (registerMatch) {
|
|
233
|
+
next = next.replace(registerMatch[0], `${registerMatch[0]}\n ${HOVER_CALL}`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Couldn't find a function signature to splice into — append a new
|
|
237
|
+
// register export. If the user already has one in a non-standard
|
|
238
|
+
// shape Next will warn at startup, which is fine — better than us
|
|
239
|
+
// silently doing nothing.
|
|
240
|
+
next = `${next}\n\nexport async function register() {\n ${HOVER_CALL}\n}\n`;
|
|
241
|
+
}
|
|
242
|
+
writeFileSync(filePath, next, 'utf-8');
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
89
245
|
// ─── Webpack: new HoverPlugin() in plugins array ────────────────────────
|
|
90
246
|
async function mutateWebpack(configPath) {
|
|
91
247
|
const mod = await loadFile(configPath);
|
|
@@ -172,6 +328,24 @@ export function manualInstructions(id) {
|
|
|
172
328
|
``,
|
|
173
329
|
` modules: ['@hover-dev/nuxt'],`,
|
|
174
330
|
].join('\n');
|
|
331
|
+
case 'next':
|
|
332
|
+
return [
|
|
333
|
+
`Three steps for Next.js:`,
|
|
334
|
+
``,
|
|
335
|
+
`1. Wrap your next.config:`,
|
|
336
|
+
` import { withHover } from '@hover-dev/next';`,
|
|
337
|
+
` export default withHover({ /* your config */ });`,
|
|
338
|
+
``,
|
|
339
|
+
`2. Create instrumentation.ts at your project root:`,
|
|
340
|
+
` import { register as registerHover } from '@hover-dev/next/instrumentation';`,
|
|
341
|
+
` export async function register() {`,
|
|
342
|
+
` await registerHover();`,
|
|
343
|
+
` }`,
|
|
344
|
+
``,
|
|
345
|
+
`3. Render <HoverScript /> in your app/layout.tsx, after {children}:`,
|
|
346
|
+
` import { HoverScript } from '@hover-dev/next';`,
|
|
347
|
+
` // ... inside <body>: {children}<HoverScript />`,
|
|
348
|
+
].join('\n');
|
|
175
349
|
case 'webpack':
|
|
176
350
|
return [
|
|
177
351
|
`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.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|