@quazardous/qdadm 2.13.0 → 2.15.0
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 +1 -0
- package/package.json +6 -4
- package/src/chain/index.ts +5 -0
- package/src/components/InfoBanner.vue +3 -3
- package/src/components/base/QdButton.vue +20 -0
- package/src/components/base/QdMessage.vue +17 -0
- package/src/components/dialogs/MultiStepDialog.vue +5 -5
- package/src/components/dialogs/SimpleDialog.vue +4 -4
- package/src/components/dialogs/UnsavedChangesDialog.vue +2 -2
- package/src/components/display/CopyableId.vue +2 -2
- package/src/components/edit/FormActions.vue +4 -4
- package/src/components/edit/FormPage.vue +7 -7
- package/src/components/edit/LookupPickerDialog.vue +3 -3
- package/src/components/editors/JsonEditorFoldable.vue +8 -8
- package/src/components/editors/JsonStructuredField.vue +3 -3
- package/src/components/editors/KeyValueEditor.vue +3 -3
- package/src/components/editors/LanguageEditor.vue +5 -5
- package/src/components/editors/ScopeEditor.vue +3 -3
- package/src/components/index.ts +4 -0
- package/src/components/layout/AppLayout.vue +2 -2
- package/src/components/layout/PageLayout.vue +2 -2
- package/src/components/layout/defaults/DefaultUserInfo.vue +2 -2
- package/src/components/lists/ActionButtons.vue +2 -2
- package/src/components/lists/ListPage.vue +4 -4
- package/src/components/pages/LoginPage.vue +2 -2
- package/src/components/show/ShowPage.vue +6 -6
- package/src/composables/useEntityItemShowPage.ts +8 -0
- package/src/composables/useListPage.ts +23 -0
- package/src/composables/useListPage.types.ts +3 -0
- package/src/composables/useLiveEntity.ts +115 -0
- package/src/deferred/index.ts +5 -0
- package/src/entity/EntityManager.cache.ts +20 -0
- package/src/entity/EntityManager.interface.ts +3 -0
- package/src/entity/EntityManager.ts +29 -1
- package/src/entity/EntityManager.types.ts +32 -0
- package/src/gen/vite-plugin.ts +4 -0
- package/src/index.ts +10 -0
- package/src/kernel/Kernel.registries.ts +31 -5
- package/src/kernel/Kernel.ts +3 -0
- package/src/kernel/Kernel.types.ts +31 -0
- package/src/kernel/LiveEntityRouter.ts +189 -0
- package/src/kernel/SSEBridge.ts +3 -0
- package/src/query/index.ts +7 -0
- package/src/security/pages/RoleList.vue +3 -3
- package/src/types/index.ts +2 -0
- package/src/utils/pluralize.ts +472 -0
- package/src/vite/qdadmDebugPlugin.ts +178 -2
- package/src/vite/qdadmPlugin.ts +5 -19
- package/src/vite-env.d.ts +0 -16
- package/src/types/pluralize.d.ts +0 -19
|
@@ -34,9 +34,22 @@ interface PendingRequest {
|
|
|
34
34
|
timeout: ReturnType<typeof setTimeout>
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Typed page-side queries (#1398) — served by the injected client. */
|
|
38
|
+
export type DebugQueryType =
|
|
39
|
+
| 'describe'
|
|
40
|
+
| 'dump'
|
|
41
|
+
| 'call'
|
|
42
|
+
| 'sessionInfo'
|
|
43
|
+
| 'routes'
|
|
44
|
+
| 'entityState'
|
|
45
|
+
| 'entityCall'
|
|
46
|
+
| 'storageDump'
|
|
47
|
+
| 'bootlog'
|
|
48
|
+
| 'recentSignals'
|
|
49
|
+
|
|
37
50
|
interface DebugRequest {
|
|
38
51
|
id: string
|
|
39
|
-
type:
|
|
52
|
+
type: DebugQueryType
|
|
40
53
|
payload?: unknown
|
|
41
54
|
/** Target a specific session (server → client). */
|
|
42
55
|
sessionId?: string
|
|
@@ -67,6 +80,26 @@ interface SessionState {
|
|
|
67
80
|
meta: Record<string, unknown>
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
/** Broker surface exposed to sibling plugins via `plugin.api` (#1398). */
|
|
84
|
+
export interface QdadmDebugPluginApi {
|
|
85
|
+
/** Send a typed query to the page and await its reply. */
|
|
86
|
+
ask: (type: DebugQueryType, payload?: unknown, sessionId?: string) => Promise<unknown>
|
|
87
|
+
/** Resolve a session param ('latest' or id) to a live session, if any. */
|
|
88
|
+
pickSession: (
|
|
89
|
+
sessionParam: string | null
|
|
90
|
+
) => { id: string; lastSeenAt: number; meta: Record<string, unknown> } | null
|
|
91
|
+
listSessions: () => Array<{
|
|
92
|
+
id: string
|
|
93
|
+
firstSeenAt: number
|
|
94
|
+
lastSeenAt: number
|
|
95
|
+
ageMs: number
|
|
96
|
+
hasDescribe: boolean
|
|
97
|
+
hasSnapshot: boolean
|
|
98
|
+
meta: Record<string, unknown>
|
|
99
|
+
}>
|
|
100
|
+
prefix: string
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
export interface QdadmDebugPluginOptions {
|
|
71
104
|
/** URL prefix (default '/__qdadm') */
|
|
72
105
|
prefix?: string
|
|
@@ -203,9 +236,21 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
203
236
|
return s
|
|
204
237
|
}
|
|
205
238
|
|
|
239
|
+
/** Inter-plugin API (#1398) — consumed by @quazardous/qdmcp. */
|
|
240
|
+
const api: QdadmDebugPluginApi = {
|
|
241
|
+
ask,
|
|
242
|
+
pickSession: (sessionParam) => {
|
|
243
|
+
const s = pickSession(sessionParam)
|
|
244
|
+
return s ? { id: s.id, lastSeenAt: s.lastSeenAt, meta: s.meta } : null
|
|
245
|
+
},
|
|
246
|
+
listSessions,
|
|
247
|
+
prefix,
|
|
248
|
+
}
|
|
249
|
+
|
|
206
250
|
return {
|
|
207
251
|
name: 'qdadm-debug',
|
|
208
252
|
apply: 'serve',
|
|
253
|
+
api,
|
|
209
254
|
|
|
210
255
|
transformIndexHtml: {
|
|
211
256
|
order: 'pre',
|
|
@@ -294,7 +339,10 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
294
339
|
res.setHeader('x-qdadm-session', session.id)
|
|
295
340
|
return sendJson(res, 200, data)
|
|
296
341
|
}
|
|
297
|
-
|
|
342
|
+
// Unknown subpath: fall through — sibling plugins (e.g.
|
|
343
|
+
// @quazardous/qdadm-mcp) mount their own endpoints under the
|
|
344
|
+
// same prefix (#1398).
|
|
345
|
+
return next()
|
|
298
346
|
} catch (e) {
|
|
299
347
|
return sendJson(res, 500, { error: (e as Error).message })
|
|
300
348
|
}
|
|
@@ -333,6 +381,34 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
333
381
|
* stays fresh. Listens for ad-hoc requests targeted at this session.
|
|
334
382
|
*/
|
|
335
383
|
const HMR_CLIENT_SCRIPT = String.raw`
|
|
384
|
+
// ── Boot capture (#1398): this head module runs BEFORE the app's entry
|
|
385
|
+
// module, so console/window errors from the very first app statement are
|
|
386
|
+
// buffered — visible over MCP even when the app dies before the bridge
|
|
387
|
+
// exists (the class of failure a bridge-only design can never see).
|
|
388
|
+
const __bootAt = Date.now()
|
|
389
|
+
const __bootlog = { errors: [], warns: [], pageErrors: [], rejections: [] }
|
|
390
|
+
function __cap(arr, entry) { arr.push(entry); if (arr.length > 200) arr.shift() }
|
|
391
|
+
function __fmt(args) {
|
|
392
|
+
try {
|
|
393
|
+
return Array.from(args).map((a) => {
|
|
394
|
+
if (a instanceof Error) return a.stack || a.message
|
|
395
|
+
if (typeof a === 'object') { try { return JSON.stringify(a) } catch (e) { return String(a) } }
|
|
396
|
+
return String(a)
|
|
397
|
+
}).join(' ').slice(0, 2000)
|
|
398
|
+
} catch (e) { return '[unformattable]' }
|
|
399
|
+
}
|
|
400
|
+
const __origError = console.error.bind(console)
|
|
401
|
+
const __origWarn = console.warn.bind(console)
|
|
402
|
+
console.error = function () { __cap(__bootlog.errors, { at: Date.now(), msg: __fmt(arguments) }); __origError.apply(null, arguments) }
|
|
403
|
+
console.warn = function () { __cap(__bootlog.warns, { at: Date.now(), msg: __fmt(arguments) }); __origWarn.apply(null, arguments) }
|
|
404
|
+
window.addEventListener('error', (e) => {
|
|
405
|
+
__cap(__bootlog.pageErrors, { at: Date.now(), msg: String(e.message || e), source: e.filename ? e.filename + ':' + e.lineno : undefined })
|
|
406
|
+
})
|
|
407
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
408
|
+
let msg; try { msg = e.reason && (e.reason.stack || e.reason.message) || String(e.reason) } catch (err) { msg = '[unreadable reason]' }
|
|
409
|
+
__cap(__bootlog.rejections, { at: Date.now(), msg: String(msg).slice(0, 2000) })
|
|
410
|
+
})
|
|
411
|
+
|
|
336
412
|
const hot = import.meta.hot
|
|
337
413
|
if (!hot) {
|
|
338
414
|
console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
|
|
@@ -344,6 +420,95 @@ if (!hot) {
|
|
|
344
420
|
|
|
345
421
|
const w = /** @type {any} */ (window)
|
|
346
422
|
|
|
423
|
+
// Signal ring buffer — armed once __qdadm.signals is reachable.
|
|
424
|
+
const __signals = []
|
|
425
|
+
let __signalsArmed = false
|
|
426
|
+
function __armSignals() {
|
|
427
|
+
if (__signalsArmed) return
|
|
428
|
+
const q = w.__qdadm
|
|
429
|
+
if (!q || !q.signals || typeof q.signals.on !== 'function') return
|
|
430
|
+
try {
|
|
431
|
+
q.signals.on('**', (event) => { __cap(__signals, { at: Date.now(), name: event && event.name }) ; if (__signals.length > 100) __signals.shift() })
|
|
432
|
+
__signalsArmed = true
|
|
433
|
+
} catch (e) {}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function __sessionInfo() {
|
|
437
|
+
const q = w.__qdadm || {}
|
|
438
|
+
let app = {}
|
|
439
|
+
try { app = (q.kernel && q.kernel.options && q.kernel.options.app) || {} } catch (e) {}
|
|
440
|
+
let route = null
|
|
441
|
+
try { const r = q.router && q.router.currentRoute && q.router.currentRoute.value; route = r ? { name: r.name, fullPath: r.fullPath } : null } catch (e) {}
|
|
442
|
+
return {
|
|
443
|
+
sessionId, bootAt: __bootAt, now: Date.now(), url: location.href,
|
|
444
|
+
app: { name: app.name, version: typeof app.version === 'function' ? app.version() : app.version },
|
|
445
|
+
route, bridgeReady: !!bridge, signalsArmed: __signalsArmed,
|
|
446
|
+
bootlogCounts: { errors: __bootlog.errors.length, warns: __bootlog.warns.length, pageErrors: __bootlog.pageErrors.length, rejections: __bootlog.rejections.length },
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function __routes() {
|
|
451
|
+
const q = w.__qdadm
|
|
452
|
+
if (!q || !q.router) throw new Error('router not ready')
|
|
453
|
+
return q.router.getRoutes().map((r) => ({
|
|
454
|
+
name: r.name, path: r.path,
|
|
455
|
+
meta: { entity: r.meta && r.meta.entity, layout: r.meta && r.meta.layout, requiresAuth: r.meta && r.meta.requiresAuth, public: r.meta && r.meta.public },
|
|
456
|
+
}))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function __manager(entity) {
|
|
460
|
+
const q = w.__qdadm
|
|
461
|
+
if (!q || !q.orchestrator) throw new Error('orchestrator not ready')
|
|
462
|
+
if (!q.orchestrator.isRegistered(entity)) throw new Error('entity not registered: ' + entity + ' (known: ' + q.orchestrator.getRegisteredNames().join(', ') + ')')
|
|
463
|
+
return q.orchestrator.get(entity)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function __entityState(payload) {
|
|
467
|
+
const q = w.__qdadm
|
|
468
|
+
if (!payload || !payload.entity) {
|
|
469
|
+
return { entities: q && q.orchestrator ? q.orchestrator.getRegisteredNames() : [] }
|
|
470
|
+
}
|
|
471
|
+
const m = __manager(payload.entity)
|
|
472
|
+
const safe = (fn) => { try { return fn() } catch (e) { return '[error: ' + (e && e.message) + ']' } }
|
|
473
|
+
const fields = {}
|
|
474
|
+
try {
|
|
475
|
+
const fs = m.fields || m._fields || {}
|
|
476
|
+
for (const k in fs) fields[k] = { type: fs[k] && fs[k].type, label: fs[k] && fs[k].label, required: !!(fs[k] && fs[k].required) }
|
|
477
|
+
} catch (e) {}
|
|
478
|
+
return {
|
|
479
|
+
entity: payload.entity,
|
|
480
|
+
idField: safe(() => m.idField), labelField: safe(() => m.labelField),
|
|
481
|
+
label: safe(() => m.label), labelPlural: safe(() => m.labelPlural),
|
|
482
|
+
can: { create: safe(() => m.canCreate()), read: safe(() => m.canRead()), update: safe(() => m.canUpdate()), delete: safe(() => m.canDelete()) },
|
|
483
|
+
fields,
|
|
484
|
+
storage: safe(() => { const st = m.storage || m._storage; return st ? { kind: st.constructor && st.constructor.name, storageKey: st._storageKey } : null }),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function __entityCall(payload) {
|
|
489
|
+
if (!payload || !payload.entity || !payload.op) throw new Error('entityCall requires { entity, op, ... }')
|
|
490
|
+
const m = __manager(payload.entity)
|
|
491
|
+
const op = payload.op
|
|
492
|
+
if (op === 'list') return await m.list(payload.params || {})
|
|
493
|
+
if (op === 'get') return await m.get(payload.id)
|
|
494
|
+
if (op === 'create') return await m.create(payload.data || {})
|
|
495
|
+
if (op === 'update') return await m.update(payload.id, payload.data || {})
|
|
496
|
+
if (op === 'delete') { await m.delete(payload.id); return { deleted: payload.id } }
|
|
497
|
+
throw new Error('unknown op "' + op + '" (list|get|create|update|delete)')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function __storageDump(payload) {
|
|
501
|
+
if (!payload || !payload.entity) throw new Error('storageDump requires { entity }')
|
|
502
|
+
const m = __manager(payload.entity)
|
|
503
|
+
const st = m.storage || m._storage
|
|
504
|
+
const key = st && st._storageKey
|
|
505
|
+
if (!key) return { storageKey: null, note: 'storage exposes no localStorage key (' + (st && st.constructor && st.constructor.name) + ')' }
|
|
506
|
+
const raw = localStorage.getItem(key)
|
|
507
|
+
let parsed = null
|
|
508
|
+
try { parsed = raw ? JSON.parse(raw) : null } catch (e) { parsed = '[unparsable]' }
|
|
509
|
+
return { storageKey: key, count: Array.isArray(parsed) ? parsed.length : null, data: parsed }
|
|
510
|
+
}
|
|
511
|
+
|
|
347
512
|
function helloIfNeeded() {
|
|
348
513
|
try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
|
|
349
514
|
}
|
|
@@ -365,6 +530,16 @@ if (!hot) {
|
|
|
365
530
|
if (msg && msg.sessionId && msg.sessionId !== sessionId) return
|
|
366
531
|
const id = (msg && msg.id) || ''
|
|
367
532
|
try {
|
|
533
|
+
// Typed queries (#1398): bridge-independent — they must answer even
|
|
534
|
+
// when the app died before the bridge existed.
|
|
535
|
+
if (msg.type === 'bootlog') { hot.send('qdadm:debug:reply', { id, ok: true, data: __bootlog, sessionId }); return }
|
|
536
|
+
if (msg.type === 'sessionInfo') { hot.send('qdadm:debug:reply', { id, ok: true, data: __sessionInfo(), sessionId }); return }
|
|
537
|
+
if (msg.type === 'recentSignals') { __armSignals(); hot.send('qdadm:debug:reply', { id, ok: true, data: { armed: __signalsArmed, events: __signals }, sessionId }); return }
|
|
538
|
+
if (msg.type === 'routes') { hot.send('qdadm:debug:reply', { id, ok: true, data: __routes(), sessionId }); return }
|
|
539
|
+
if (msg.type === 'entityState') { hot.send('qdadm:debug:reply', { id, ok: true, data: __entityState(msg.payload), sessionId }); return }
|
|
540
|
+
if (msg.type === 'entityCall') { const data = await __entityCall(msg.payload); hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId }); return }
|
|
541
|
+
if (msg.type === 'storageDump') { hot.send('qdadm:debug:reply', { id, ok: true, data: __storageDump(msg.payload), sessionId }); return }
|
|
542
|
+
|
|
368
543
|
const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
|
|
369
544
|
if (!b) throw new Error('debug bridge not ready')
|
|
370
545
|
let data
|
|
@@ -385,6 +560,7 @@ if (!hot) {
|
|
|
385
560
|
bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
|
|
386
561
|
if (!bridge) return
|
|
387
562
|
clearInterval(attachTimer)
|
|
563
|
+
__armSignals()
|
|
388
564
|
helloIfNeeded()
|
|
389
565
|
pushDescribe()
|
|
390
566
|
pushSnapshot()
|
package/src/vite/qdadmPlugin.ts
CHANGED
|
@@ -23,13 +23,11 @@
|
|
|
23
23
|
* - `optimizeDeps.exclude` keeps primevue/@primeuix/themes/qdadm out of the
|
|
24
24
|
* pre-bundle so BOTH pipelines resolve the same raw modules. Excluding
|
|
25
25
|
* qdadm also keeps HMR sane when the package is consumed via a symlink.
|
|
26
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* nested path can't resolve) forms are declared; the unused one is a
|
|
32
|
-
* no-op warning at worst.
|
|
26
|
+
* - qdadm has NO CJS transitives (pluralize is vendored as ESM, #1454), so
|
|
27
|
+
* there is nothing to `optimizeDeps.include` — which also means nothing
|
|
28
|
+
* for the optimizer to fail resolving in file:-linked installs where
|
|
29
|
+
* npm (install-links=false) never materializes qdadm's deps at the
|
|
30
|
+
* consumer root.
|
|
33
31
|
* - When `node_modules/@quazardous/qdadm` is a symlink (file:/workspace
|
|
34
32
|
* link — qdadm's own dev mode against a testbed app), its realpath is
|
|
35
33
|
* appended to `server.fs.allow` so qdadm's `?raw` dynamic imports (i18n
|
|
@@ -44,9 +42,6 @@ import fs from 'node:fs'
|
|
|
44
42
|
import path from 'node:path'
|
|
45
43
|
import { searchForWorkspaceRoot, type Plugin, type UserConfig } from 'vite'
|
|
46
44
|
|
|
47
|
-
/** qdadm's CJS transitive deps that must be pre-bundled (internal knowledge). */
|
|
48
|
-
const CJS_TRANSITIVES = ['pluralize']
|
|
49
|
-
|
|
50
45
|
export interface QdadmVitePluginOptions {
|
|
51
46
|
/**
|
|
52
47
|
* Extra packages to dedupe on top of the defaults
|
|
@@ -84,12 +79,10 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
|
84
79
|
// Symlinked install (file:/workspace link)? Allow serving from the
|
|
85
80
|
// link target, which typically lives outside the consumer's root.
|
|
86
81
|
const fsAllow: string[] = []
|
|
87
|
-
let symlinked = false
|
|
88
82
|
try {
|
|
89
83
|
const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm')
|
|
90
84
|
const real = fs.realpathSync(pkgDir)
|
|
91
85
|
if (real !== pkgDir) {
|
|
92
|
-
symlinked = true
|
|
93
86
|
fsAllow.push(searchForWorkspaceRoot(root), real)
|
|
94
87
|
}
|
|
95
88
|
} catch {
|
|
@@ -123,13 +116,6 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
|
123
116
|
},
|
|
124
117
|
optimizeDeps: {
|
|
125
118
|
exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
|
|
126
|
-
// One form per install mode: the nested path resolves through a
|
|
127
|
-
// real node_modules chain (npm installs), the plain form through a
|
|
128
|
-
// symlink realpath — declaring the unused one logs a "Failed to
|
|
129
|
-
// resolve dependency" warning (skybot testbed, #1389).
|
|
130
|
-
include: symlinked
|
|
131
|
-
? [...CJS_TRANSITIVES]
|
|
132
|
-
: CJS_TRANSITIVES.map((dep) => `@quazardous/qdadm > ${dep}`),
|
|
133
119
|
},
|
|
134
120
|
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
135
121
|
}
|
package/src/vite-env.d.ts
CHANGED
|
@@ -32,19 +32,3 @@ declare module '*.sass' {
|
|
|
32
32
|
const content: string
|
|
33
33
|
export default content
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
// `pluralize` ships as plain JS without bundled `.d.ts`. The official
|
|
37
|
-
// types live in `@types/pluralize` but pulling them in as a runtime
|
|
38
|
-
// dep here is overkill for a single-call usage. Declare the minimum
|
|
39
|
-
// surface we use so `strict` mode is happy without DefinitelyTyped.
|
|
40
|
-
declare module 'pluralize' {
|
|
41
|
-
interface PluralizeFn {
|
|
42
|
-
(word: string, count?: number, inclusive?: boolean): string
|
|
43
|
-
singular(word: string): string
|
|
44
|
-
plural(word: string, count?: number, inclusive?: boolean): string
|
|
45
|
-
isPlural(word: string): boolean
|
|
46
|
-
isSingular(word: string): boolean
|
|
47
|
-
}
|
|
48
|
-
const pluralize: PluralizeFn
|
|
49
|
-
export default pluralize
|
|
50
|
-
}
|
package/src/types/pluralize.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type declarations for pluralize module
|
|
3
|
-
*/
|
|
4
|
-
declare module 'pluralize' {
|
|
5
|
-
function pluralize(word: string, count?: number, inclusive?: boolean): string
|
|
6
|
-
|
|
7
|
-
namespace pluralize {
|
|
8
|
-
function plural(word: string): string
|
|
9
|
-
function singular(word: string): string
|
|
10
|
-
function isPlural(word: string): boolean
|
|
11
|
-
function isSingular(word: string): boolean
|
|
12
|
-
function addPluralRule(rule: string | RegExp, replacement: string): void
|
|
13
|
-
function addSingularRule(rule: string | RegExp, replacement: string): void
|
|
14
|
-
function addIrregularRule(single: string, plural: string): void
|
|
15
|
-
function addUncountableRule(word: string | RegExp): void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export = pluralize
|
|
19
|
-
}
|