@quazardous/qdadm 2.10.0 → 2.12.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 +13 -123
- package/package.json +7 -2
- package/src/chain/StackHydrator.ts +1 -3
- package/src/components/dialogs/MultiStepDialog.vue +1 -1
- package/src/components/edit/FormInput.vue +12 -7
- package/src/components/layout/AppLayout.vue +4 -1
- package/src/components/layout/defaults/DefaultUserInfo.vue +4 -1
- package/src/components/show/ShowDisplay.vue +6 -1
- package/src/components/show/ShowField.vue +3 -1
- package/src/composables/useAuth.ts +4 -2
- package/src/composables/useEntityItemFormPage.types.ts +1 -1
- package/src/composables/useEntityItemPage.ts +5 -5
- package/src/composables/useEntityItemShowPage.ts +1 -1
- package/src/composables/useNavContext.ts +10 -1
- package/src/entity/auth/SecurityChecker.ts +23 -0
- package/src/entity/storage/MockApiStorage.ts +6 -1
- package/src/kernel/Kernel.registries.ts +10 -0
- package/src/kernel/Kernel.types.ts +9 -2
- package/src/kernel/ModuleLoader.ts +0 -6
- package/src/orchestrator/Orchestrator.ts +2 -2
- package/src/query/FilterQuery.ts +0 -4
- package/src/security/EntityRolesProvider.ts +0 -3
- package/src/styles/index.d.ts +5 -0
- package/src/types/index.ts +6 -0
- package/src/utils/formatters.ts +14 -0
- package/src/vite/qdadmPlugin.ts +97 -0
package/README.md
CHANGED
|
@@ -2,143 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
**Vue 3 admin framework. PrimeVue. Zero boilerplate.**
|
|
4
4
|
|
|
5
|
-
Quick start: [README](https://github.com/quazardous/qdadm/blob/main/README.md) (5 min tutorial)
|
|
6
|
-
|
|
7
|
-
Concepts & patterns: [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md)
|
|
8
|
-
|
|
9
|
-
Changelog: [CHANGELOG](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
|
|
10
|
-
|
|
11
5
|
## Installation
|
|
12
6
|
|
|
13
7
|
```bash
|
|
14
|
-
npm install @quazardous/qdadm primevue @primeuix/themes
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Exports
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
// Core
|
|
21
|
-
import { Kernel, EntityManager } from '@quazardous/qdadm'
|
|
22
|
-
|
|
23
|
-
// Storage backends
|
|
24
|
-
import { MockApiStorage, ApiStorage, SdkStorage } from '@quazardous/qdadm'
|
|
25
|
-
|
|
26
|
-
// Auth
|
|
27
|
-
import { SessionAuthAdapter, LocalStorageSessionAuthAdapter } from '@quazardous/qdadm'
|
|
28
|
-
|
|
29
|
-
// Security (permissions, roles)
|
|
30
|
-
import { SecurityChecker, PermissionMatcher, PermissionRegistry } from '@quazardous/qdadm/security'
|
|
31
|
-
import { PersistableRoleProvider, createLocalStorageRolesProvider } from '@quazardous/qdadm/security'
|
|
32
|
-
|
|
33
|
-
// Composables
|
|
34
|
-
import { useBareForm, useListPage } from '@quazardous/qdadm/composables'
|
|
35
|
-
|
|
36
|
-
// Components
|
|
37
|
-
import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
|
|
38
|
-
|
|
39
|
-
// Module system
|
|
40
|
-
import { KernelContext } from '@quazardous/qdadm/module'
|
|
41
|
-
|
|
42
|
-
// Styles
|
|
43
|
-
import '@quazardous/qdadm/styles'
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## ctx.crud() - Route Helper
|
|
47
|
-
|
|
48
|
-
Register CRUD routes + navigation in one call:
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
class BooksModule extends Module {
|
|
52
|
-
async connect(ctx) {
|
|
53
|
-
ctx.entity('books', { ... })
|
|
54
|
-
|
|
55
|
-
// Full CRUD with single form
|
|
56
|
-
ctx.crud('books', {
|
|
57
|
-
list: () => import('./pages/BookList.vue'),
|
|
58
|
-
form: () => import('./pages/BookForm.vue') // create + edit
|
|
59
|
-
}, {
|
|
60
|
-
nav: { section: 'Library', icon: 'pi pi-book' }
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// List only (read-only entity)
|
|
64
|
-
ctx.crud('settings', {
|
|
65
|
-
list: () => import('./pages/SettingsPage.vue')
|
|
66
|
-
}, { nav: { section: 'Config', icon: 'pi pi-cog' } })
|
|
67
|
-
}
|
|
68
|
-
}
|
|
8
|
+
npm install @quazardous/qdadm primevue @primeuix/themes primeicons vue-router pinia
|
|
69
9
|
```
|
|
70
10
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Adapter for generated SDK clients (hey-api, openapi-generator, etc.):
|
|
76
|
-
|
|
77
|
-
```js
|
|
78
|
-
import { SdkStorage } from '@quazardous/qdadm'
|
|
11
|
+
```ts
|
|
12
|
+
// vite.config.ts — the plugin wires the resolver/optimizer settings
|
|
13
|
+
// qdadm's raw-source packaging needs (npm and file:/workspace installs)
|
|
14
|
+
import { qdadmVitePlugin } from '@quazardous/qdadm/vite'
|
|
79
15
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
methods: {
|
|
83
|
-
list: 'getApiBooks',
|
|
84
|
-
get: 'getApiBooksById',
|
|
85
|
-
create: 'postApiBooks',
|
|
86
|
-
update: 'patchApiBooksById',
|
|
87
|
-
delete: 'deleteApiBooksById'
|
|
88
|
-
}
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
plugins: [vue(), qdadmVitePlugin()],
|
|
89
18
|
})
|
|
90
19
|
```
|
|
91
20
|
|
|
92
|
-
Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSidePagination`.
|
|
93
|
-
|
|
94
21
|
## Documentation
|
|
95
22
|
|
|
96
|
-
|
|
97
|
-
|-----|---------|
|
|
98
|
-
| [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md) | Philosophy, patterns, concepts |
|
|
99
|
-
| [Page compositions](https://github.com/quazardous/qdadm/blob/main/docs/page-compositions.md) | **Start here** — which composition for which need |
|
|
100
|
-
| [CRUD Pages](https://github.com/quazardous/qdadm/blob/main/docs/crud.md) | List / form / show / child pages reference |
|
|
101
|
-
| [Navigation](https://github.com/quazardous/qdadm/blob/main/docs/navigation.md) | Breadcrumb, sibling navlinks, View↔Edit toggle |
|
|
102
|
-
| [Architecture](https://github.com/quazardous/qdadm/blob/main/docs/architecture.md) | PAC pattern, layers |
|
|
103
|
-
| [Extension](https://github.com/quazardous/qdadm/blob/main/docs/extension.md) | Decorators, bundles, multi-storage |
|
|
104
|
-
| [Hooks](https://github.com/quazardous/qdadm/blob/main/docs/hooks.md) | Alter / invoke registry |
|
|
105
|
-
| [Signals](https://github.com/quazardous/qdadm/blob/main/docs/signals.md) | Event bus, SSE bridge |
|
|
106
|
-
| [Zones](https://github.com/quazardous/qdadm/blob/main/docs/zones.md) | Extensible UI blocks |
|
|
107
|
-
| [Security](https://github.com/quazardous/qdadm/blob/main/docs/security.md) | Permissions, roles |
|
|
108
|
-
| [i18n](https://github.com/quazardous/qdadm/blob/main/docs/i18n.md) | Messages, providers, strategies |
|
|
109
|
-
| [Codegen](https://github.com/quazardous/qdadm/blob/main/docs/gen.md) | Generate managers (OpenAPI / manual) |
|
|
110
|
-
| [Deferred](https://github.com/quazardous/qdadm/blob/main/docs/deferred.md) | Deferred values & warmup |
|
|
111
|
-
| [AGENT_GUIDE](https://github.com/quazardous/qdadm/blob/main/docs/AGENT_GUIDE.md) | Quick index for AI agents |
|
|
112
|
-
|
|
113
|
-
## Peer Dependencies
|
|
114
|
-
|
|
115
|
-
- vue ^3.3.0
|
|
116
|
-
- vue-router ^4.0.0
|
|
117
|
-
- primevue ^4.0.0
|
|
118
|
-
- pinia ^2.0.0
|
|
119
|
-
|
|
120
|
-
## Consuming via `file:` link from a sibling monorepo
|
|
23
|
+
Start with the **[step-by-step tutorial](https://github.com/quazardous/qdadm/blob/main/docs/tutorial-mini-admin.md)** — bootstrap, CRUD, auth, navigation, parent-child in ~310 lines.
|
|
121
24
|
|
|
122
|
-
|
|
25
|
+
- [Full documentation](https://github.com/quazardous/qdadm/tree/main/docs)
|
|
26
|
+
- [Concepts & patterns (CREDO)](https://github.com/quazardous/qdadm/blob/main/docs/QDADM_CREDO.md)
|
|
27
|
+
- [Changelog](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
|
|
123
28
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Fix in the host's bundler config — `dedupe` collapses duplicates back to a single resolved copy:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
// vite.config.ts
|
|
130
|
-
export default defineConfig({
|
|
131
|
-
resolve: {
|
|
132
|
-
dedupe: ['vue', 'vue-router', 'primevue', '@primeuix/themes'],
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
This is the canonical Vite/Webpack pattern for Vue plugin singletons. The `file:` link can stay in place — useful for hot-reload during qdadm development — and the dedupe ensures only one instance of each peer ends up in the bundle.
|
|
138
|
-
|
|
139
|
-
Installing qdadm from npm (`@quazardous/qdadm@^1.19.3`) doesn't have this issue: the published tarball ships no `node_modules/`, so peers always resolve from the host. The trap is specific to `file:` / `workspace:` links onto a package that lives inside another monorepo.
|
|
29
|
+
## Peer Dependencies
|
|
140
30
|
|
|
141
|
-
|
|
31
|
+
vue ^3.3 · vue-router ^4 || ^5 · primevue ^4 · pinia ^2 || ^3 || ^4
|
|
142
32
|
|
|
143
33
|
## License
|
|
144
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,10 +35,14 @@
|
|
|
35
35
|
"./module": "./src/module/index.ts",
|
|
36
36
|
"./utils": "./src/utils/index.ts",
|
|
37
37
|
"./modules/debug": "./src/modules/debug/index.ts",
|
|
38
|
-
"./styles":
|
|
38
|
+
"./styles": {
|
|
39
|
+
"types": "./src/styles/index.d.ts",
|
|
40
|
+
"default": "./src/styles/index.scss"
|
|
41
|
+
},
|
|
39
42
|
"./styles/variables": "./src/styles/_variables.scss",
|
|
40
43
|
"./gen": "./src/gen/index.ts",
|
|
41
44
|
"./gen/vite-plugin": "./src/gen/vite-plugin.ts",
|
|
45
|
+
"./vite": "./src/vite/qdadmPlugin.ts",
|
|
42
46
|
"./vite-plugin-debug": "./src/vite/qdadmDebugPlugin.ts"
|
|
43
47
|
},
|
|
44
48
|
"files": [
|
|
@@ -48,6 +52,7 @@
|
|
|
48
52
|
],
|
|
49
53
|
"dependencies": {
|
|
50
54
|
"@quazardous/qdcore": "^1.0.0",
|
|
55
|
+
"@types/pluralize": "^0.0.33",
|
|
51
56
|
"@quazardous/qddebug": "^1.0.0",
|
|
52
57
|
"@quazardous/quarkernel": "^2.1.0",
|
|
53
58
|
"pluralize": "^8.0.0",
|
|
@@ -54,7 +54,6 @@ export interface HydratedPayload {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export class StackHydrator implements Hydrator<EntityStackLevel, HydratedLevel> {
|
|
57
|
-
private _activeStack: ActiveStack
|
|
58
57
|
private _orchestrator: Orchestrator
|
|
59
58
|
private _signalBus: SignalBus | null
|
|
60
59
|
private _levels: HydratedLevel[] = []
|
|
@@ -62,11 +61,10 @@ export class StackHydrator implements Hydrator<EntityStackLevel, HydratedLevel>
|
|
|
62
61
|
private _unsubscribe: (() => void) | null = null
|
|
63
62
|
|
|
64
63
|
constructor(
|
|
65
|
-
|
|
64
|
+
_activeStack: ActiveStack,
|
|
66
65
|
orchestrator: Orchestrator,
|
|
67
66
|
signalBus: SignalBus | null = null
|
|
68
67
|
) {
|
|
69
|
-
this._activeStack = activeStack
|
|
70
68
|
this._orchestrator = orchestrator
|
|
71
69
|
this._signalBus = signalBus
|
|
72
70
|
|
|
@@ -28,18 +28,23 @@ interface SelectOption {
|
|
|
28
28
|
|
|
29
29
|
interface FieldConfig {
|
|
30
30
|
name: string
|
|
31
|
-
|
|
31
|
+
/** Known widget types get dedicated rendering; other strings (e.g. from
|
|
32
|
+
* ResolvedFieldConfig, #1387) fall back to the text input. */
|
|
33
|
+
type?: InputType | (string & {})
|
|
32
34
|
placeholder?: string
|
|
33
35
|
disabled?: boolean
|
|
34
36
|
readonly?: boolean
|
|
35
|
-
options?: SelectOption[]
|
|
37
|
+
options?: SelectOption[] | unknown[]
|
|
36
38
|
optionLabel?: string
|
|
37
39
|
optionValue?: string
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const props = defineProps({
|
|
41
43
|
field: { type: Object as PropType<FieldConfig>, required: true },
|
|
42
|
-
|
|
44
|
+
// unknown: v-model over Record<string, unknown> indexes (the documented
|
|
45
|
+
// generateFields loop) must bind without casts (#1387)
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
modelValue: { type: null as unknown as PropType<any>, default: null },
|
|
43
48
|
hint: { type: String as () => InputType | null, default: null } // Optional type hint override
|
|
44
49
|
})
|
|
45
50
|
|
|
@@ -48,7 +53,7 @@ const emit = defineEmits<{
|
|
|
48
53
|
}>()
|
|
49
54
|
|
|
50
55
|
const value = computed<ModelValue>({
|
|
51
|
-
get: (): ModelValue => props.modelValue,
|
|
56
|
+
get: (): ModelValue => (props.modelValue ?? null) as ModelValue,
|
|
52
57
|
set: (v: ModelValue): void => emit('update:modelValue', v)
|
|
53
58
|
})
|
|
54
59
|
|
|
@@ -64,17 +69,17 @@ const numberValue = computed<number | null>({
|
|
|
64
69
|
})
|
|
65
70
|
|
|
66
71
|
const booleanValue = computed<boolean>({
|
|
67
|
-
get: (): boolean => (props.modelValue as boolean) ?? false,
|
|
72
|
+
get: (): boolean => (props.modelValue as boolean | null | undefined) ?? false,
|
|
68
73
|
set: (v: boolean): void => emit('update:modelValue', v)
|
|
69
74
|
})
|
|
70
75
|
|
|
71
76
|
// Resolve component type: field.type > hint > 'text'
|
|
72
|
-
const inputType = computed<InputType>(() => props.field?.type || props.hint || 'text')
|
|
77
|
+
const inputType = computed<InputType>(() => (props.field?.type as InputType) || props.hint || 'text')
|
|
73
78
|
|
|
74
79
|
// Date value with string ↔ Date conversion for DatePicker
|
|
75
80
|
const dateValue = computed<Date | null>({
|
|
76
81
|
get: (): Date | null => {
|
|
77
|
-
const v = props.modelValue
|
|
82
|
+
const v = props.modelValue as string | Date | null
|
|
78
83
|
if (!v) return null
|
|
79
84
|
if (v instanceof Date) return v
|
|
80
85
|
// Convert ISO string to Date
|
|
@@ -16,6 +16,7 @@ import { ref, watch, onMounted, onUnmounted, computed, inject, provide, useSlots
|
|
|
16
16
|
import { SIDEBAR_COLLAPSED_KEY } from '../../composables/useSidebarState'
|
|
17
17
|
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
|
|
18
18
|
import { useNavigation, type NavSection } from '../../composables/useNavigation'
|
|
19
|
+
import { prettifyRole } from '../../utils/formatters'
|
|
19
20
|
import { useApp } from '../../composables/useApp'
|
|
20
21
|
import { useAuth } from '../../composables/useAuth'
|
|
21
22
|
import { useGuardDialog } from '../../composables/useGuardStore'
|
|
@@ -210,7 +211,9 @@ const userDisplayName = computed<string>(() => {
|
|
|
210
211
|
|
|
211
212
|
const userSubtitle = computed<string>(() => {
|
|
212
213
|
const userData = user.value as UserData | null
|
|
213
|
-
|
|
214
|
+
if (userData?.email) return userData.email
|
|
215
|
+
// raw ROLE_* constants are ugly by default — prettify (#1388)
|
|
216
|
+
return userData?.role ? prettifyRole(userData.role) : ''
|
|
214
217
|
})
|
|
215
218
|
|
|
216
219
|
async function handleLogout(): Promise<void> {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { computed, type ComputedRef } from 'vue'
|
|
11
11
|
import { useRouter } from 'vue-router'
|
|
12
|
+
import { prettifyRole } from '../../../utils/formatters'
|
|
12
13
|
import { useAuth } from '../../../composables/useAuth'
|
|
13
14
|
import Button from 'primevue/button'
|
|
14
15
|
|
|
@@ -37,7 +38,9 @@ const userDisplayName: ComputedRef<string> = computed(() => {
|
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
const userSubtitle: ComputedRef<string> = computed(() => {
|
|
40
|
-
|
|
41
|
+
if (userData.value?.email) return userData.value.email
|
|
42
|
+
// raw ROLE_* constants are ugly by default — prettify (#1388)
|
|
43
|
+
return userData.value?.role ? prettifyRole(userData.value.role) : ''
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
function handleLogout(): void {
|
|
@@ -62,6 +62,8 @@ interface FieldConfig {
|
|
|
62
62
|
dateFormat?: string
|
|
63
63
|
currencyCode?: string
|
|
64
64
|
locale?: string
|
|
65
|
+
/** false → no locale digit grouping for number fields (years, ids) (#1388) */
|
|
66
|
+
useGrouping?: boolean
|
|
65
67
|
booleanLabels?: { true: string; false: string }
|
|
66
68
|
// Reference options
|
|
67
69
|
reference?: string
|
|
@@ -107,7 +109,10 @@ const numberValue = computed(() => {
|
|
|
107
109
|
if (isEmpty.value) return '-'
|
|
108
110
|
const num = Number(props.value)
|
|
109
111
|
if (isNaN(num)) return String(props.value)
|
|
110
|
-
|
|
112
|
+
// useGrouping: false → no locale digit grouping (years, ids… "1965",
|
|
113
|
+
// not "1 965") (#1388)
|
|
114
|
+
const opts = props.field.useGrouping === false ? { useGrouping: false } : {}
|
|
115
|
+
return formatNumber(num, opts, props.field.locale)
|
|
111
116
|
})
|
|
112
117
|
|
|
113
118
|
// Format currency value
|
|
@@ -27,7 +27,9 @@ const props = defineProps({
|
|
|
27
27
|
// Field config (for auto-display mode)
|
|
28
28
|
field: { type: Object as PropType<FieldConfig | null>, default: null },
|
|
29
29
|
// Value (for auto-display mode)
|
|
30
|
-
|
|
30
|
+
// unknown: show.data.value?.[f.name] over the default Record generic (#1387)
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
value: { type: null as unknown as PropType<any>, default: null },
|
|
31
33
|
// Manual props (override field config)
|
|
32
34
|
name: { type: String, default: '' },
|
|
33
35
|
label: { type: String, default: '' },
|
|
@@ -61,8 +61,10 @@ export function useAuth(): UseAuthReturn {
|
|
|
61
61
|
throw new Error('[qdadm] useAuth() must be called within component setup()')
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// null defaults: a no-auth app must not log "injection not found" warnings
|
|
65
|
+
// on every page (#1388)
|
|
66
|
+
const auth = inject<AuthAdapter | null>('authAdapter', null)
|
|
67
|
+
const features = inject<FeaturesConfig | null>('qdadmFeatures', null)
|
|
66
68
|
|
|
67
69
|
// If auth disabled or not provided, return neutral values
|
|
68
70
|
if (!features?.auth || !auth) {
|
|
@@ -271,7 +271,7 @@ export interface UseEntityItemFormPageReturn<T extends Record<string, unknown> =
|
|
|
271
271
|
// Parent chain (from route.meta.parent, supports N-level nesting)
|
|
272
272
|
parentConfig: ComputedRef<ParentConfig | null>
|
|
273
273
|
parentId: ComputedRef<string | number | null>
|
|
274
|
-
parentData: ComputedRef<unknown | null>
|
|
274
|
+
parentData: ComputedRef<Record<string, unknown> | null>
|
|
275
275
|
parentChain: Ref<Map<number, unknown>>
|
|
276
276
|
getChainDepth: (config?: ParentConfig | null) => number
|
|
277
277
|
|
|
@@ -103,8 +103,8 @@ export interface UseEntityItemPageReturn<T = unknown> {
|
|
|
103
103
|
// Parent chain
|
|
104
104
|
parentConfig: ComputedRef<ParentConfig | null>
|
|
105
105
|
parentId: ComputedRef<string | number | null>
|
|
106
|
-
parentData: ComputedRef<unknown | null>
|
|
107
|
-
parentChain: Ref<Map<number, unknown
|
|
106
|
+
parentData: ComputedRef<Record<string, unknown> | null>
|
|
107
|
+
parentChain: Ref<Map<number, Record<string, unknown>>>
|
|
108
108
|
parentLoading: Ref<boolean>
|
|
109
109
|
loadParent: () => Promise<void>
|
|
110
110
|
loadParentChain: () => Promise<void>
|
|
@@ -189,7 +189,7 @@ export function useEntityItemPage<T = unknown>(config: UseEntityItemPageOptions<
|
|
|
189
189
|
* Loaded parent entities data (Map: level -> data)
|
|
190
190
|
* Level 1 = immediate parent, Level 2 = grandparent, etc.
|
|
191
191
|
*/
|
|
192
|
-
const parentChain = ref<Map<number, unknown
|
|
192
|
+
const parentChain = ref<Map<number, Record<string, unknown>>>(new Map())
|
|
193
193
|
const parentLoading = ref(false)
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -228,7 +228,7 @@ export function useEntityItemPage<T = unknown>(config: UseEntityItemPageOptions<
|
|
|
228
228
|
if (!parentConfig.value || !parentId.value) return
|
|
229
229
|
|
|
230
230
|
parentLoading.value = true
|
|
231
|
-
const newChain = new Map<number, unknown
|
|
231
|
+
const newChain = new Map<number, Record<string, unknown>>()
|
|
232
232
|
|
|
233
233
|
try {
|
|
234
234
|
// Calculate total depth to set correct breadcrumb levels
|
|
@@ -251,7 +251,7 @@ export function useEntityItemPage<T = unknown>(config: UseEntityItemPageOptions<
|
|
|
251
251
|
|
|
252
252
|
const loadedData = await parentManager.get(parentEntityId)
|
|
253
253
|
if (loadedData) {
|
|
254
|
-
newChain.set(level, loadedData)
|
|
254
|
+
newChain.set(level, loadedData as Record<string, unknown>)
|
|
255
255
|
|
|
256
256
|
// Update active stack
|
|
257
257
|
hydrator.setEntityData(currentConfig.entity, loadedData)
|
|
@@ -257,7 +257,7 @@ interface ConfirmService {
|
|
|
257
257
|
}) => void
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
export function useEntityItemShowPage<T = unknown
|
|
260
|
+
export function useEntityItemShowPage<T = Record<string, unknown>>(
|
|
261
261
|
options: UseEntityItemShowPageOptions<T>
|
|
262
262
|
): UseEntityItemShowPageReturn<T> {
|
|
263
263
|
const { entity, loadOnMount = true, transformLoad, onLoadSuccess, onLoadError } = options
|
|
@@ -409,10 +409,19 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
409
409
|
)
|
|
410
410
|
for (const sibling of siblings) {
|
|
411
411
|
const sibManager = sibling.meta?.entity ? getManager(sibling.meta.entity as string) : null
|
|
412
|
+
// Only pass the params the sibling path declares — forwarding the full
|
|
413
|
+
// current params (e.g. the child item id on an edit page) triggers
|
|
414
|
+
// vue-router's "Discarded invalid param(s)" warning (#1388)
|
|
415
|
+
const declared = new Set(
|
|
416
|
+
(String(sibling.path || '').match(/:([A-Za-z0-9_]+)/g) || []).map((m) => m.slice(1))
|
|
417
|
+
)
|
|
418
|
+
const params = declared.size
|
|
419
|
+
? Object.fromEntries(Object.entries(route.params).filter(([k]) => declared.has(k)))
|
|
420
|
+
: route.params
|
|
412
421
|
links.push({
|
|
413
422
|
label:
|
|
414
423
|
(sibling.meta?.navLabel as string) || sibManager?.labelPlural || (sibling.name as string),
|
|
415
|
-
to: { name: sibling.name as string, params:
|
|
424
|
+
to: { name: sibling.name as string, params: params as RouteParamsRawGeneric },
|
|
416
425
|
active: route.name === sibling.name,
|
|
417
426
|
})
|
|
418
427
|
}
|
|
@@ -53,6 +53,8 @@ export interface SecurityCheckerConfig {
|
|
|
53
53
|
export class SecurityChecker {
|
|
54
54
|
protected _rolesProvider: RoleProvider
|
|
55
55
|
readonly getCurrentUser: () => AuthUser | null
|
|
56
|
+
/** Roles already warned about (warn-once, #1388) */
|
|
57
|
+
protected _warnedRoles = new Set<string>()
|
|
56
58
|
|
|
57
59
|
constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
|
|
58
60
|
if (rolesProvider) {
|
|
@@ -134,6 +136,7 @@ export class SecurityChecker {
|
|
|
134
136
|
const rolePerms = this._rolesProvider.getPermissions(r)
|
|
135
137
|
rolePerms.forEach((p) => perms.add(p))
|
|
136
138
|
}
|
|
139
|
+
this._warnUnknownRole(role)
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
// User-specific permission overrides
|
|
@@ -144,6 +147,26 @@ export class SecurityChecker {
|
|
|
144
147
|
return [...perms]
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A user role that matches NO configured role silently yields zero
|
|
152
|
+
* permissions (e.g. `admin` instead of `ROLE_ADMIN`) — surface it once
|
|
153
|
+
* per role so the misconfiguration is debuggable (#1388).
|
|
154
|
+
*/
|
|
155
|
+
protected _warnUnknownRole(role: string): void {
|
|
156
|
+
if (this._warnedRoles.has(role)) return
|
|
157
|
+
const known = this._rolesProvider.getRoles()
|
|
158
|
+
if (known.includes(role)) return
|
|
159
|
+
// Hierarchy-only roles (declared in role_hierarchy without own
|
|
160
|
+
// permissions) still count as known.
|
|
161
|
+
if (Object.keys(this._rolesProvider.getHierarchy()).includes(role)) return
|
|
162
|
+
this._warnedRoles.add(role)
|
|
163
|
+
console.warn(
|
|
164
|
+
`[qdadm] SecurityChecker: user role "${role}" matches no configured role ` +
|
|
165
|
+
`(known: ${known.join(', ') || 'none'}) — the user gets NO permissions. ` +
|
|
166
|
+
`Roles must match role_permissions/role_hierarchy keys exactly.`
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
/**
|
|
148
171
|
* Check if user can assign a role to another user
|
|
149
172
|
*/
|
|
@@ -13,6 +13,10 @@ export interface MockApiStorageOptions<T extends EntityRecord = EntityRecord> {
|
|
|
13
13
|
generateId?: () => string
|
|
14
14
|
initialData?: T[] | null
|
|
15
15
|
authCheck?: (() => boolean) | null
|
|
16
|
+
/** Override the localStorage key (default `mockapi_${entityName}_data`).
|
|
17
|
+
* Needed when several apps sharing an origin use the same entity names
|
|
18
|
+
* (e.g. multiple demos on one GitHub Pages site) (#1382). */
|
|
19
|
+
storageKey?: string
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -63,6 +67,7 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
|
|
|
63
67
|
generateId = defaultGenerateId,
|
|
64
68
|
initialData = null,
|
|
65
69
|
authCheck = null,
|
|
70
|
+
storageKey = null,
|
|
66
71
|
} = options
|
|
67
72
|
|
|
68
73
|
if (!entityName) {
|
|
@@ -72,7 +77,7 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
|
|
|
72
77
|
this.entityName = entityName
|
|
73
78
|
this.idField = idField
|
|
74
79
|
this.generateId = generateId
|
|
75
|
-
this._storageKey = `mockapi_${entityName}_data`
|
|
80
|
+
this._storageKey = storageKey || `mockapi_${entityName}_data`
|
|
76
81
|
this._data = new Map()
|
|
77
82
|
this._authCheck = authCheck
|
|
78
83
|
|
|
@@ -117,6 +117,16 @@ export function applyRegistryMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
117
117
|
|
|
118
118
|
if (!security) return
|
|
119
119
|
|
|
120
|
+
// security configured but no user source: every isGranted() falls back
|
|
121
|
+
// to permissive — a newcomer believes gating is on when it isn't (#1388)
|
|
122
|
+
if (!entityAuthAdapter) {
|
|
123
|
+
console.warn(
|
|
124
|
+
'[Kernel] security is configured but entityAuthAdapter is not — entity ' +
|
|
125
|
+
'permission checks default to PERMISSIVE (no current user to check). ' +
|
|
126
|
+
'Pass entityAuthAdapter (e.g. `() => authAdapter.getUser()`) to enable gating.'
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
let rolesProvider = security.rolesProvider
|
|
121
131
|
if (!rolesProvider && (security.role_permissions || security.role_hierarchy)) {
|
|
122
132
|
rolesProvider = new StaticRoleProvider({
|
|
@@ -21,7 +21,9 @@ export interface AuthAdapter {
|
|
|
21
21
|
getToken?(): string | null
|
|
22
22
|
logout?(): void
|
|
23
23
|
connectSignals?(signals: SignalBus): () => void
|
|
24
|
-
|
|
24
|
+
// NOTE: no string index signature here — class instances (e.g. a
|
|
25
|
+
// LocalStorageSessionAuthAdapter subclass, the documented pattern) do not
|
|
26
|
+
// satisfy implicit index signatures and would stop being assignable (#1387).
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -161,7 +163,12 @@ export interface KernelOptions {
|
|
|
161
163
|
storageResolver?: (config: unknown, entityName: string) => unknown
|
|
162
164
|
managerResolver?: (config: unknown, entityName: string, context: unknown) => EntityManager
|
|
163
165
|
authAdapter?: AuthAdapter | null
|
|
164
|
-
entityAuthAdapter?:
|
|
166
|
+
entityAuthAdapter?:
|
|
167
|
+
| EntityAuthAdapter
|
|
168
|
+
| string
|
|
169
|
+
| Record<string, unknown>
|
|
170
|
+
| (() => Record<string, unknown> | null)
|
|
171
|
+
| null
|
|
165
172
|
authTypes?: Record<string, unknown>
|
|
166
173
|
pages?: Pages
|
|
167
174
|
homeRoute?: HomeRoute
|
|
@@ -151,7 +151,6 @@ export class ModuleLoadError extends Error {
|
|
|
151
151
|
*/
|
|
152
152
|
class ObjectModuleAdapter implements ModuleLike {
|
|
153
153
|
private _def: ObjectModuleDefinition
|
|
154
|
-
private _ctx: ModuleContext | null = null
|
|
155
154
|
|
|
156
155
|
constructor(def: ObjectModuleDefinition) {
|
|
157
156
|
this._def = def
|
|
@@ -177,7 +176,6 @@ class ObjectModuleAdapter implements ModuleLike {
|
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
async connect(ctx: ModuleContext): Promise<void> {
|
|
180
|
-
this._ctx = ctx
|
|
181
179
|
if (typeof this._def.connect === 'function') {
|
|
182
180
|
await this._def.connect(ctx)
|
|
183
181
|
}
|
|
@@ -187,7 +185,6 @@ class ObjectModuleAdapter implements ModuleLike {
|
|
|
187
185
|
if (typeof this._def.disconnect === 'function') {
|
|
188
186
|
await this._def.disconnect()
|
|
189
187
|
}
|
|
190
|
-
this._ctx = null
|
|
191
188
|
}
|
|
192
189
|
}
|
|
193
190
|
|
|
@@ -201,7 +198,6 @@ class ObjectModuleAdapter implements ModuleLike {
|
|
|
201
198
|
class ClassModuleAdapter implements ModuleLike {
|
|
202
199
|
private _ClassDef: ModuleClassConstructor
|
|
203
200
|
private _instance: ModuleLike
|
|
204
|
-
private _ctx: ModuleContext | null = null
|
|
205
201
|
|
|
206
202
|
constructor(ClassDef: ModuleClassConstructor) {
|
|
207
203
|
this._ClassDef = ClassDef
|
|
@@ -237,7 +233,6 @@ class ClassModuleAdapter implements ModuleLike {
|
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
async connect(ctx: ModuleContext): Promise<void> {
|
|
240
|
-
this._ctx = ctx
|
|
241
236
|
if (typeof this._instance.connect === 'function') {
|
|
242
237
|
await this._instance.connect(ctx)
|
|
243
238
|
}
|
|
@@ -247,7 +242,6 @@ class ClassModuleAdapter implements ModuleLike {
|
|
|
247
242
|
if (typeof this._instance.disconnect === 'function') {
|
|
248
243
|
await this._instance.disconnect()
|
|
249
244
|
}
|
|
250
|
-
this._ctx = null
|
|
251
245
|
}
|
|
252
246
|
}
|
|
253
247
|
|
|
@@ -434,8 +434,8 @@ export function createOrchestrator(options?: OrchestratorOptions): Orchestrator
|
|
|
434
434
|
// ============================================================================
|
|
435
435
|
type _AssertManagerLike<T extends EntityManagerLike> = T
|
|
436
436
|
type _AssertOrchestratorLike<T extends OrchestratorLike> = T
|
|
437
|
-
//
|
|
438
|
-
type _LikeChecks = [
|
|
437
|
+
// (exported so strict consumer typechecks don't flag it unused, #1386)
|
|
438
|
+
export type _LikeChecks = [
|
|
439
439
|
_AssertManagerLike<EntityManager>,
|
|
440
440
|
_AssertOrchestratorLike<Orchestrator>,
|
|
441
441
|
]
|
package/src/query/FilterQuery.ts
CHANGED
|
@@ -107,7 +107,6 @@ export class FilterQuery<T = unknown> {
|
|
|
107
107
|
toQuery: ((value: unknown) => Record<string, unknown>) | null
|
|
108
108
|
|
|
109
109
|
private _options: FilterOption[] | null = null
|
|
110
|
-
private _signals: SignalBusLike | null = null
|
|
111
110
|
private _subscriptions: Array<() => void> = []
|
|
112
111
|
|
|
113
112
|
constructor(options: FilterQueryOptions<T>) {
|
|
@@ -164,8 +163,6 @@ export class FilterQuery<T = unknown> {
|
|
|
164
163
|
// Clean up existing subscriptions if any
|
|
165
164
|
this._cleanupSubscriptions()
|
|
166
165
|
|
|
167
|
-
this._signals = signals
|
|
168
|
-
|
|
169
166
|
if (!signals) return this
|
|
170
167
|
|
|
171
168
|
// Subscribe to entity CRUD signals for cache invalidation
|
|
@@ -200,7 +197,6 @@ export class FilterQuery<T = unknown> {
|
|
|
200
197
|
*/
|
|
201
198
|
dispose(): void {
|
|
202
199
|
this._cleanupSubscriptions()
|
|
203
|
-
this._signals = null
|
|
204
200
|
this._options = null
|
|
205
201
|
}
|
|
206
202
|
|
|
@@ -88,7 +88,6 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
88
88
|
private _inheritsField: string
|
|
89
89
|
|
|
90
90
|
private _cache: RoleCache | null = null
|
|
91
|
-
private _ctx: RoleProviderContext | null = null
|
|
92
91
|
private _orchestrator: Orchestrator | null = null
|
|
93
92
|
private _signalCleanup: (() => void) | null = null
|
|
94
93
|
private _loading: Promise<void> | null = null
|
|
@@ -106,7 +105,6 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
106
105
|
* Install adapter (called by Kernel after orchestrator ready)
|
|
107
106
|
*/
|
|
108
107
|
install(ctx: RoleProviderContext): void {
|
|
109
|
-
this._ctx = ctx
|
|
110
108
|
this._orchestrator = ctx.orchestrator as Orchestrator
|
|
111
109
|
|
|
112
110
|
// Auto-invalidate on role changes
|
|
@@ -125,7 +123,6 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
125
123
|
this._signalCleanup()
|
|
126
124
|
this._signalCleanup = null
|
|
127
125
|
}
|
|
128
|
-
this._ctx = null
|
|
129
126
|
this._orchestrator = null
|
|
130
127
|
this._cache = null
|
|
131
128
|
this._loading = null
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Type companion for the `@quazardous/qdadm/styles` export (#1386).
|
|
2
|
+
// The export resolves to a .scss file, which TypeScript cannot see through
|
|
3
|
+
// the exports map — this empty module makes the side-effect import
|
|
4
|
+
// `import '@quazardous/qdadm/styles'` typecheck in strict consumers.
|
|
5
|
+
export {}
|
package/src/types/index.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface FieldConfig {
|
|
|
84
84
|
type: FieldType
|
|
85
85
|
label?: string
|
|
86
86
|
required?: boolean
|
|
87
|
+
/** number fields: false → no locale digit grouping in displays (#1388) */
|
|
88
|
+
useGrouping?: boolean
|
|
87
89
|
default?: unknown | (() => unknown)
|
|
88
90
|
options?: Array<{ label: string; value: unknown }>
|
|
89
91
|
reference?: {
|
|
@@ -108,6 +110,10 @@ export interface FieldConfig {
|
|
|
108
110
|
export interface ChildConfig {
|
|
109
111
|
entity: string
|
|
110
112
|
endpoint?: string
|
|
113
|
+
/** FK field on the child pointing at this entity (documented README shape, #1387) */
|
|
114
|
+
foreignKey?: string
|
|
115
|
+
/** Label for child navlinks/tabs */
|
|
116
|
+
label?: string
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
/**
|
package/src/utils/formatters.ts
CHANGED
|
@@ -203,3 +203,17 @@ export function formatPercent(value: number | null | undefined, decimals = 1): s
|
|
|
203
203
|
if (value === null || value === undefined) return '-'
|
|
204
204
|
return `${(value * 100).toFixed(decimals)}%`
|
|
205
205
|
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Prettify a role constant for display when no role_labels entry exists:
|
|
209
|
+
* ROLE_SUPER_USER → "Super User" (#1388).
|
|
210
|
+
*/
|
|
211
|
+
export function prettifyRole(role: string): string {
|
|
212
|
+
return role
|
|
213
|
+
.replace(/^ROLE_/, '')
|
|
214
|
+
.toLowerCase()
|
|
215
|
+
.split('_')
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
218
|
+
.join(' ')
|
|
219
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm Vite consumer plugin (#1385)
|
|
3
|
+
*
|
|
4
|
+
* qdadm publishes raw TS/.vue sources. Without help, Vite pre-bundles the
|
|
5
|
+
* host app's `primevue` import into `.vite/deps` while qdadm's raw sources
|
|
6
|
+
* import `node_modules/primevue/...` directly — two module instances whose
|
|
7
|
+
* injection symbols don't match, and the app dies at boot with
|
|
8
|
+
* `Error: No PrimeVue Toast provided!`.
|
|
9
|
+
*
|
|
10
|
+
* This plugin applies the required config in one line:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // vite.config.ts
|
|
14
|
+
* import { qdadmVitePlugin } from '@quazardous/qdadm/vite'
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* plugins: [vue(), qdadmVitePlugin()],
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* - `resolve.dedupe` collapses duplicate peer copies (also covers the
|
|
22
|
+
* `file:`/workspace-link scenario described in the package README).
|
|
23
|
+
* - `optimizeDeps.exclude` keeps primevue/@primeuix/themes/qdadm out of the
|
|
24
|
+
* pre-bundle so BOTH pipelines resolve the same raw modules. Excluding
|
|
25
|
+
* qdadm also keeps HMR sane when the package is consumed via a symlink.
|
|
26
|
+
* - `optimizeDeps.include` pre-bundles qdadm's CJS transitives (currently
|
|
27
|
+
* `pluralize`), which cannot be served raw as ESM. The list lives HERE —
|
|
28
|
+
* consumers should not have to track qdadm's internal CJS deps (skybot
|
|
29
|
+
* feedback on #1389). Both the nested (`@quazardous/qdadm > pluralize`,
|
|
30
|
+
* npm installs) and plain (`pluralize`, symlinked installs where the
|
|
31
|
+
* nested path can't resolve) forms are declared; the unused one is a
|
|
32
|
+
* no-op warning at worst.
|
|
33
|
+
* - When `node_modules/@quazardous/qdadm` is a symlink (file:/workspace
|
|
34
|
+
* link — qdadm's own dev mode against a testbed app), its realpath is
|
|
35
|
+
* appended to `server.fs.allow` so qdadm's `?raw` dynamic imports (i18n
|
|
36
|
+
* YAML defaults) are servable from outside the consumer's workspace
|
|
37
|
+
* root. The workspace root is re-added explicitly because a populated
|
|
38
|
+
* `fs.allow` disables Vite's implicit default.
|
|
39
|
+
*
|
|
40
|
+
* Returned partial config is deep-merged by Vite (arrays concatenate), so
|
|
41
|
+
* consumer overrides in their own `defineConfig` still apply.
|
|
42
|
+
*/
|
|
43
|
+
import fs from 'node:fs'
|
|
44
|
+
import path from 'node:path'
|
|
45
|
+
import { searchForWorkspaceRoot, type Plugin, type UserConfig } from 'vite'
|
|
46
|
+
|
|
47
|
+
/** qdadm's CJS transitive deps that must be pre-bundled (internal knowledge). */
|
|
48
|
+
const CJS_TRANSITIVES = ['pluralize']
|
|
49
|
+
|
|
50
|
+
export interface QdadmVitePluginOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Extra packages to dedupe on top of the defaults
|
|
53
|
+
* (vue, vue-router, primevue, pinia).
|
|
54
|
+
*/
|
|
55
|
+
dedupe?: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
59
|
+
return {
|
|
60
|
+
name: 'qdadm',
|
|
61
|
+
config: (userConfig: UserConfig = {}) => {
|
|
62
|
+
const root = path.resolve(userConfig.root ?? process.cwd())
|
|
63
|
+
|
|
64
|
+
// Symlinked install (file:/workspace link)? Allow serving from the
|
|
65
|
+
// link target, which typically lives outside the consumer's root.
|
|
66
|
+
const fsAllow: string[] = []
|
|
67
|
+
let symlinked = false
|
|
68
|
+
try {
|
|
69
|
+
const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm')
|
|
70
|
+
const real = fs.realpathSync(pkgDir)
|
|
71
|
+
if (real !== pkgDir) {
|
|
72
|
+
symlinked = true
|
|
73
|
+
fsAllow.push(searchForWorkspaceRoot(root), real)
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// not installed under root node_modules (aliased/hoisted) — nothing to allow
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
resolve: {
|
|
81
|
+
dedupe: ['vue', 'vue-router', 'primevue', 'pinia', ...(options.dedupe ?? [])],
|
|
82
|
+
},
|
|
83
|
+
optimizeDeps: {
|
|
84
|
+
exclude: ['primevue', '@primeuix/themes', '@quazardous/qdadm'],
|
|
85
|
+
// One form per install mode: the nested path resolves through a
|
|
86
|
+
// real node_modules chain (npm installs), the plain form through a
|
|
87
|
+
// symlink realpath — declaring the unused one logs a "Failed to
|
|
88
|
+
// resolve dependency" warning (skybot testbed, #1389).
|
|
89
|
+
include: symlinked
|
|
90
|
+
? [...CJS_TRANSITIVES]
|
|
91
|
+
: CJS_TRANSITIVES.map((dep) => `@quazardous/qdadm > ${dep}`),
|
|
92
|
+
},
|
|
93
|
+
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|