vue-micro-router 1.0.5 → 1.0.8
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 +30 -12
- package/bin/gen-attrs-types.mjs +89 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -333,26 +333,37 @@ const { title, message, onConfirm } = useMicroState<Attrs>({
|
|
|
333
333
|
**Step 2:** Run code generation:
|
|
334
334
|
|
|
335
335
|
```bash
|
|
336
|
-
|
|
336
|
+
npx vue-micro-router-gen # auto-detects src/ or app/, outputs src/vue-micro-router.d.ts
|
|
337
|
+
npx vue-micro-router-gen -d src # explicit scan directory
|
|
338
|
+
npx vue-micro-router-gen -o types/router.d.ts # custom output path
|
|
337
339
|
```
|
|
338
340
|
|
|
339
|
-
|
|
341
|
+
The script:
|
|
342
|
+
- Scans **all `.ts` files** for `defineFeaturePlugin()` calls (any folder structure)
|
|
343
|
+
- Resolves `@/`, `~/`, `#/` path aliases automatically
|
|
344
|
+
- Finds `.vue` components with `export interface Attrs`
|
|
345
|
+
- Generates `vue-micro-router.d.ts` with Register + AttrsMap augmentations
|
|
340
346
|
|
|
341
|
-
**Step 3:**
|
|
347
|
+
**Step 3:** Include in tsconfig and enjoy typed everywhere:
|
|
348
|
+
|
|
349
|
+
```jsonc
|
|
350
|
+
// tsconfig.json — add the generated file
|
|
351
|
+
{ "include": ["src/**/*.ts", "src/**/*.vue", "src/vue-micro-router.d.ts"] }
|
|
352
|
+
```
|
|
342
353
|
|
|
343
354
|
```ts
|
|
344
355
|
push('profile', { userId: 42, username: 'Danh' }); // ✅ typed, autocomplete
|
|
345
356
|
push('profile'); // ❌ TS error: missing required props
|
|
346
|
-
push('
|
|
357
|
+
push('profile', { meta: { title: 'Hi' } }); // ✅ optional fields can be skipped
|
|
358
|
+
push('home'); // ✅ OK (no Attrs → untyped)
|
|
347
359
|
openDialog('confirm', { title: 'Sure?' }); // ✅ typed
|
|
348
|
-
stepWisePush('profile', { userId: 1, username: 'A' }); // ✅ typed
|
|
349
360
|
```
|
|
350
361
|
|
|
351
362
|
**Rules:**
|
|
352
363
|
- Required fields in `Attrs` → must pass in `push()` / `openDialog()`
|
|
353
|
-
- Optional fields (`?`) → can
|
|
364
|
+
- Optional fields (`?`) → can skip entirely, including the whole props arg
|
|
354
365
|
- Routes without `export interface Attrs` → `push()` accepts any `Record<string, unknown>`
|
|
355
|
-
- Re-run `
|
|
366
|
+
- Re-run `npx vue-micro-router-gen` after adding/changing `Attrs` interfaces
|
|
356
367
|
|
|
357
368
|
### State Serialization
|
|
358
369
|
|
|
@@ -604,12 +615,19 @@ Includes page slide/fade transitions, dialog animations, control fade transition
|
|
|
604
615
|
## Development
|
|
605
616
|
|
|
606
617
|
```bash
|
|
607
|
-
bun run lint
|
|
608
|
-
bun run lint:fix
|
|
609
|
-
bun test
|
|
610
|
-
bun run typecheck
|
|
611
|
-
bun run build
|
|
618
|
+
bun run lint # ESLint check
|
|
619
|
+
bun run lint:fix # Auto-fix lint issues
|
|
620
|
+
bun test # Run all tests
|
|
621
|
+
bun run typecheck # TypeScript strict check
|
|
622
|
+
bun run build # Build package
|
|
623
|
+
bun run gen:types # Generate vue-micro-router.d.ts (local dev)
|
|
612
624
|
bun run dev:example # Run example app
|
|
625
|
+
bun run publish:npm # Bump version + build + publish + tag + release
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
For consumers:
|
|
629
|
+
```bash
|
|
630
|
+
npx vue-micro-router-gen # Generate type augmentations in consumer project
|
|
613
631
|
```
|
|
614
632
|
|
|
615
633
|
## API Reference
|
package/bin/gen-attrs-types.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* npx vue-micro-router-gen -d src # scan specific dir
|
|
13
13
|
* npx vue-micro-router-gen --help
|
|
14
14
|
*/
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, watch as fsWatch } from 'fs';
|
|
16
16
|
import { resolve, relative, dirname, join, extname } from 'path';
|
|
17
17
|
|
|
18
18
|
const CWD = process.cwd();
|
|
@@ -23,10 +23,12 @@ const MODULE_NAME = 'vue-micro-router';
|
|
|
23
23
|
const args = process.argv.slice(2);
|
|
24
24
|
let outputFile = '';
|
|
25
25
|
let scanDir = '';
|
|
26
|
+
let watchMode = false;
|
|
26
27
|
|
|
27
28
|
for (let i = 0; i < args.length; i++) {
|
|
28
29
|
if (args[i] === '-o' || args[i] === '--output') outputFile = args[++i];
|
|
29
30
|
else if (args[i] === '-d' || args[i] === '--dir') scanDir = args[++i];
|
|
31
|
+
else if (args[i] === '-w' || args[i] === '--watch') watchMode = true;
|
|
30
32
|
else if (args[i] === '--help' || args[i] === '-h') {
|
|
31
33
|
console.log(`
|
|
32
34
|
vue-micro-router-gen — Auto-generate typed augmentations
|
|
@@ -36,6 +38,7 @@ Usage: npx vue-micro-router-gen [options]
|
|
|
36
38
|
Options:
|
|
37
39
|
-o, --output <file> Output file (auto: src/vue-micro-router.d.ts or ./vue-micro-router.d.ts)
|
|
38
40
|
-d, --dir <dir> Directory to scan (auto-detects src/, app/, or cwd)
|
|
41
|
+
-w, --watch Watch .vue/.ts files and regenerate on change
|
|
39
42
|
-h, --help Show this help
|
|
40
43
|
|
|
41
44
|
How it works:
|
|
@@ -200,85 +203,106 @@ function hasAttrs(filePath) {
|
|
|
200
203
|
} catch { return false; }
|
|
201
204
|
}
|
|
202
205
|
|
|
203
|
-
// ──
|
|
206
|
+
// ── Generate ────────────────────────────────────────────────────────────────
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
208
|
+
function generate() {
|
|
209
|
+
const plugins = findPlugins();
|
|
210
|
+
if (plugins.length === 0) {
|
|
211
|
+
console.log('No defineFeaturePlugin() calls found.');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
211
214
|
|
|
212
|
-
console.log(
|
|
213
|
-
plugins.forEach(p => console.log(` ${p.relPath} → ${p.exportName}`));
|
|
215
|
+
console.log(`Plugins: ${plugins.length}`);
|
|
216
|
+
plugins.forEach(p => console.log(` ${p.relPath} → ${p.exportName}`));
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
const allEntries = plugins.flatMap(p => extractEntries(p.absPath));
|
|
218
|
+
const allEntries = plugins.flatMap(p => extractEntries(p.absPath));
|
|
217
219
|
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const untyped = { route: 0, dialog: 0, control: 0 };
|
|
220
|
+
const typed = { route: [], dialog: [], control: [] };
|
|
221
|
+
const untyped = { route: 0, dialog: 0, control: 0 };
|
|
221
222
|
|
|
222
|
-
for (const entry of allEntries) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
for (const entry of allEntries) {
|
|
224
|
+
if (entry.componentFile && hasAttrs(entry.componentFile)) {
|
|
225
|
+
typed[entry.kind].push(entry);
|
|
226
|
+
} else {
|
|
227
|
+
untyped[entry.kind]++;
|
|
228
|
+
}
|
|
227
229
|
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
console.log(`Routes: ${typed.route.length} typed / ${untyped.route} untyped`);
|
|
231
|
-
console.log(`Dialogs: ${typed.dialog.length} typed / ${untyped.dialog} untyped`);
|
|
232
|
-
console.log(`Controls: ${typed.control.length} typed / ${untyped.control} untyped`);
|
|
233
230
|
|
|
234
|
-
|
|
231
|
+
console.log(`Routes: ${typed.route.length} typed / ${untyped.route} untyped`);
|
|
232
|
+
console.log(`Dialogs: ${typed.dialog.length} typed / ${untyped.dialog} untyped`);
|
|
233
|
+
console.log(`Controls: ${typed.control.length} typed / ${untyped.control} untyped`);
|
|
235
234
|
|
|
236
|
-
const outDir = dirname(resolve(CWD, outputFile));
|
|
237
|
-
const lines = [
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
// Import plugins
|
|
244
|
-
for (const p of plugins) {
|
|
245
|
-
const rel = relative(outDir, resolve(CWD, p.relPath)).replace(/\\/g, '/');
|
|
246
|
-
const imp = rel.startsWith('.') ? rel : `./${rel}`;
|
|
247
|
-
lines.push(`import type { ${p.exportName} } from '${imp.replace(/\.ts$/, '')}';`);
|
|
248
|
-
}
|
|
235
|
+
const outDir = dirname(resolve(CWD, outputFile));
|
|
236
|
+
const lines = [
|
|
237
|
+
'/* Auto-generated by vue-micro-router-gen. DO NOT EDIT. */',
|
|
238
|
+
'/* Regenerate: npx vue-micro-router-gen */',
|
|
239
|
+
'',
|
|
240
|
+
];
|
|
249
241
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (const e of entries) {
|
|
253
|
-
const alias = `${kind}_${e.key.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
254
|
-
const rel = relative(outDir, resolve(CWD, e.componentFile)).replace(/\\/g, '/');
|
|
242
|
+
for (const p of plugins) {
|
|
243
|
+
const rel = relative(outDir, resolve(CWD, p.relPath)).replace(/\\/g, '/');
|
|
255
244
|
const imp = rel.startsWith('.') ? rel : `./${rel}`;
|
|
256
|
-
lines.push(`import type {
|
|
245
|
+
lines.push(`import type { ${p.exportName} } from '${imp.replace(/\.ts$/, '')}';`);
|
|
257
246
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
lines.push('');
|
|
261
|
-
lines.push(`declare module '${MODULE_NAME}' {`);
|
|
262
247
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
lines.push(' interface Register {');
|
|
266
|
-
lines.push(` plugin: ${plugins.map(p => `typeof ${p.exportName}`).join(' | ')};`);
|
|
267
|
-
lines.push(' }');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// AttrsMap
|
|
271
|
-
for (const [mapName, kind] of [['RouteAttrsMap', 'route'], ['DialogAttrsMap', 'dialog'], ['ControlAttrsMap', 'control']]) {
|
|
272
|
-
if (typed[kind].length > 0) {
|
|
273
|
-
lines.push(` interface ${mapName} {`);
|
|
274
|
-
for (const e of typed[kind]) {
|
|
248
|
+
for (const [kind, entries] of Object.entries(typed)) {
|
|
249
|
+
for (const e of entries) {
|
|
275
250
|
const alias = `${kind}_${e.key.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
276
|
-
|
|
251
|
+
const rel = relative(outDir, resolve(CWD, e.componentFile)).replace(/\\/g, '/');
|
|
252
|
+
const imp = rel.startsWith('.') ? rel : `./${rel}`;
|
|
253
|
+
lines.push(`import type { Attrs as ${alias} } from '${imp}';`);
|
|
277
254
|
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push(`declare module '${MODULE_NAME}' {`);
|
|
259
|
+
|
|
260
|
+
if (plugins.length > 0) {
|
|
261
|
+
lines.push(' interface Register {');
|
|
262
|
+
lines.push(` plugin: ${plugins.map(p => `typeof ${p.exportName}`).join(' | ')};`);
|
|
278
263
|
lines.push(' }');
|
|
279
264
|
}
|
|
265
|
+
|
|
266
|
+
for (const [mapName, kind] of [['RouteAttrsMap', 'route'], ['DialogAttrsMap', 'dialog'], ['ControlAttrsMap', 'control']]) {
|
|
267
|
+
if (typed[kind].length > 0) {
|
|
268
|
+
lines.push(` interface ${mapName} {`);
|
|
269
|
+
for (const e of typed[kind]) {
|
|
270
|
+
const alias = `${kind}_${e.key.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
271
|
+
lines.push(` '${e.key}': ${alias};`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(' }');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines.push('}', '');
|
|
278
|
+
writeFileSync(resolve(CWD, outputFile), lines.join('\n'));
|
|
279
|
+
console.log(`✅ ${outputFile}`);
|
|
280
|
+
return true;
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
// ── Run ─────────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
generate();
|
|
286
|
+
|
|
287
|
+
if (watchMode) {
|
|
288
|
+
console.log(`\n👀 Watching ${scanDir}/ for .vue/.ts changes...\n`);
|
|
289
|
+
|
|
290
|
+
let debounce = null;
|
|
291
|
+
const regenerate = () => {
|
|
292
|
+
clearTimeout(debounce);
|
|
293
|
+
debounce = setTimeout(() => {
|
|
294
|
+
console.log(`\n[${new Date().toLocaleTimeString()}] Change detected, regenerating...`);
|
|
295
|
+
generate();
|
|
296
|
+
}, 300);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const outputBasename = outputFile.split('/').pop();
|
|
300
|
+
|
|
301
|
+
fsWatch(SCAN_PATH, { recursive: true }, (event, filename) => {
|
|
302
|
+
if (!filename) return;
|
|
303
|
+
// Skip output file to prevent infinite loop
|
|
304
|
+
if (filename.endsWith(outputBasename)) return;
|
|
305
|
+
const ext = extname(filename);
|
|
306
|
+
if (ext === '.vue' || ext === '.ts') regenerate();
|
|
307
|
+
});
|
|
308
|
+
}
|